paint-brush
Phản hồi giao diện người dùng: Tạo Thanh tiến trình Retro cho iOS bằng cách sử dụng CALayersby@maxkalik
637
637

Phản hồi giao diện người dùng: Tạo Thanh tiến trình Retro cho iOS bằng cách sử dụng CALayers

Max Kalik22m2023/12/17
Read on Terminal Reader

Hướng dẫn này khám phá cách tạo thanh tiến trình theo phong cách cổ điển trong iOS, sử dụng CALayers để tạo cảm giác hoài cổ. Nó bao gồm quy trình từng bước, từ thiết kế từng lớp đến triển khai hoạt ảnh, cung cấp thông tin chuyên sâu về việc kết hợp tính thẩm mỹ cổ điển với quá trình phát triển iOS hiện đại. Lý tưởng cho các nhà phát triển và nhà thiết kế giao diện người dùng, bài viết cung cấp cả chiều sâu kỹ thuật và nguồn cảm hứng sáng tạo cho các thành phần giao diện người dùng độc đáo. Mã nguồn: https://github.com/maxkalik/RetroProgressBar
featured image - Phản hồi giao diện người dùng: Tạo Thanh tiến trình Retro cho iOS bằng cách sử dụng CALayers
Max Kalik HackerNoon profile picture
0-item


Tôi thích xây dựng các thành phần giao diện người dùng. Nó mang lại cho tôi một loại niềm vui thẩm mỹ khi xem và sử dụng chúng sau khi xây dựng. Điều này cũng kết hợp với tình yêu của tôi dành cho hệ sinh thái MacOS/iOS nói chung. Gần đây, tôi được giao nhiệm vụ tạo thanh tiến trình tùy chỉnh bằng UIKit . Đó không phải là vấn đề lớn, nhưng có lẽ 99% nhà phát triển iOS đã tạo ra một iOS từ đầu vào một thời điểm nào đó trong sự nghiệp của họ. Trong trường hợp của tôi, tôi muốn làm nhiều hơn là chỉ tạo một vài CALayer với hình ảnh động dựa trên phạm vi giá trị từ 0 đến 1. Tôi đang hướng tới một giải pháp phù hợp với nhiều tình huống cần có thanh tiến trình .


Tôi muốn quay lại thời kỳ mà các thành phần giao diện người dùng trong thế giới Apple trông như thế này:


nguồn: https://www.smashingmagazine.com/2010/08/free-wireframing-kits-ui-design-kits-pdfs-and-resources/


Phản ánh về sự phát triển của thiết kế giao diện người dùng, đã gần một thập kỷ kể từ khi các thành phần giao diện người dùng trong thế giới Apple áp dụng thiết kế phẳng với sự ra đời của iOS 7 vào năm 2014. Vì vậy, hãy cùng đắm chìm trong hoài niệm về các phiên bản Mac OS và iOS trước đó và tạo ra một thanh tiến trình đẹp cùng nhau.


Nếu bạn thích sử dụng UIKit nhưng vẫn chưa chắc chắn về cách thao tác với CALayers thì bài viết này sẽ hữu ích cho bạn. Tại đây, chúng ta sẽ xem xét các thử thách về bố cục, hoạt ảnh và vẽ điển hình, đồng thời cung cấp thông tin chi tiết và giải pháp để trợ giúp bạn trong quá trình thực hiện.

Từ UIView đi sâu vào API CALayers.

Thanh tiến trình có vẻ đơn giản, nhưng nếu chúng ta đi sâu vào chi tiết, bạn sẽ hiểu rằng nó yêu cầu rất nhiều lớp con.


Thanh tiến trình cổ điển


Tôi sẽ không đi sâu vào chi tiết về CALayers ở đây. Bạn có thể xem bài viết này trong đó tôi mô tả ngắn gọn CALayers là gì và cách chúng hoạt động: https://hackernoon.com/rolling-numbers-animation-USE-only-calayers .


Vì vậy, hãy chia nó thành các lớp nhỏ:

  1. Lớp nền chung
  2. Lớp mặt nạ (Phần hoạt hình)
  3. Lớp nền tiến trình
  4. Lớp chói
  5. Lớp lung linh


Lớp nền có thêm hai lớp con:

  1. Lớp bóng bên trong
  2. Lớp chuyển màu viền


Hãy cấu trúc nó đúng cách:


 RetroProgressBar [UIView] |--General background [CALayer] |--Inner Shadow [CALayer] |--Border Gradient [CALayer] |--Progress Background Layer [CALayer] |--Animated Mask Layer [CALayer] |--Glare [CALayer] |--Shimering [CALayer]


Tóm lại, đây là cách nó hoạt động: Thanh tiến trình sẽ phản ứng với giá trị nằm trong khoảng từ 0,0 đến 1,0. Khi giá trị thay đổi, chúng ta cần điều chỉnh độ rộng của lớp dựa trên giá trị này:


 bounds.width * value


Sử dụng phép tính đơn giản này, chúng ta có thể sửa đổi độ rộng bằng CATransaction . Nhưng trước khi làm điều này, chúng ta cần hiểu lớp nào sẽ thay đổi khi giá trị thay đổi. Hãy xem lại cấu trúc và xác định các lớp cần đáp ứng với những thay đổi về giá trị.


 RetroProgressBar [UIView] |--General background [CALayer] |--Inner Shadow [CALayer] |--Border Gradient [CALayer] |--Progress Background Layer [CALayer] |--Animated Mask Layer [CALayer] : [Animated Width] |--Glare [CALayer] : [Animated Width] |--Shimering [CALayer] : [Animated Width]


Có vẻ như chúng ta cần phát triển một giao thức có thể được áp dụng bởi các lớp cần độ rộng hoạt ảnh dựa trên giá trị.

Giao thức ProgressAnimatable

Trong giao thức, chúng ta chỉ cần hai phương pháp: thay đổi độ rộng của lớp có hoạt ảnh và không có hoạt ảnh.


 protocol ProgressAnimatable: CALayer { /// Sets the layer's width instantly to the specified value. func setToWidth(_ width: CGFloat) /// Animates the layer's width to the specified value. func animateToWidth(_ width: CGFloat, duration: TimeInterval, animationType: CAMediaTimingFunctionName, completion: (() -> Void)?) }


Thực hiện:


 extension ProgressAnimatable { func setToWidth(_ width: CGFloat) { guard width >= 0 else { return } removeAllAnimations() CATransaction.begin() CATransaction.setDisableActions(true) self.bounds.size.width = width CATransaction.commit() } func animateToWidth(_ width: CGFloat, duration: TimeInterval, animationType: CAMediaTimingFunctionName = .easeInEaseOut, completion: (() -> Void)? = nil) { guard width >= 0, width != self.bounds.width else { completion?() return } let animation = CABasicAnimation(keyPath: "bounds.size.width") animation.fromValue = bounds.width animation.toValue = width animation.duration = duration animation.fillMode = .forwards animation.isRemovedOnCompletion = false animation.timingFunction = CAMediaTimingFunction(name: animationType) CATransaction.begin() CATransaction.setCompletionBlock { self.bounds.size.width = width completion?() } self.add(animation, forKey: "widthChange") CATransaction.commit() } }


Cả hai phương pháp đều sử dụng CATransaction . Phương pháp đầu tiên có vẻ kỳ quặc vì chúng ta chỉ cần thay đổi một giá trị mà không cần hoạt ảnh. Vậy tại sao CATransaction lại cần thiết?


Hãy thử tạo CALayer cơ bản và thay đổi chiều rộng hoặc chiều cao của nó mà không có bất kỳ hoạt ảnh nào. Khi chạy một bản dựng, bạn sẽ nhận thấy CALayer thay đổi kèm theo hình ảnh động. Sao có thể như thế được? Trong Core Animation, các thay đổi đối với thuộc tính lớp (chẳng hạn như giới hạn, vị trí, độ mờ, v.v.) thường được tạo hoạt ảnh dựa trên hành động mặc định được liên kết với thuộc tính đó. Hành vi mặc định này nhằm mục đích cung cấp các chuyển tiếp trực quan mượt mà mà không cần hoạt ảnh rõ ràng cho mỗi thay đổi thuộc tính.


Điều này có nghĩa là chúng ta cần vô hiệu hóa rõ ràng các hành động trong CALayer . Bằng cách đặt CATransaction.setDisableActions(true) , bạn đảm bảo rằng chiều rộng của lớp được cập nhật ngay lập tức thành giá trị mới mà không cần bất kỳ chuyển đổi hoạt ảnh trung gian nào.


Phương pháp thứ hai cung cấp một cách linh hoạt và có thể tái sử dụng để tạo hiệu ứng cho chiều rộng của bất kỳ lớp nào tuân theo giao thức ProgressAnimatable . Nó cung cấp khả năng kiểm soát thời lượng và nhịp độ của hoạt ảnh, đồng thời bao gồm tùy chọn cho hành động hoàn thành. Điều này làm cho nó rất phù hợp với các nhu cầu hoạt hình khác nhau trong khung UIKit.


Các ứng cử viên của chúng tôi tuân thủ giao thức này sẽ có ba lớp: Lớp mặt nạ hoạt hình, Lớp chói và Lớp lung linh. Hãy tiếp tục và thực hiện nó.

Lớp mặt nạ tiến độ

Đã đến lúc tuân thủ giao thức của chúng tôi và lớp quan trọng nhất trong thiết lập này sẽ là CAShapeLayer chung của chúng tôi. Tại sao chúng ta cần mặt nạ này? Tại sao nó không thể chỉ là một CALayer có chiều rộng hoạt ảnh? Với tư cách là Kỹ sư iOS , đặc biệt là người tập trung vào phát triển giao diện người dùng, bạn thường cần dự đoán các trường hợp sử dụng tiềm năng cho các thành phần của mình. Trong trường hợp của tôi, có Lớp nền tiến trình. Đây không phải là CALayer hoạt hình, nhưng nếu nó được tạo hoạt hình bằng thứ gì đó như CAReplicatorLayer thì sao? Nếu bạn lo ngại về hiệu suất, bạn có thể cân nhắc rằng một lớp như vậy sẽ được hiển thị một lần và sau đó chỉ cần thực hiện hoạt ảnh của nó. Đó là lý do tại sao mặt nạ này có thể cực kỳ hữu ích. Nó cho phép hiển thị hiệu quả trong khi vẫn mang lại sự linh hoạt cần thiết cho các tình huống hoạt hình khác nhau.


 final class ProgressMaskLayer: CAShapeLayer, ProgressAnimatable { override init() { super.init() backgroundColor = UIColor.black.cgColor anchorPoint = CGPoint(x: 0, y: 0.5) } override init(layer: Any) { super.init(layer: layer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } }


Chúng ta hãy nhìn vào dòng này:


 anchorPoint = CGPoint(x: 0, y: 0.5)


Dòng này rất quan trọng đối với Thanh tiến trình của chúng tôi. Theo mặc định, CALayers được căn giữa, với anchorPoint nằm ở giữa lớp chứ không phải ở giữa cạnh trái như thường được giả định. anchorPoint là điểm xung quanh đó tất cả các biến đổi của lớp xảy ra. Đối với lớp được sử dụng làm thanh tiến trình, cài đặt này thường có nghĩa là khi chiều rộng của lớp thay đổi, nó sẽ mở rộng bằng nhau theo cả hai hướng từ anchorPoint . Tuy nhiên, nếu chúng ta điều chỉnh anchorPoint ở giữa cạnh trái, lớp sẽ chỉ mở rộng sang bên phải trong khi cạnh trái vẫn cố định tại chỗ. Hành vi này rất cần thiết cho thanh tiến trình, đảm bảo rằng sự phát triển của thanh xuất hiện từ một phía chứ không phải cả hai.


 private let progressMaskLayer = ProgressMaskLayer() private let progressBackgroundLayer = ProgressBackgroundLayer() public init() { super.init(frame: .zero) layer.addSublayer(progressMaskLayer) layer.addSublayer(progressBackgroundLayer) progressBackgroundLayer.mask = progressMaskLayer }


Trong đoạn mã trên, chúng tôi khởi tạo hai phần chính của Thanh tiến trình: ProgressMaskLayerProgressBackgroundLayer . Chúng ta hãy nhìn vào cái thứ hai.


 final class ProgressBackgroundLayer: CAGradientLayer { override init() { super.init() locations = [0, 0.5, 1] } override init(layer: Any) { super.init(layer: layer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func layoutSublayers() { super.layoutSublayers() colors = [ backgroundColor?.lightened(by: 0.3), backgroundColor, backgroundColor?.darkened(by: 0.5) ].compactMap({ $0 }) } }


Như bạn có thể thấy, đó là CAGradientLayer có ba màu. Việc triển khai này biến đổi màu được chèn thành dải màu dọc, nâng cao khả năng hiển thị của thanh tiến trình và mang lại cho nó vẻ ngoài đồ sộ hơn.


Dốc

Để thực hiện được điều này, tôi đã chuẩn bị hai phương pháp trong tiện ích mở rộng CGColor . Để giải trí, tôi đã chọn không chuyển đổi CGColor trở lại UIColor mà chỉ chơi trong lĩnh vực CGColor :


 extension CGColor { func darkened(by value: CGFloat) -> CGColor { guard let colorSpace = self.colorSpace, let components = self.components, colorSpace.model == .rgb, components.count >= 3 else { return self } let multiplier = 1 - min(max(value, 0), 1) let red = components[0] * multiplier let green = components[1] * multiplier let blue = components[2] * multiplier let alpha = components.count > 3 ? components[3] : 1.0 return CGColor( colorSpace: colorSpace, components: [red, green, blue, alpha] ) ?? self } func lightened(by value: CGFloat) -> CGColor { guard let colorSpace = self.colorSpace, let components = self.components, colorSpace.model == .rgb, components.count >= 3 else { return self } let red = min(components[0] + value, 1.0) let green = min(components[1] + value, 1.0) let blue = min(components[2] + value, 1.0) let alpha = components.count > 3 ? components[3] : 1.0 return CGColor( colorSpace: colorSpace, components: [red, green, blue, alpha] ) ?? self } }


Như bạn có thể thấy, nó không quá phức tạp. Chúng ta chỉ cần lấy colorSpace , components ( red , green , bluealpha ) và model . Điều này có nghĩa là dựa trên giá trị, chúng ta có thể tính toán màu mới: đậm hơn hoặc nhạt hơn.


Lớp chói

Hãy hướng tới việc làm cho Thanh tiến trình của chúng ta trở nên 3D hơn. Bây giờ, chúng ta cần thêm một lớp khác, chúng ta sẽ gọi là Glare.


Đây là cách triển khai đơn giản nhưng phải tuân theo giao thức ProgressAnimatable .


 final class ProgressGlareLayer: CALayer, ProgressAnimatable { override init() { super.init() backgroundColor = UIColor.white.withAlphaComponent(0.3).cgColor anchorPoint = CGPoint(x: 0, y: 0.5) } override init(layer: Any) { super.init(layer: layer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } 


Với ánh sáng chói


Để đơn giản, chúng ta chỉ cần màu nền ở đây là màu trắng với alpha là 0,3.


Little Nice Shimmering (Lớp lung linh)

Tôi biết một số người có thể nói rằng MacOS X hoặc iOS trước phiên bản 6 không có các thành phần UI với hiệu ứng lung linh như thế này. Nhưng như tôi đã nói, đây giống như một bộ phim lấy cảm hứng từ một câu chuyện có thật. Sự lung linh này làm nổi bật hiệu ứng 3D và làm cho nó rõ ràng hơn.


 final class ShimmeringLayer: CAGradientLayer, ProgressAnimatable { let glowColor: UIColor = UIColor.white private func shimmerAnimation() -> CABasicAnimation { let animation = CABasicAnimation(keyPath: "locations") animation.fromValue = [-1.0, -0.5, 0.0] animation.toValue = [1.0, 1.5, 2.0] animation.duration = 1.5 animation.repeatCount = Float.infinity return animation } private func opacityAnimation() -> CABasicAnimation { let animation = CABasicAnimation(keyPath: "opacity") animation.fromValue = 1.0 animation.toValue = 0.0 animation.duration = 0.1 // Quick transition to transparent animation.beginTime = shimmerAnimation().duration animation.fillMode = .forwards animation.isRemovedOnCompletion = false return animation } private func animationGroup() -> CAAnimationGroup { let pauseDuration = 3.0 // Duration of pause between shimmering let shimmerAnimation = shimmerAnimation() let opacityAnimation = opacityAnimation() let animationGroup = CAAnimationGroup() animationGroup.animations = [shimmerAnimation, opacityAnimation] animationGroup.duration = shimmerAnimation.duration + pauseDuration animationGroup.repeatCount = Float.infinity return animationGroup } override init() { super.init() setupLayer() } override init(layer: Any) { super.init(layer: layer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func layoutSublayers() { super.layoutSublayers() startAnimation() } private func setupLayer() { let lightColor = UIColor.white.withAlphaComponent(0.7).cgColor let darkColor = UIColor.white.withAlphaComponent(0).cgColor colors = [ darkColor, lightColor, darkColor ] anchorPoint = CGPoint(x: 0, y: 0.5) locations = [0.0, 0.5, 1.0] startPoint = CGPoint(x: 0.0, y: 0.5) endPoint = CGPoint(x: 1.0, y: 0.5) shadowColor = glowColor.cgColor shadowRadius = 5.0 shadowOpacity = 1 shadowOffset = .zero } private func startAnimation() { if animation(forKey: "shimmerEffect") == nil { let animationGroup = animationGroup() add(animationGroup, forKey: "shimmerEffect") } } }


Ở đây, chúng ta cần kết hợp hai ảnh động (Độ mờ và Vị trí) thành một CAAnimationGroup . Ngoài ra, việc sử dụng CAAnimationGroup cho phép chúng tôi sắp xếp tạm dừng giữa các phiên hoạt ảnh. Tính năng này khá hữu ích vì khách hàng cũng có thể muốn kiểm soát thuộc tính này.


Với sự lung linh


Như bạn có thể thấy trong hình, điều này bổ sung thêm nhiều hiệu ứng 3D hơn, nhưng vẫn chưa đủ. Hãy tiến về phía trước.

Lớp nền

Chúng ta cần làm việc với lớp nền tĩnh nằm bên dưới Đường tiến độ hoạt hình. Nếu bạn nhớ lại, chúng ta có hai lớp con bổ sung ở đó: Inner Shadow và Border gradient.


 |--General background [CALayer] |--Inner Shadow [CALayer] |--Border Gradient [CALayer]


Tôi có thể nói rằng hai lớp này khá phổ biến trong các truy vấn tìm kiếm cách đây vài năm, có thể là do xu hướng giao diện người dùng. Hãy tạo ra những phiên bản của riêng mình và lưu giữ chúng trong bài viết này cho thế hệ tương lai :)

Lớp bóng bên trong

Lớp này được thiết kế để tạo một lớp có hiệu ứng bóng bên trong, đây có thể là một cách hấp dẫn về mặt hình ảnh để thêm chiều sâu cho các thành phần giao diện người dùng.


 final class InnerShadowLayer: CAShapeLayer { override init() { super.init() masksToBounds = true shadowRadius = 3 shadowColor = UIColor.black.cgColor shadowOffset = CGSize(width: 0.0, height: 1.0) shadowOpacity = 0.5 } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func layoutSublayers() { super.layoutSublayers() // Creates a path for the shadow that extends slightly outside the bounds of the layer. let shadowPath = UIBezierPath(roundedRect: bounds.insetBy(dx: -5, dy: -5), cornerRadius: cornerRadius) // Creates a cutout path that is the inverse of the layer's bounds. let cutoutPath = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius).reversing() shadowPath.append(cutoutPath) self.shadowPath = shadowPath.cgPath } }


Chúng tôi cần CAShapeLayer vì chúng tôi phải đặt shadowPath . Để làm được điều đó, chúng ta cần tạo một đường dẫn cho bóng hơi mở rộng ra ngoài giới hạn của lớp bằng cách sử dụng insetBy từ giới hạn. Và sau đó là một UIBezierPath khác - một đường dẫn bị cắt ngược với giới hạn của lớp.


Bóng bên trong


Trong hình minh họa, nó gần như đã ở đó. Nhưng chúng ta cần lớp cuối cùng để đưa chúng ta trở lại MacOS X Leopard.

Lớp chuyển màu viền

Đó là một lớp con của CAGradientLayer được thiết kế để tạo đường viền chuyển màu xung quanh một lớp. Nó sử dụng CAShapeLayer làm mặt nạ để đạt được hiệu ứng đường viền. shapeLayer được định cấu hình với màu nét vẽ (ban đầu là màu đen) và không có màu tô và được sử dụng làm mặt nạ cho BorderGradientLayer . Thiết lập này cho phép độ dốc chỉ hiển thị ở nơi shapeLayer có nét, tạo đường viền chuyển màu một cách hiệu quả.


 final class BorderGradientLayer: CAGradientLayer { let shapeLayer: CAShapeLayer = { let layer = CAShapeLayer() layer.strokeColor = UIColor.black.cgColor // Temporary color layer.fillColor = nil return layer }() init(borderWidth: CGFloat, colors: [UIColor]) { super.init() self.mask = shapeLayer self.colors = colors.map { $0.cgColor } self.setBorderWidth(borderWidth) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func layoutSublayers() { super.layoutSublayers() shapeLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius).cgPath } func setBorderWidth(_ borderWidth: CGFloat) { self.shapeLayer.lineWidth = borderWidth } }


Trình khởi tạo của BorderGradientLayer chấp nhận borderWidth và một mảng UIColor cho gradient. Nó thiết lập các màu gradient, áp dụng độ rộng đường viền cho shapeLayer và sử dụng shapeLayer làm mặt nạ để hạn chế độ chuyển màu cho vùng viền.


Phương thức layoutSublayers đảm bảo rằng đường dẫn của shapeLayer được cập nhật để khớp với giới hạn và bán kính góc của lớp, đảm bảo đường viền khớp với lớp một cách chính xác. Phương thức setBorderWidth cho phép điều chỉnh động độ rộng của đường viền sau khi khởi tạo.


Có viền


Tốt hơn nhiều. Được rồi. Hãy ngừng tạo ra những thứ trực quan và hãy làm việc dựa trên logic.

Tất cả cùng nhau

Bây giờ là lúc kết hợp tất cả các lớp này và đưa chúng vào cuộc sống.


 private lazy var backgroundLayer = BackgroundLayer( borderWidth: borderWidth, colors: borderColors ) private let glareLayer = ProgressGlareLayer() private let shimmeringLayer = ShimmeringLayer() private let progressMaskLayer = ProgressMaskLayer() private let progressBackgroundLayer = ProgressBackgroundLayer() public init() { super.init(frame: .zero) layer.backgroundColor = UIColor.white.cgColor layer.masksToBounds = true layer.addSublayer(progressMaskLayer) layer.addSublayer(progressBackgroundLayer) layer.insertSublayer(backgroundLayer, at: 0) progressBackgroundLayer.addSublayer(shimmeringLayer) progressBackgroundLayer.addSublayer(glareLayer) progressBackgroundLayer.mask = progressMaskLayer }


Chúng ta hãy xem cách chúng ta xử lý tất cả các lớp sẽ thay đổi độ rộng của chúng bằng hoạt ảnh:


 public func setValueWithAnimation(_ value: Double, duration: TimeInterval = 1.0, animationType: CAMediaTimingFunctionName = .easeInEaseOut, completion: (() -> Void)? = nil) { let newWidth = calculateProgressWidth(value) let animationGroup = DispatchGroup() animationGroup.enter() shimmeringLayer.animateToWidth( newWidth - WIDTH_ANIMATABLE_INSET, duration: duration, animationType: animationType, completion: animationGroup.leave ) animationGroup.enter() progressMaskLayer.animateToWidth( newWidth, duration: duration, animationType: animationType, completion: animationGroup.leave ) animationGroup.enter() glareLayer.animateToWidth( newWidth - WIDTH_ANIMATABLE_INSET, duration: duration, animationType: animationType, completion: animationGroup.leave ) animationGroup.notify(queue: .main) { completion?() } }


Đầu tiên, chúng ta cần chuẩn bị chiều rộng từ một giá trị. Giá trị phải nhỏ hơn 0 và lớn hơn 1. Ngoài ra, chúng ta cần xem xét độ rộng đường viền có thể có.


 fileprivate func normalizeValue(_ value: Double) -> Double { max(min(value, 1), 0) } private func calculateProgressWidth(_ value: Double) -> CGFloat { let normalizedValue = normalizeValue(value) let width = bounds.width * normalizedValue - borderWidth return width }


Để có hiệu ứng 3D chính xác, các lớp chói và lung linh phải có một chút đệm từ bên phải và bên trái:


 newWidth - WIDTH_ANIMATABLE_INSET


Và cuối cùng, phương thức này sử dụng một nhóm hoạt ảnh ( DispatchGroup ) để đồng bộ hóa hoạt ảnh của ba lớp: shimmeringLayer , progressMaskLayerglareLayer . Chiều rộng của mỗi lớp được tạo hiệu ứng động theo chiều rộng đã tính toán (được điều chỉnh bởi WIDTH_ANIMATABLE_INSET để shimmeringLayerglareLayer để phù hợp với thiết kế) trong khoảng thời gian được chỉ định và với đường cong hoạt ảnh được chỉ định.


Các hoạt ảnh được điều phối bằng cách sử dụng DispatchGroup , đảm bảo rằng tất cả các hoạt ảnh bắt đầu đồng thời và việc đóng hoàn thành chỉ được gọi sau khi tất cả các hoạt ảnh đã kết thúc. Phương pháp này cung cấp một cách trực quan hấp dẫn để cập nhật giá trị của thanh tiến trình bằng hoạt ảnh mượt mà, đồng bộ trên các lớp trang trí khác nhau của nó.

Mã nguồn

Phiên bản mới nhất tồn tại dưới dạng Gói Swift và Pod. Vì vậy, bạn có thể lấy nó từ đây Kho lưu trữ Retro Progress Bar . Btw, đóng góp đều được chào đón!

Cách sử dụng

Tạo một phiên bản của RetroProgressBar. Bạn có thể thêm nó vào hệ thống phân cấp chế độ xem của mình giống như với bất kỳ UIView nào.


 let progressBar = RetroProgressBar()


Tùy chỉnh:


 progressBar.progressColor = UIColor.systemBlue progressBar.cornerRadius = 5.0 progressBar.borderWidth = 2.0 progressBar.borderColors = [UIColor.white, UIColor.gray]


Để thay đổi giá trị bằng hình ảnh động:


 progressBar.setValueWithAnimation(0.75, duration: 1.0, animationType: .easeInEaseOut) { print("Animation Completed") }


Không có hình ảnh động:


 progressBar.setValue(0.5)

suy nghĩ cuối cùng

Việc tạo lại thanh tiến trình kiểu cũ này là một sự hoài niệm về tính thẩm mỹ mà tôi vô cùng nhớ. Làm việc với UIKit và CALayer nhắc nhở tôi về tính linh hoạt và sức mạnh của các công cụ của chúng tôi với tư cách là nhà phát triển. Dự án này không chỉ là triển khai kỹ thuật; đó là cuộc hành trình quay trở lại kỷ nguyên thiết kế được yêu thích, chứng minh rằng chúng ta vẫn có thể tạo ra bất cứ thứ gì, bao gồm cả sự quyến rũ vượt thời gian của các thiết kế cũ.


Trải nghiệm này là một lời nhắc nhở sâu sắc rằng trong thiết kế giao diện người dùng , sự đơn giản và hoài cổ có thể cùng tồn tại một cách đẹp đẽ, bắc cầu giữa quá khứ và hiện tại.


Chúc mừng mã hóa!