【问题标题】:Animate UIBezierPath arc between 2 angles在 2 个角度之间为 UIBezierPath 弧设置动画
【发布时间】:2020-09-15 16:30:30
【问题描述】:

我已经构建了一个自定义的圆形范围选择器,类似于 iOS 的闹钟/就寝时间应用中的选择器。为了实现这一点,我使用了两个 UIButton thumbs 并附加了一个 UIPanGestureRecognizer 和一个 CAShapeLayer + UIBezierPath 指示它们之间的时间范围。当我使用触摸输入沿圆圈移动和调整圆弧本身的大小时,它的效果非常好。

请原谅我对圆圈的粗略描述,但这里是选择器工作原理的要点:

问题是我需要使用UISegmentedControl 在某些时间范围预设之间切换,我真的希望为这个更改设置动画。我已经使用CAKeyframeAnimation(keyPath: "position") 成功地为两个拇指(绿色) 沿着圆圈在所有 360 度上漂亮地设置了动画,但是我无法在我的时间范围内做到这一点(红色) CAShapeLayer。我管理的最好的方法是使用CABasicAnimation(keyPath: "path") 为范围层设置动画,但在动画过程中它发生了扭曲并且没有跟随圆弧,而是以直线矢量穿过圆。

为简洁起见,我将避免发布 getStuff() 函数的内容,因为它们与问题无关。

// This works
let startAngle: CGFloat = getAngle(for: start)
let startPath: UIBezierPath = getThumbTravelPath(fromAngle: startThumbAngle, toAngle: startAngle)
let startAnimation = CAKeyframeAnimation(keyPath: "position")
startAnimation.path = startPath.cgPath
startAnimation.isRemovedOnCompletion = false
startAnimation.calculationMode = .cubicPaced
startAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
startAnimation.duration = 0.2
startThumbView.layer.add(startAnimation, forKey: nil)
    
let endAngle: CGFloat = getAngle(for: end)
let endPath: UIBezierPath = getThumbTravelPath(fromAngle: endThumbAngle, toAngle: endAngle)
let endAnimation = CAKeyframeAnimation(keyPath: "position")
endAnimation.path = endPath.cgPath
endAnimation.isRemovedOnCompletion = false
endAnimation.calculationMode = .cubicPaced
endAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
endAnimation.duration = 0.2
endThumbView.layer.add(endAnimation, forKey: nil)

// This doesn't work
let path: UIBezierPath = getRangePathFor(startAngle: startAngle, endAngle: endAngle)
let pathAnimation = CABasicAnimation(keyPath: "path")
pathAnimation.fromValue = rangePath.cgPath 
pathAnimation.toValue = path.cgPath
pathAnimation.duration = 0.2
pathAnimation.isRemovedOnCompletion = false
pathAnimation.isAdditive = true
rangeLayer.path = path.cgPath
rangeLayer.add(pathAnimation, forKey: nil)

我看到有人建议使用 CABasicAnimation(keyPath: "startStroke") + CABasicAnimation(keyPath: "endStroke") 方法,但据我所知,这行不通,因为我不能为 startStroke 设置负值,因此我不能有一个从 270° 延伸到 45° 的弧。

【问题讨论】:

  • 为什么不从 270º 到 405º 设置动画?
  • 问题是startStroke & endStroke 的范围只有0.0到1.0,这意味着我不能从0.75到1.125

标签: ios swift core-animation uibezierpath caanimation


【解决方案1】:

看起来 strokeStartstrokeEnd 毕竟是答案。


解决方案

我设法通过旋转rangeLayer 本身以始终对应起始拇指 的角度来解决这个问题,因此我的strokeStart 始终为零。作为下一步,我只是从endAngle 中减去startAngle (以度为单位,而不是弧度) 并用它来计算endStroke 的值。

    private func setRangeLayer(startRadian: CGFloat, endRadian: CGFloat, animated: Bool, duration: CFTimeInterval? = nil) {
    let endAngle = endRadian / .pi * 180
    let endStroke = endAngle / 360
    let rotation = CGAffineTransform(rotationAngle: startRadian + .pi / 2)
    
    CATransaction.begin()
    if !animated {
        CATransaction.disableActions()
        CATransaction.setAnimationDuration(0)
    } else {
        CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: .easeInEaseOut))
        if let duration = duration {
            CATransaction.setAnimationDuration(duration)
        }
     }
    rangeLayer.strokeStart = 0
    rangeLayer.strokeEnd = endStroke
    rangeLayer.setAffineTransform(rotation)
    CATransaction.commit()
    }

示例

我将度数转换为弧度然后再转换回度数的原因是为了在整个课程中针对不同用例对函数进行标准化。但是,您可以轻松地将弧度用于rotation,将度数用于endStroke

    let startAngle: CGFloat = getAngle(for: startTimestamp)
    let endAngle: CGFloat = getAngle(for: endTimestamp)
    let startRadian: CGFloat = (startAngle * .pi / 180) - .pi / 2
    let endRadian: CGFloat = (endAngle - startAngle) * .pi / 180
    setRangeLayer(startRadian: startRadian, endRadian: endRadian, animated: true, duration: animationDuration)

重要提示

为了正确地将rotate CAShapeLayer 围绕其中心,您必须事先设置其frame / bounds 否则它将像飞机转子一样沿其原点旋转。


希望这会对处于类似情况的人有所帮助。

【讨论】:

    猜你喜欢
    • 2012-12-02
    • 1970-01-01
    • 2012-06-07
    • 1970-01-01
    • 1970-01-01
    • 2018-04-04
    • 2023-03-22
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多