【问题标题】:Animate CAShapeLayer with circle UIBezierPath and CABasicAnimation使用圆形 UIBezierPath 和 CABasicAnimation 为 CAShapeLayer 设置动画
【发布时间】:2017-07-25 15:39:14
【问题描述】:

我想在 15 秒内从角度 0 到 360 度为圆设置动画。

动画很奇怪。我知道这可能是一个开始/结束角度的问题,我已经遇到过圆形动画的这种问题,但我不知道如何解决这个问题。

var circle_layer=CAShapeLayer()
var circle_anim=CABasicAnimation(keyPath: "path")

func init_circle_layer(){
    let w=circle_view.bounds.width
    let center=CGPoint(x: w/2, y: w/2)

    //initial path
    let start_angle:CGFloat = -0.25*360*CGFloat.pi/180
    let initial_path=UIBezierPath(arcCenter: center, radius: w/2, startAngle: start_angle, endAngle: start_angle, clockwise: true)
    initial_path.addLine(to: center)

    //final path
    let end_angle:CGFloat=start_angle+360*CGFloat(CGFloat.pi/180)
    let final_path=UIBezierPath(arcCenter: center, radius: w/2, startAngle: start_angle, endAngle: end_angle, clockwise: true)
    final_path.addLine(to: center)

    //init layer
    circle_layer.path=initial_path.cgPath
    circle_layer.fillColor=UIColor(hex_code: "EA535D").cgColor
    circle_view.layer.addSublayer(circle_layer)

    //init anim
    circle_anim.duration=15
    circle_anim.fromValue=initial_path.cgPath
    circle_anim.toValue=final_path.cgPath
    circle_anim.isRemovedOnCompletion=false
    circle_anim.fillMode=kCAFillModeForwards
    circle_anim.delegate=self
}

func start_circle_animation(){
    circle_layer.add(circle_anim, forKey: "circle_anim")
}

我想从 0 度的顶部开始,并在完整的巡演后在顶部结束:

【问题讨论】:

标签: ios swift uibezierpath cashapelayer cabasicanimation


【解决方案1】:

您不能轻松地为 UIBezierPath 的填充设置动画(或者至少在控制良好的情况下不引入奇怪的伪影)。但是您可以为strokeEndpathCAShapeLayer 设置动画。如果你使描边路径的线宽非常宽(即最后一个圆的半径),并将路径的半径设置为圆的一半,你会得到你正在寻找的东西。

private var circleLayer = CAShapeLayer()

private func configureCircleLayer() {
    let radius = min(circleView.bounds.width, circleView.bounds.height) / 2

    circleLayer.strokeColor = UIColor(hexCode: "EA535D").cgColor
    circleLayer.fillColor = UIColor.clear.cgColor
    circleLayer.lineWidth = radius
    circleView.layer.addSublayer(circleLayer)

    let center = CGPoint(x: circleView.bounds.width/2, y: circleView.bounds.height/2)
    let startAngle: CGFloat = -0.25 * 2 * .pi
    let endAngle: CGFloat = startAngle + 2 * .pi
    circleLayer.path = UIBezierPath(arcCenter: center, radius: radius / 2, startAngle: startAngle, endAngle: endAngle, clockwise: true).cgPath

    circleLayer.strokeEnd = 0
}

private func startCircleAnimation() {
    circleLayer.strokeEnd = 1
    let animation = CABasicAnimation(keyPath: "strokeEnd")
    animation.fromValue = 0
    animation.toValue = 1
    animation.duration = 15
    circleLayer.add(animation, forKey: nil)
}

为了实现终极控制,在制作复杂的UIBezierPath 动画时,可以使用CADisplayLink,避免使用CABasicAnimationCABasicAnimation 时有时会导致的伪影:

private var circleLayer = CAShapeLayer()
private weak var displayLink: CADisplayLink?
private var startTime: CFTimeInterval!

private func configureCircleLayer() {
    circleLayer.fillColor = UIColor(hexCode: "EA535D").cgColor
    circleView.layer.addSublayer(circleLayer)
    updatePath(percent: 0)
}

private func startCircleAnimation() {
    startTime = CACurrentMediaTime()
    displayLink = {
        let _displayLink = CADisplayLink(target: self, selector: #selector(handleDisplayLink(_:)))
        _displayLink.add(to: .current, forMode: .commonModes)
        return _displayLink
    }()
}

@objc func handleDisplayLink(_ displayLink: CADisplayLink) {   // the @objc qualifier needed for Swift 4 @objc inference
    let percent = CGFloat(CACurrentMediaTime() - startTime) / 15.0
    updatePath(percent: min(percent, 1.0))
    if percent > 1.0 {
        displayLink.invalidate()
    }
}

private func updatePath(percent: CGFloat) {
    let w = circleView.bounds.width
    let center = CGPoint(x: w/2, y: w/2)
    let startAngle: CGFloat = -0.25 * 2 * .pi
    let endAngle: CGFloat = startAngle + percent * 2 * .pi
    let path = UIBezierPath()
    path.move(to: center)
    path.addArc(withCenter: center, radius: w/2, startAngle: startAngle, endAngle: endAngle, clockwise: true)
    path.close()

    circleLayer.path = path.cgPath
}

那么你可以这样做:

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    configureCircleLayer()
    startCircleAnimation()
}

override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)

    displayLink?.invalidate()   // to avoid displaylink keeping a reference to dismissed view during animation
}

产生:

【讨论】:

  • 我之前使用的是Timer,我想删除它。由于我同时还显示视频,因此我希望动画流畅运行并避免屏幕冻结(视频冻结、圆形动画冻结或任何 UI 冻结)。我觉得Timer 太“重”了,因为它必须在主线程上每 0.01 秒(在我的情况下)更新一次。 CADisplayLink 真的与 Timer 不同吗?这是一个更好的解决方案吗?
  • 是的,CADisplayLinkTimer 好(因为它在每个屏幕刷新周期的开始都有最佳时间)。但它仍然可能比CABasicAnimation 更加计算密集。但是,如果您不介意笨拙的方法,因为您不能轻松地为CAShapeLayer 的填充设置动画,您可以使用CABasicAnimation 为路径(无填充)的strokeEnd 设置动画。只需将描边路径的路径设为所需半径的一半,然后将描边的lineWidth 设置为所需半径即可。这是违反直觉的,但会产生预期的效果。请参阅修改后的答案。
  • 是的,我正在考虑这种可能性。我会试试看,然后回来找你。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-03-22
  • 1970-01-01
  • 1970-01-01
  • 2014-08-10
  • 1970-01-01
  • 2020-08-15
  • 1970-01-01
相关资源
最近更新 更多