- 小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
接下来要为进度条添加扩散的特效 添加一个shapeLayer让VC持有,名字叫做pulsatingLayer。
let pulsatingLayer = CAShapeLayer()
在viewDidLoad中设置好属性并添加为view的layer的sublayer
pulsatingLayer.path = circularPath.cgPath
pulsatingLayer.strokeColor = UIColor.clear.cgColor
pulsatingLayer.lineWidth = 10
pulsatingLayer.position = view.center
pulsatingLayer.fillColor = UIColor.yellow.cgColor
pulsatingLayer.lineCap = .round
view.layer.addSublayer(pulsatingLayer)
添加一个方法animatePulsatingLayer,并且在添加pulsatingLayer后调用。在方法里为pulsatingLayer添加一个放大1.5倍,时间为1秒的动画,并将其autoreverses设为true,这样放大后会自动缩放到原来的大小,最后将重复次数设为无限大。
func animatePulsatingLayer(){
let animation = CABasicAnimation(keyPath: "transform.scale")
animation.toValue = 1.5
animation.duration = 1
animation.autoreverses = true
animation.repeatCount = Float.infinity
pulsatingLayer.add(animation, forKey: "pulsing")
}
接下来微调一下
trackLayer.fillColor = UIColor.black.cgColor
view.backgroundColor = .black
label.textColor = .white
就可以看到这样的动画
这样已经十分接近想要的效果了,但是还有一个很严重的bug就是当切换到主屏幕在回来的时候,动画就失效了。这就需要去监听回到前台的状态,如果收到通知就做相对应的处理。
添加通知的观察者,并添加响应方法。
NotificationCenter.default.addObserver(self, selector: #selector(handleEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
在响应方法中调用 animatePulsatingLayer()。
@objc private func handleEnterForeground() {
animatePulsatingLayer()
}
这样一个会动的进度条就完成了。但是现在整个文件有点太多代码,难以阅读了,所以需要稍微重构一下,比如提取出一个方法来创建并返回 shapeLayer
private func createCircleShapeLayer(strokeColor: UIColor, fillColor: UIColor) -> CAShapeLayer {
let layer = CAShapeLayer()
let circularPath = UIBezierPath(arcCenter: .zero, radius: 100, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
layer.path = circularPath.cgPath
layer.strokeColor = strokeColor.cgColor
layer.lineWidth = 20
layer.fillColor = fillColor.cgColor
layer.lineCap = kCALineCapRound
layer.position = view.center
return layer
}
这样创建shapelayer的时候就只要三行代码了。
pulsatingLayer = createCircleShapeLayer(strokeColor: .clear, fillColor: UIColor.pulsatingFillColor)
let trackLayer = createCircleShapeLayer(strokeColor: .trackStrokeColor, fillColor: .backgroundColor)
shapeLayer = createCircleShapeLayer(strokeColor: .outlineStrokeColor, fillColor: .clear)
重构过的完整代码
import UIKit
class ViewController: UIViewController, URLSessionDownloadDelegate {
var shapeLayer: CAShapeLayer!
var pulsatingLayer: CAShapeLayer!
let percentageLabel: UILabel = {
let label = UILabel()
label.text = "Start"
label.textAlignment = .center
label.font = UIFont.boldSystemFont(ofSize: 32)
label.textColor = .white
return label
}()
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
private func setupNotificationObservers() {
NotificationCenter.default.addObserver(self, selector: #selector(handleEnterForeground), name: .UIApplicationWillEnterForeground, object: nil)
}
@objc private func handleEnterForeground() {
animatePulsatingLayer()
}
private func createCircleShapeLayer(strokeColor: UIColor, fillColor: UIColor) -> CAShapeLayer {
let layer = CAShapeLayer()
let circularPath = UIBezierPath(arcCenter: .zero, radius: 100, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
layer.path = circularPath.cgPath
layer.strokeColor = strokeColor.cgColor
layer.lineWidth = 20
layer.fillColor = fillColor.cgColor
layer.lineCap = kCALineCapRound
layer.position = view.center
return layer
}
override func viewDidLoad() {
super.viewDidLoad()
setupNotificationObservers()
view.backgroundColor = UIColor.backgroundColor
setupCircleLayers()
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap)))
setupPercentageLabel()
}
private func setupPercentageLabel() {
view.addSubview(percentageLabel)
percentageLabel.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
percentageLabel.center = view.center
}
private func setupCircleLayers() {
pulsatingLayer = createCircleShapeLayer(strokeColor: .clear, fillColor: UIColor.pulsatingFillColor)
view.layer.addSublayer(pulsatingLayer)
animatePulsatingLayer()
let trackLayer = createCircleShapeLayer(strokeColor: .trackStrokeColor, fillColor: .backgroundColor)
view.layer.addSublayer(trackLayer)
shapeLayer = createCircleShapeLayer(strokeColor: .outlineStrokeColor, fillColor: .clear)
shapeLayer.transform = CATransform3DMakeRotation(-CGFloat.pi / 2, 0, 0, 1)
shapeLayer.strokeEnd = 0
view.layer.addSublayer(shapeLayer)
}
private func animatePulsatingLayer() {
let animation = CABasicAnimation(keyPath: "transform.scale")
animation.toValue = 1.5
animation.duration = 0.8
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
animation.autoreverses = true
animation.repeatCount = Float.infinity
pulsatingLayer.add(animation, forKey: "pulsing")
}
let urlString = "https://firebasestorage.googleapis.com/v0/b/firestorechat-e64ac.appspot.com/o/intermediate_training_rec.mp4?alt=media&token=e20261d0-7219-49d2-b32d-367e1606500c"
private func beginDownloadingFile() {
print("Attempting to download file")
shapeLayer.strokeEnd = 0
percentageLabel.text = "0%"
let configuration = URLSessionConfiguration.default
let operationQueue = OperationQueue()
let urlSession = URLSession(configuration: configuration, delegate: self, delegateQueue: operationQueue)
guard let url = URL(string: urlString) else { return }
let downloadTask = urlSession.downloadTask(with: url)
downloadTask.resume()
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
let percentage = CGFloat(totalBytesWritten) / CGFloat(totalBytesExpectedToWrite)
DispatchQueue.main.async {
self.percentageLabel.text = "\(Int(percentage * 100))%"
self.shapeLayer.strokeEnd = percentage
}
print(percentage)
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
print("Finished downloading file")
}
fileprivate func animateCircle() {
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.toValue = 1
basicAnimation.duration = 2
basicAnimation.fillMode = kCAFillModeForwards
basicAnimation.isRemovedOnCompletion = false
shapeLayer.add(basicAnimation, forKey: "urSoBasic")
}
@objc private func handleTap() {
print("Attempting to animate stroke")
beginDownloadingFile()
// animateCircle()
}
}
extension UIColor {
static func rgb(r: CGFloat, g: CGFloat, b: CGFloat) -> UIColor {
return UIColor(red: r/255, green: g/255, blue: b/255, alpha: 1)
}
static let backgroundColor = UIColor.rgb(r: 21, g: 22, b: 33)
static let outlineStrokeColor = UIColor.rgb(r: 234, g: 46, b: 111)
static let trackStrokeColor = UIColor.rgb(r: 56, g: 25, b: 49)
static let pulsatingFillColor = UIColor.rgb(r: 86, g: 30, b: 63)
}