【问题标题】:Animate CAShapeLayer as Timer runs定时器运行时为 CAShapeLayer 设置动画
【发布时间】:2020-06-28 05:09:36
【问题描述】:

我有一个圆形按钮,外面有一个 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 将填充外部灰色圆圈。它应该指示用户还剩下多少时间来记录,因为没有显示计时器运行时的标签。

【问题讨论】:

标签: ios swift nstimer cashapelayer


【解决方案1】:

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

StrokeView.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()

【讨论】:

  • 给我一些来试试这个。需要处理很多代码。
  • 我要对此表示赞成,因为它似乎适用于旋转视图。在我的情况下,问题是客户特别想要一个记录按钮,用户点击并按住它,当他们按住它时,外部灰色圆圈填充红色。当我浏览 cmets 中的所有内容时,我突然意识到,当他们的拇指在按钮上时,他们只能在动画填满时部分看到动画。如果他们有一个胖拇指,他们将无法看到任何东西。我的问题实际上是他们想要完成的方式的一个坏主意。但我必须给他们他们想要的东西
  • 好的。没问题...这只是基础,您可以通过多种方式自定义它,例如添加中止按钮等等。
  • 好的,非常感谢您的帮助。你在更多方面帮助了我,因为如果我们没有进行这些对话,我永远不会意识到整个用户体验一开始就是一个糟糕的想法。这周我要让他们知道。希望他们放弃它。感谢您的帮助,享受您的夏天 :) 干杯!!!
  • 好的。如果您有更多问题,请告诉我。例如,如果你想让轮子跑得更快,可以在 RotateMe 类中更改 dur。
猜你喜欢
  • 2023-04-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多