首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Swift动画 —— 进度条

Swift动画 —— 进度条

作者头像
CC老师
发布于 2022-01-14 14:57:49
发布于 2022-01-14 14:57:49
3.5K0
举报

最后需要完成的效果:

首先要做的就是用CAShapeLayer画一个圆。这里先创建一个路径,把圆的中心放在视图的中心,半径设为100,然后设置起始角度和结束角度,并将clockwise设为true。

然后将shapeLayer的路径设为刚才创建的路径,最后将shapeLayer添加到view的layer的sublayer中。

代码语言:javascript
复制
 let shapeLayer = CAShapeLayer()

    let circularPath = UIBezierPath(arcCenter: .zero, radius: 100, startAngle: 0, endAngle: CGFloat.pi * 2, clockwise: true)
        shapeLayer.path = circularPath.cgPath
        shapeLayer.position = view.center

        view.layer.addSublayer(shapeLayer)

(滑动显示更多)

接下来在shapeLayer外面添加一圈红色的圆圈。

代码语言:javascript
复制
 shapeLayer.strokeColor = UIColor.red.cgColor
 shapeLayer.lineWidth = 10

(滑动显示更多)

效果如下:

接下来为view添加一个点击手势,以便后面来进行外面圆圈的动画。

代码语言:javascript
复制
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap)))

(滑动显示更多)

添加响应方法

代码语言:javascript
复制
@objc func handleTap() {
}

在添加外面圆圈的动画时候需要用到shapeLayer,所以把shapeLayer放到外面声明让VC持有。

在viewDidLoad中将shapeLayer的.strokeEnd 设为0

代码语言:javascript
复制
 shapeLayer.strokeEnd = 0

之后在handleTap中为shapeLayer添加动画。

代码语言:javascript
复制
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
        basicAnimation.toValue = 1
        basicAnimation.duration = 2
        shapeLayer.add(basicAnimation, forKey: "stokeAnimation")

(滑动显示更多)

这样点击之后就会画一个圆圈了。

这里有个问题就是开始的位置应该是圆的上方而不是右边,这里就需要去修改shapeLayer的transform,这样就会从圆的上方开始动画了。

代码语言:javascript
复制
        shapeLayer.transform = CATransform3DMakeRotation(-CGFloat.pi / 2, 0, 0, 1)

(滑动显示更多)

这里还有个问题就是圆圈的头是直线而不是圆形的,这里只需要改shapeLayer的lineCap就可以了。

代码语言:javascript
复制
 shapeLayer.lineCap = .round

接下来移除掉圆圈中间的黑色部分,这里修改shapeLayer的填充色为无色。

代码语言:javascript
复制
 shapeLayer.fillColor = UIColor.clear.cgColor

(滑动显示更多)

接下来为进度条添加一个底部轨道(track layer)。

代码语言:javascript
复制
    let trackLayer = CAShapeLayer()
        trackLayer.path = circularPath.cgPath
        trackLayer.strokeColor = UIColor.lightGray.cgColor
        trackLayer.lineWidth = 10
        trackLayer.position = view.center
        trackLayer.fillColor = UIColor.clear.cgColor
        trackLayer.lineCap = .round

(滑动显示更多)

现在的效果:

完整代码:

代码语言:javascript
复制
import UIKit

class ViewController: UIViewController {

    let shapeLayer = CAShapeLayer()

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
          let circularPath = UIBezierPath(arcCenter: .zero, radius: 100, startAngle: 0, endAngle: CGFloat.pi * 2, clockwise: true)

        let trackLayer = CAShapeLayer()
        trackLayer.path = circularPath.cgPath
        trackLayer.strokeColor = UIColor.lightGray.cgColor
        trackLayer.lineWidth = 10
        trackLayer.position = view.center
        trackLayer.fillColor = UIColor.clear.cgColor
        trackLayer.lineCap = .round

        view.layer.addSublayer(trackLayer)
        shapeLayer.path = circularPath.cgPath
        shapeLayer.strokeEnd = 0
        shapeLayer.position = view.center
        shapeLayer.strokeColor = UIColor.red.cgColor
        shapeLayer.lineWidth = 10
        shapeLayer.fillColor = UIColor.clear.cgColor
        shapeLayer.lineCap = .round
        shapeLayer.transform = CATransform3DMakeRotation(-CGFloat.pi / 2, 0, 0, 1)


        view.layer.addSublayer(shapeLayer)
        view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap)))
    }

    @objc func handleTap() {
        let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
        basicAnimation.toValue = 1
        basicAnimation.duration = 2
        basicAnimation.fillMode = .forwards
        basicAnimation.isRemovedOnCompletion = false
        shapeLayer.add(basicAnimation, forKey: "stokeAnimation")

    }
}

(滑动显示更多)

接下来模拟下载文件的过程。

创建一个方法来下载文件,这里使用URLSession以方便观察下载的情况。

这里需要遵守URLSessionDownloadDelegate,并实现代理方法,否则会报错。

代码语言:javascript
复制
func beginDownloadingFile() {
        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()
    }

(滑动显示更多)

接下来在URLSession的代理方法中,根据已经写的data和总共需要写的data计算出百分比,然后根据得出的百分比改变shapeLayer的strokeEnd。

这里改变strokeEnd的时候需要放在主线程,否则视图不会发生改变。

代码语言:javascript
复制
 func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        let percentage = CGFloat(totalBytesWritten) / CGFloat(totalBytesExpectedToWrite)

        DispatchQueue.main.async {
            self.shapeLayer.strokeEnd = percentage
        }

        print(percentage)
    }

(滑动显示更多)

接下来需要创建一个label放在shapelayer的中间进行显示。

创建一个UILabel让VC持有,文字为Start,文字居中对齐,字体大小为加粗32号,字体颜色为黑色。

代码语言:javascript
复制
   let percentageLabel: UILabel = {
        let label = UILabel()
        label.text = "Start"
        label.textAlignment = .center
        label.font = UIFont.boldSystemFont(ofSize: 32)
        label.textColor = .black
        return label
    }()

(滑动显示更多)

在视图中添加percentageLabel为视图的子view,设置好label的frame,并将其放在视图中心。

代码语言:javascript
复制
 view.addSubview(percentageLabel)
 percentageLabel.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
 percentageLabel.center = view.center

(滑动显示更多)

接下来需要在下载任务开始时,将percentageLabel的文字改为对应的百分比。

这个就在刚才URLSession的代理方法里面设置strokeEnd的地方设置percentageLabel的text就好了。

这里注意设置text也需要在主线程里面。

代码语言:javascript
复制
        DispatchQueue.main.async {
            self.percentageLabel.text = "\(Int(percentage * 100))%"
            self.shapeLayer.strokeEnd = percentage
        }

(滑动显示更多)

到这里进度条就会跟着下载的进度前进,并且在中间显示白分比了。

完整代码:

代码语言:javascript
复制
import UIKit

class ViewController: UIViewController,URLSessionDownloadDelegate {



    let shapeLayer = CAShapeLayer()
    let percentageLabel: UILabel = {
        let label = UILabel()
        label.text = "Start"
        label.textAlignment = .center
        label.font = UIFont.boldSystemFont(ofSize: 32)
        label.textColor = .black
        return label
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.

        view.addSubview(percentageLabel)
        percentageLabel.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
        percentageLabel.center = view.center
        let circularPath = UIBezierPath(arcCenter: .zero, radius: 100, startAngle: 0, endAngle: CGFloat.pi * 2, clockwise: true)

        let trackLayer = CAShapeLayer()
        trackLayer.path = circularPath.cgPath
        trackLayer.strokeColor = UIColor.lightGray.cgColor
        trackLayer.lineWidth = 10
        trackLayer.position = view.center
        trackLayer.fillColor = UIColor.clear.cgColor
        trackLayer.lineCap = .round

        view.layer.addSublayer(trackLayer)
        shapeLayer.path = circularPath.cgPath
        shapeLayer.strokeEnd = 0
        shapeLayer.position = view.center
        shapeLayer.strokeColor = UIColor.red.cgColor
        shapeLayer.lineWidth = 10
        shapeLayer.fillColor = UIColor.clear.cgColor
        shapeLayer.lineCap = .round
        shapeLayer.transform = CATransform3DMakeRotation(-CGFloat.pi / 2, 0, 0, 1)


        view.layer.addSublayer(shapeLayer)
        view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap)))
    }

    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"

    func beginDownloadingFile() {
        shapeLayer.strokeEnd = 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, didFinishDownloadingTo location: URL) {

    }

    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)
    }

    @objc func handleTap() {

        beginDownloadingFile()
//        let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
//        basicAnimation.toValue = 1
//        basicAnimation.duration = 2
//        basicAnimation.fillMode = .forwards
//        basicAnimation.isRemovedOnCompletion = false
//        shapeLayer.add(basicAnimation, forKey: "stokeAnimation")

    }

}

(滑动显示更多)

接下来要为进度条添加扩散的特效

添加一个shapeLayer让VC持有,名字叫做pulsatingLayer。

代码语言:javascript
复制
let pulsatingLayer = CAShapeLayer()

在viewDidLoad中设置好属性并添加为view的layer的sublayer

代码语言:javascript
复制
  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,这样放大后会自动缩放到原来的大小,最后将重复次数设为无限大。

代码语言:javascript
复制
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")
    }

(滑动显示更多)

接下来微调一下

代码语言:javascript
复制
  trackLayer.fillColor = UIColor.black.cgColor
  view.backgroundColor = .black
   label.textColor = .white

(滑动显示更多)

就可以看到这样的动画

这样已经十分接近想要的效果了,但是还有一个很严重的bug就是当切换到主屏幕在回来的时候,动画就失效了。

这就需要去监听回到前台的状态,如果收到通知就做相对应的处理。

添加通知的观察者,并添加响应方法。

代码语言:javascript
复制
  NotificationCenter.default.addObserver(self, selector: #selector(handleEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)

(滑动显示更多)

在响应方法中调用 animatePulsatingLayer()。

代码语言:javascript
复制
  @objc private func handleEnterForeground() {
        animatePulsatingLayer()
    }

(滑动显示更多)

这样一个会动的进度条就完成了。但是现在整个文件有点太多代码,难以阅读了。

所以需要稍微重构一下,比如提取出一个方法来创建并返回 shapeLayer

代码语言:javascript
复制
    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的时候就只要三行代码了。

代码语言:javascript
复制
   pulsatingLayer = createCircleShapeLayer(strokeColor: .clear, fillColor: UIColor.pulsatingFillColor)
  let trackLayer = createCircleShapeLayer(strokeColor: .trackStrokeColor, fillColor: .backgroundColor)
  shapeLayer = createCircleShapeLayer(strokeColor: .outlineStrokeColor, fillColor: .clear)

(滑动显示更多)

重构过的完整代码

代码语言:javascript
复制
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)
}


(滑动显示更多)

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-10-28,如有侵权请联系 cloudcommunity@tencent.com 删除
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档