Làm cho ứng dụng của bạn đẹp hơn? Tại sao không?
Điều kiện tiên quyết
Bạn không cần phải có kiến thức chuyên sâu về phát triển ứng dụng iOS, vì vậy cứ thoải mái! Tất cả những gì bạn cần là một máy tính có MacOS và Xcode.
Đến cuối bài, chúng ta sẽ có:
- Background animation quay vòng liên tục qua các màu được xác định trước.
- Countdown progress bar nằm ở giữa.
- Pulsing animation xung quanh progress bar.
Ít thôi nhưng tôi hy vọng nó sẽ giúp được phần nào khi bạn bắt đầu với Core Animation!
Hướng dẫn
Cùng đi tiếp và tạo một ứng dụng mới trong Xcode. Trước tiên chúng ta sẽ bắt đầu với background, và làm thế nào để có được animation như vậy.
Gradient Background
1 2 3 | CAGradientLayer Một layer vẽ một dải màu trên màu nền của nó, điền vào hình dạng của layer (bao gồm các góc tròn) |
Đối với nền, chúng tôi sẽ sử dụng CAGradientLayer để tạo hiệu ứng cho nó. Chúng tôi sẽ khai báo các biến như vậy:
Đầu tiên chúng ta đã tạo ra CAGradientLayer
và cũng là một mảng sẽ chứa nhiều màu sắc khác nhau, sẽ được sử dụng làm gradient sau.
Khi View tải, chúng ta sẽ tạo chế độ view gradient như trên.
- Nối tất cả các màu có sẵn vào Set
- Đặt khung gradient để chiếm toàn bộ màn hình
- Đặt màu ban đầu được sử dụng, là chỉ số 0 trong tập gradient của chúng ta
- Chúng ta đã xác định điểm bắt đầu và điểm kết thúc sao cho gradient flow theo đường chéo từ trên trái sang dưới phải.
- Và cuối cùng, tôi chèn gradient làm first layer tiên trong sublayers.
Sau khi tôi đã thiết lập layer gradient, đây là lúc để tạo hiệu ứng cho layer!
- Bất cứ khi nào hàm này được gọi, tôi sẽ tăng chỉ số
currentGradient
lên 1 và lặp lại theo đó. - Thiết lập
CABasicAnimation
để thay đổicolors
- Set animation để animate
toValue
chỉ số màu hiện tại của tôi trong bộ của chúng tôi trong thời gian 3 giây. - Một vài dòng tiếp theo để đảm bảo rằng hình ảnh động gradient của chúng tôi không bị xóa khi hoàn thành.
- Sử dụng delegate
animationDidStop
để detect khi animation gradient của chúng ta đã hoàn thành. Nếu nó đã kết thúc, hãy update màu gradient hiện tại thành chỉ mục hiện tại của chúng ta (điều này sẽ hoạt động giống nhưfromValue
trong animation của chúng ta) và gọi lạianimateGradient ().
Tuyệt quá! Animating background thông qua CAGradientLayer
khá đơn giản! Hãy để đi trước và thêm một thanh tiến trình đếm ngược!
Countdown Progress Bar
Để đạt được hiệu quả như trên, chúng ta sẽ cần những điều sau đây:
- 3
CAShapeLayers
– Một cho background, một cho foreground và một cho pulsation - 2
CAGradientLayer
—Điều này là để có một nét với một số gradient so với mộtUIColor
rắn Timer
để theo dõi thời gian còn lại và updateUILabel
trên màn hình
Đầu tiên, tôi đã tạo một lớp và tuân thủ nó theo UIView
để tôi có thể sử dụng nó theo chương trình hoặc thông qua storyboard.
Những gì tôi đã tuyên bố ban đầu là 3 CAShapeLayer
cần thiết và một UILabel
, đó là các biến lười biếng và tôi đã cho chúng các giá trị ban đầu. Lazy có nghĩa là các closures sẽ được thực thi khi các biến này được gọi trong thời gian chạy.
Ngoài ra, vì tôi có timer
, tôi cũng đã thêm một deinit
để đảm bảo rằng timer không hợp lệ khi chế độ xem bị hủy:
1 2 3 4 | deinit { timer.invalidate() } |
Ngoài ra, vì tôi muốn foreground và pulse của mình có một số gradientc, tôi đã tạo thêm 2 biến, thuộc loại CAGradientLayer
. Chúng ta sẽ che foreground và pulse của chúng ta trên các lớp gradient để đạt được hiệu quả mà chúng ta muốn.
Tôi đã cho họ một số màu gradient cũng như vị trí của họ. Đây là tất cả các biến chúng ta cần để bắt đầu! Hàm init
gọi một phương thức loadLayers ()
để cho phép xem điều đó làm gì.
Khi chúng ta gọi phương thức, đầu tiên chúng ta tạo một circular path sẽ được CAShapeLayers
sử dụng. Vì chúng tôi muốn các foreground và pulse có gradient, tôi đã áp dụng mặt nạ tương ứng. Cuối cùng, tôi đã thêm UILabel
ở giữa View sẽ được sử dụng để hiển thị thời gian còn lại.
Animating cho layers
Bây giờ chúng ta đã thiết lập tất cả các layers, hãy để xem xét cách chúng ta có thể đạt được animations. Dưới đây cho thấy làm thế nào chúng ta có thể làm animate foreground layer.
Chúng tôi đã khai báo CABasicAnimation
với keyPath 'StroEnd'
Điều này có nghĩa là chúng ta muốn tạo hiệu ứng nơi stroke sẽ kết thúc.
Animation sẽ đi từ đầu đến cuối (là một vòng tròn như được chỉ định trong đường dẫn của chúng ta trước đó) trong một khoảng thời gian được chỉ định. Cuối cùng, chúng ta không muốn Animation của mình bị xóa khi hoàn thành, vì vậy chúng ta đặt isRemondOnCompletion
thành false. Bước cuối cùng là thêm Animation vào foregroundLayer (CAShapeLayer)
của chúng ta
pulsing layer animation gần như giống nhau, ngoại trừ việc chúng ta sẽ nhóm 2 animation. Nếu bạn thấy kết quả cuối cùng, pulse layer của chúng ta sẽ scales ra bên ngoài và cũng trở nên ít nhìn thấy hơn về cuối.
Chúng tôi sẽ khai báo 2 CABasicAnimations
với các đường dẫn transform.scale
để điều chỉnh kích thước và opacity
để điều chỉnh mức độ hiển thị. Layer sẽ tăng 20% kích thước và giảm đến 0 opacity trong suốt thời gian của animation. Khi chúng ta đã khai báo 2 animations, chúng ta có thể sử dụng CAAnimationgroup
để thêm các animations lại với nhau. Sau đó, để làm cho nó trông đẹp hơn và không tuyến tính như foreground animation của chúng ta, chúng ta đã đặt timingFunction
thành easyInEaseOut
, điều này có nghĩa là animation của chúng ta sẽ bắt đầu chậm, tăng tốc giữa duration của nó và sau đó chậm lại trước khi kết thúc. Cuối cùng, chúng ta đã đặt duration là 1 giây và lặp lại animation liên tục. Nhưng đừng lo lắng, khi chúng ta phát hiện ra rằng foreground animation của chúng ta đã dừng (có nghĩa là thời gian còn lại là 0), chúng ta cũng sẽ loại bỏ pulsing animation.
Cuối cùng, đây là phần còn lại của các methods được tìm thấy trong tệp của chúng ta. HandleTimerTick
giảm thời gian còn lại 0,1 giây và cập nhật UILabel
của chúng ta bằng cách sử dụng main thread (vì đây là thao tác UI).
Public method duy nhất của chúng ta là startCountdown
, mất nhiều thời gian và có animate hay không.
AnimationDidStop delegate
hoạt động giống như background gradient
của chúng ta, chúng ta đã làm trước đó. Khi foreground layer của chúng ta kết thúc animating, chúng ta sẽ xóa tất cả animations và làm mất hiệu lựctimer của chúng ta.
Đó là tất cả những gì chúng ta thực sự cần để có được đầu ra giống nhau! Dưới đây là 2 tập tin nếu có ai bị mất.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | // // ViewController.swift // Core Animations // // Created by Bilguun Batbold on 29/3/19. // Copyright © 2019 Bilguun. All rights reserved. // import UIKit class ViewController: UIViewController, CAAnimationDelegate { let gradient = CAGradientLayer() // list of array holding 2 colors var gradientSet = [[CGColor]]() // current gradient index var currentGradient: Int = 0 // colors to be added to the set let colorOne = #colorLiteral(red: 0.2392156869, green: 0.6745098233, blue: 0.9686274529, alpha: 1).cgColor let colorTwo = #colorLiteral(red: 0.8078431487, green: 0.02745098062, blue: 0.3333333433, alpha: 1).cgColor let colorThree = #colorLiteral(red: 0.9607843161, green: 0.7058823705, blue: 0.200000003, alpha: 1).cgColor // create outlet in the storyboard with type CountdownProgressBar @IBOutlet weak var countdownProgressBar: CountdownProgressBar! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap))) } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() createGradientView() countdownProgressBar.startCoundown(duration: 10, showPulse: true) } /// Creates gradient view func createGradientView() { // overlap the colors and make it 3 sets of colors gradientSet.append([colorOne, colorTwo]) gradientSet.append([colorTwo, colorThree]) gradientSet.append([colorThree, colorOne]) // set the gradient size to be the entire screen gradient.frame = self.view.bounds gradient.colors = gradientSet[currentGradient] gradient.startPoint = CGPoint(x:0, y:0) gradient.endPoint = CGPoint(x:1, y:1) gradient.drawsAsynchronously = true self.view.layer.insertSublayer(gradient, at: 0) animateGradient() } @objc func handleTap() { print("Tapped") countdownProgressBar.startCoundown(duration: 10, showPulse: true) } func animateGradient() { // cycle through all the colors, feel free to add more to the set if currentGradient < gradientSet.count - 1 { currentGradient += 1 } else { currentGradient = 0 } // animate over 3 seconds let gradientChangeAnimation = CABasicAnimation(keyPath: "colors") gradientChangeAnimation.duration = 3.0 gradientChangeAnimation.toValue = gradientSet[currentGradient] gradientChangeAnimation.fillMode = CAMediaTimingFillMode.forwards gradientChangeAnimation.isRemovedOnCompletion = false gradientChangeAnimation.delegate = self gradient.add(gradientChangeAnimation, forKey: "gradientChangeAnimation") } func animationDidStop(_ anim: CAAnimation, finished flag: Bool) { // if our gradient animation ended animating, restart the animation by changing the color set if flag { gradient.colors = gradientSet[currentGradient] animateGradient() } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 | // // CountdownProgressBar.swift // Core Animations // // Created by Bilguun Batbold on 29/3/19. // Copyright © 2019 Bilguun. All rights reserved. // import Foundation import UIKit class CountdownProgressBar: UIView { private var timer = Timer() private var duration = 5.0 private var remainingTime = 0.0 private var showPulse = false // label that will show the remaining time private lazy var remainingTimeLabel: UILabel = { let remainingTimeLabel = UILabel(frame: CGRect(origin: CGPoint(x: 0, y: 0) , size: CGSize(width: bounds.width, height: bounds.height))) remainingTimeLabel.font = UIFont.systemFont(ofSize: 32, weight: .heavy) remainingTimeLabel.textAlignment = NSTextAlignment.center return remainingTimeLabel }() // foreground layer that will be animated private lazy var foregroundLayer: CAShapeLayer = { let foregroundLayer = CAShapeLayer() foregroundLayer.lineWidth = 10 foregroundLayer.strokeColor = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 1).cgColor foregroundLayer.lineCap = .round foregroundLayer.fillColor = UIColor.clear.cgColor foregroundLayer.strokeEnd = 0 foregroundLayer.frame = bounds return foregroundLayer }() // background layer to show a gray path private lazy var backgroundLayer: CAShapeLayer = { let backgroundLayer = CAShapeLayer() backgroundLayer.lineWidth = 10 backgroundLayer.strokeColor = UIColor.lightGray.cgColor backgroundLayer.lineCap = .round backgroundLayer.fillColor = UIColor.clear.cgColor backgroundLayer.frame = bounds return backgroundLayer }() // layer that will be used to get the pulsing effect animation private lazy var pulseLayer: CAShapeLayer = { let pulseLayer = CAShapeLayer() pulseLayer.lineWidth = 10 pulseLayer.strokeColor = UIColor.lightGray.cgColor pulseLayer.lineCap = .round pulseLayer.fillColor = UIColor.clear.cgColor pulseLayer.frame = bounds return pulseLayer }() // called when creating programmatically override init(frame: CGRect) { super.init(frame: frame) loadLayers() } // called when creating via storyboard required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) loadLayers() } deinit { timer.invalidate() } private lazy var foregroundGradientLayer: CAGradientLayer = { let foregroundGradientLayer = CAGradientLayer() foregroundGradientLayer.frame = bounds let startColor = #colorLiteral(red: 0.8078431487, green: 0.02745098062, blue: 0.3333333433, alpha: 1).cgColor let secondColor = #colorLiteral(red: 0.9607843161, green: 0.7058823705, blue: 0.200000003, alpha: 1).cgColor foregroundGradientLayer.colors = [startColor, secondColor] foregroundGradientLayer.startPoint = CGPoint(x: 0, y: 0) foregroundGradientLayer.endPoint = CGPoint(x: 1, y: 1) return foregroundGradientLayer }() private lazy var pulseGradientLayer: CAGradientLayer = { let pulseGradientLayer = CAGradientLayer() pulseGradientLayer.frame = bounds let startColor = #colorLiteral(red: 0.5090036988, green: 0.04135338217, blue: 0.2113225758, alpha: 1).cgColor let secondColor = #colorLiteral(red: 0.4990308285, green: 0.3679353595, blue: 0.1137484089, alpha: 1).cgColor pulseGradientLayer.colors = [startColor, secondColor] pulseGradientLayer.startPoint = CGPoint(x: 0, y: 0) pulseGradientLayer.endPoint = CGPoint(x: 1, y: 1) return pulseGradientLayer }() private func loadLayers() { // get the center point of the view let centerPoint = CGPoint(x: frame.width/2 , y: frame.height/2) // create a circular path that is just slightly smaller than the view // set the start angle to be 12 o'clock and end angle to be 360 degrees clockwise let circularPath = UIBezierPath(arcCenter: centerPoint, radius: bounds.width / 2 - 20, startAngle: -CGFloat.pi/2, endAngle: 2 * CGFloat.pi - CGFloat.pi/2, clockwise: true) // give the CAShapeLayers the circular path to follow // pulse and foreground layers will be the masks over the gradient layers // add the background CAShapeLayer and the 2 CAGradientLayer as a sublayer pulseLayer.path = circularPath.cgPath pulseGradientLayer.mask = pulseLayer layer.addSublayer(pulseGradientLayer) backgroundLayer.path = circularPath.cgPath layer.addSublayer(backgroundLayer) foregroundLayer.path = circularPath.cgPath foregroundGradientLayer.mask = foregroundLayer layer.addSublayer(foregroundGradientLayer) addSubview(remainingTimeLabel) print(remainingTimeLabel.frame) } private func beginAnimation() { animateForegroundLayer() // only show the pulse if required if showPulse { animatePulseLayer() } } private func animateForegroundLayer() { let foregroundAnimation = CABasicAnimation(keyPath: "strokeEnd") foregroundAnimation.fromValue = 0 foregroundAnimation.toValue = 1 foregroundAnimation.duration = CFTimeInterval(duration) foregroundAnimation.fillMode = CAMediaTimingFillMode.forwards foregroundAnimation.isRemovedOnCompletion = false foregroundAnimation.delegate = self foregroundLayer.add(foregroundAnimation, forKey: "foregroundAnimation") } private func animatePulseLayer() { let pulseAnimation = CABasicAnimation(keyPath: "transform.scale") pulseAnimation.fromValue = 1.0 pulseAnimation.toValue = 1.2 let pulseOpacityAnimation = CABasicAnimation(keyPath: "opacity") pulseOpacityAnimation.fromValue = 0.7 pulseOpacityAnimation.toValue = 0.0 let groupedAnimation = CAAnimationGroup() groupedAnimation.animations = [pulseAnimation, pulseOpacityAnimation] groupedAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut) groupedAnimation.duration = 1.0 groupedAnimation.repeatCount = Float.infinity pulseLayer.add(groupedAnimation, forKey: "pulseAnimation") } @objc private func handleTimerTick() { remainingTime -= 0.1 if remainingTime > 0 { } else { remainingTime = 0 timer.invalidate() } DispatchQueue.main.async { self.remainingTimeLabel.text = "(String.init(format: "%.1f", self.remainingTime))" } } //MARK: - Public Functions /** Stars the countdown with defined duration. - Parameter duration: Countdown time duration. - Parameter showPulse: By default false, set to true to show pulse around the countdown progress bar. - Returns: null. */ func startCoundown(duration: Double, showPulse: Bool = false) { self.duration = duration self.showPulse = showPulse remainingTime = duration remainingTimeLabel.text = "(remainingTime)" timer.invalidate() timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(handleTimerTick), userInfo: nil, repeats: true) beginAnimation() } } extension CountdownProgressBar: CAAnimationDelegate { func animationDidStop(_ anim: CAAnimation, finished flag: Bool) { pulseLayer.removeAllAnimations() timer.invalidate() } } |
Sử dụng lớp CountdownProgressBar
Để sử dụng lớp của chúng ta, đầu tiên tôi đã tạo một UIView
và đặt nó ở giữa View. Sau đó tôi đã thay đổi class
của nó thành lớp CountdownProgressBar
của chúng ta. Cuối cùng, trong ViewControll.swift
của tôi, tôi bắt đầu animation như sau:
countdownProgressBar.startCoundown(duration: 10, showPulse: true)
Hãy thử xem pulsation animation hoặc thử vô hiệu hóa nó.
Thế là xong! Chúng ta đã thực hiện một vài animations thực sự thú vị mà không khó chút nào! Ban đầu tôi đã viết custom countdown class
của riêng mình cho một dự án tôi đang làm, và không thể tìm thấy nhiều ví dụ trực tuyến. Hãy sửa đổi và sử dụng mã như bạn muốn.
Có rất nhiều điều thú vị mà bạn có thể làm, với hiệu quả cao hơn UIView
, bằng cách sử dụng CALayers
!
Đầu tiên, bạn có thể thử nghiệm với các giá trị và màu sắc khác nhau để thực sự hiểu cách hoạt động của mã. Ví dụ: bạn có thể chỉnh sửa màu background , điều chỉnh radius của hiệu ứng pulsing của chúng ta hoặc chơi với màu của foreground layer.
Đây chỉ là một số ví dụ đơn giản làm thế nào bạn có thể thêm gia vị cho ứng dụng của bạn. Sẽ rất tuyệt khi kết hợp một animated background cho Login Page của bạn hoặc tạo custom loading bar của riêng bạn.