2个角度之间的动画UIBezierPath弧

Animate UIBezierPath arc between 2 angles

我构建了一个自定义的圆形范围选择器,类似于 iOS 的 Alarm/Bedtime 应用程序中的那个。为此,我使用了两个 UIButton thumbs 并附有 UIPanGestureRecognizerCAShapeLayer + 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° 的弧度。

看起来 strokeStart & strokeEnd 才是答案。


解决方案

我设法通过旋转 rangeLayer 本身来解决这个问题,其方式始终对应于 start thumb 的角度,所以我的 strokeStart 是总是零。作为下一步,我只是从 endAngle (以度为单位,而不是弧度) 中减去 startAngle 并使用它来计算 [=18= 的值].

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

例子

我将度数转换为弧度然后再转换回度数的原因是为了在整个 class 中针对不同用例对函数进行标准化。但是,您可以轻松地对 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 或者它会像飞机旋翼一样沿着原点旋转。


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