首页 > 解决方案 > 定时器运行时为 CAShapeLayer 设置动画

问题描述

我有一个圆形按钮,外面有一个 CAShapeLayer。我也有作为视频播放运行的计时器。随着计时器的运行,我想更新 CAShapeLayer,就好像它是一个进度指示器一样。问题是当计时器运行时,shapelayer 上的动画开始跳来跳去,不流畅。我尝试在主队列上更新它,但这也不起作用。

我哪里错了?

lazy var roundButton: UIButton = {
    // create button
}()

var seconds = 15
weak var videoTimer: Timer?
let shapeLayer = CAShapeLayer()
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")

func addCAShapeLayerToButton() {

    // there is another shapelayer that is gray used as the outer circle background layer

    let circularPath = UIBezierPath(arcCenter: roundButton.center, radius: (roundButton.frame.width / 2) + 10, startAngle: -.pi/2, endAngle: 3 * .pi/2, clockwise: true)
    shapeLayer.path = timerCircularPath.cgPath
    shapeLayer.strokeColor = UIColor.red.cgColor
    shapeLayer.fillColor = fillColor
    shapeLayer.lineWidth = lineWidth
    shapeLayer.strokeEnd = 0
    view.layer.addSublayer(shapeLayer)

    basicAnimation.fillMode = CAMediaTimingFillMode.forwards
    basicAnimation.isRemovedOnCompletion = false   
    shapeLayer.add(basicAnimation, forKey: nil)
}

func startTimer() {
        
    videoTimer?.invalidate()
    videoTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { [weak self] _ in
        self?.timerIsRunning()
    })
    if let videoTimer = videoTimer {
        RunLoop.current.add(videoTimer, forMode: RunLoop.Mode.common)
    }
}

@objc fileprivate func timerIsRunning() {

    if seconds != 0 {
        seconds -= 1

        let recordingProgress: CGFloat = CGFloat(seconds) / CGFloat(15)
        shapeLayer.strokeEnd = recordingProgress // tried updating on the mainqueue

        /*
          I also tried
          basicAnimation.fromValue = 0
          basicAnimation.toValue = 1
          basicAnimation.duration = CFTimeInterval(recordingProgress)
        */

    } else {

        videoTimer?.invalidate()
    }
}

在此处输入图像描述

当计时器运行时,红色 shapeLayer 将填充外部灰色圆圈。它应该指示用户还剩下多少时间来记录,因为没有标签显示计时器运行时。

标签: iosswiftnstimercashapelayer

解决方案


此示例进度轮涉及 的子类UIView和 的子类NSObjet。前者创建一个轮子视图,而后者在动画中执行 Energizer bunny,直到用户停止它

中风视图.swift

import UIKit

class StrokeView: UIView {
    var shapeLayer = CAShapeLayer()
    var strokeColor: UIColor // strokeColor
    var start: CGFloat
    var end: CGFloat
    var radius: CGFloat // radius
    var weight: CGFloat // weight
    var view: UIView // view
    
    init(frame: CGRect, strokeColor: UIColor, start: CGFloat, end: CGFloat, radius: CGFloat, weight: CGFloat, view: UIView){
        self.strokeColor = strokeColor
        self.start = start
        self.end = end
        self.radius = radius
        self.weight = weight
        self.view = view
        super.init(frame: frame)
        self.isOpaque = false
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func draw(_ rect: CGRect) {
        super.draw(rect)
        
        let bounds = self.bounds
        let centerPoint = CGPoint(x: bounds.width/2, y: bounds.height/2)
        let circlePath = UIBezierPath(arcCenter: centerPoint, radius: radius, startAngle: start, endAngle: end, clockwise: true)
        shapeLayer.path = circlePath.cgPath
        shapeLayer.fillColor = UIColor.clear.cgColor
        shapeLayer.strokeColor = strokeColor.cgColor
        shapeLayer.lineWidth = weight
        self.layer.addSublayer(shapeLayer)
    }
}

RotateMe.swift

import UIKit

class RotateMe: NSObject {
    var myView = UIView()
    
    var strokeColor0: UIColor
    var start0: CGFloat
    var end0: CGFloat
    
    var strokeColor1: UIColor
    var start1: CGFloat
    var end1: CGFloat
    
    var strokeColor2: UIColor
    var start2: CGFloat
    var end2: CGFloat
    
    var rect: CGRect
    var radius: CGFloat
    var weight: CGFloat
    var slowness: Double
    var view: UIView
    
    init(rect: CGRect, strokeColor0: UIColor, start0: CGFloat, end0: CGFloat, strokeColor1: UIColor, start1: CGFloat, end1: CGFloat, strokeColor2: UIColor, start2: CGFloat, end2: CGFloat, radius: CGFloat, weight: CGFloat, slowness: Double, view: UIView) {
        self.rect = rect
        self.strokeColor0 = strokeColor0
        self.start0 = start0
        self.end0 = end0
        self.strokeColor1 = strokeColor1
        self.start1 = start1
        self.end1 = end1
        self.strokeColor2 = strokeColor2
        self.start2 = start2
        self.end2 = end2
        self.radius = radius
        self.weight = weight
        self.slowness = slowness
        self.view = view
    }
    
    func circleMe() {
        let minSize = min(rect.width, rect.height)
        let myRect = CGRect(origin: CGPoint(x: (view.frame.width - minSize)/2.0, y: (view.frame.height - minSize)/2.0), size: CGSize(width: rect.width, height: rect.height))
        myView = UIView(frame: myRect)
        let strokeView0 = StrokeView(frame: rect, strokeColor: strokeColor0, start: start0, end: end0, radius: radius, weight: weight, view: view)
        let strokeView1 = StrokeView(frame: rect, strokeColor: strokeColor1, start: start1, end: end1, radius: radius, weight: weight, view: view)
        let strokeView2 = StrokeView(frame: rect, strokeColor: strokeColor2, start: start2, end: end2, radius: radius, weight: weight, view: view)
        myView.addSubview(strokeView0)
        myView.addSubview(strokeView1)
        myView.addSubview(strokeView2)
        //myView.backgroundColor = UIColor.blue.withAlphaComponent(0.4)
        
        let start = 0.0
        let end = start + 2 * Double.pi
        let dur = slowness as Double
        let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
        rotateAnimation.duration = dur
        rotateAnimation.repeatCount = .infinity
        rotateAnimation.fromValue = start
        rotateAnimation.toValue = end
        //rotateAnimation.removedOnCompletion = true;
        rotateAnimation.fillMode = CAMediaTimingFillMode.forwards;
        rotateAnimation.autoreverses = false
        rotateAnimation.isCumulative = true // if set it to no, the view won't rotate and will only move in an awkward way
        myView.layer.add(rotateAnimation, forKey: "rotationAnimation")
        view.addSubview(myView)
    }
    
    func stopRotation() {
        myView.removeFromSuperview()
    }
}

如何通过视图控制器运行进度轮

在此处输入图像描述

在这种情况下,用户会看到开始按钮。当他们点击它时,会出现一个进度轮。

在此处输入图像描述

import UIKit

class ViewController: UIViewController {
    // MARK: - Variables
    
    // MARK: - IBOutlet
    @IBOutlet var rotateView: RotateMe?
    @IBOutlet weak var startButton: UIButton!
    @IBOutlet weak var endButton: UIButton!
    
    // MARK: - IBAction
    @IBAction func startTapped(_ sender: UIButton) {
        startButton.isEnabled = false
        endButton.isEnabled = true
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
            self.makeProgress()
        }
    }
    
    @IBAction func endTapped(_ sender: UIButton) {
        startButton.isEnabled = true
        endButton.isEnabled = false
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
            self.rotateView?.stopRotation()
        }
    }
    
    // MARK: - Life cycle
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    func makeProgress() {
        let selfFrame = view.frame
        let minFloat = min(selfFrame.size.width, selfFrame.size.height)
        let rect = CGRect(x: 0.0, y: 0.0, width: minFloat, height: minFloat)
        let color0 = UIColor.orange
        let start0 = CGFloat(-120.0).degreesToRadians()
        let end0 = CGFloat(0.0).degreesToRadians()
        let color1 = UIColor.purple
        let start1 = CGFloat(0.0).degreesToRadians()
        let end1 = CGFloat(120.0).degreesToRadians()
        
        let color2 = UIColor.green
        let start2 = CGFloat(120.0).degreesToRadians()
        let end2 = CGFloat(-120.0).degreesToRadians()
        
        rotateView = RotateMe(rect: rect, strokeColor0: color0, start0: start0, end0: end0, strokeColor1: color1, start1: start1, end1: end1, strokeColor2: color2, start2: start2, end2: end2, radius: 80.0, weight: 30.0, slowness: 2.0, view: view)
        rotateView?.circleMe()
    }
}

extension CGFloat {
    func degreesToRadians() -> CGFloat {
        return self * .pi / 180.0
    }
    
    func radiansToDegrees() -> CGFloat {
        return self * (180.0 / .pi)
    }
}

在您的情况下,运行Timer并计时 15 秒。然后只需调用

rotateView?.stopRotation()

推荐阅读