圆形动画 iOS 尾部未完全完成的 UIKit 行为

Circle animation iOS UIKit behaviour with tail not completing fully

所以我正在尝试学习如何在 UIKit 中绘制圆圈,并且我已经基本弄明白了,但我只是想再实现一件事。在下面的视频中,当圆的尾部到达尽头时,我希望尾部没有完全到达头部,这意味着我希望圆的大小不会完全缩小。

我在下面的视频中有它,但仍然有尾巴消失并且动画再次从头部开始的快照。所以希望尾巴的消失不要消失

视频演示: https://github.com/DJSimonSays93/CircleAnimation/blob/main/README.md

代码如下:

class SpinningView: UIView {

let circleLayer = CAShapeLayer()

let rotationAnimation: CAAnimation = {
    let animation = CABasicAnimation(keyPath: "transform.rotation.z")
    animation.fromValue = 0
    animation.toValue = Double.pi * 2
    animation.duration = 3 // increase this duration to slow down the circle animation effect
    animation.repeatCount = MAXFLOAT
    return animation
}()

override func awakeFromNib() {
    super.awakeFromNib()
    setup()
}

func setup() {
    circleLayer.lineWidth = 10.0
    circleLayer.fillColor = nil
    //circleLayer.strokeColor = UIColor(red: 0.8078, green: 0.2549, blue: 0.2392, alpha: 1.0).cgColor
    circleLayer.strokeColor = UIColor.systemBlue.cgColor
    circleLayer.lineCap = .round
    layer.addSublayer(circleLayer)
    updateAnimation()
}

override func layoutSubviews() {
    super.layoutSubviews()
    let center = CGPoint(x: bounds.midX, y: bounds.midY)
    let radius = min(bounds.width, bounds.height) / 2 - circleLayer.lineWidth / 2
    
    let startAngle: CGFloat = -90.0
    let endAngle: CGFloat = startAngle + 360.0
    
    circleLayer.position = center
    circleLayer.path = createCircle(startAngle: startAngle, endAngle: endAngle, radius: radius).cgPath
}

private func updateAnimation() {
    //The strokeStartAnimation beginTime + duration value need to add up to the strokeAnimationGroup.duration value
    let strokeStartAnimation: CABasicAnimation = CABasicAnimation(keyPath: "strokeStart")
    strokeStartAnimation.beginTime = 0.5
    strokeStartAnimation.fromValue = 0
    strokeStartAnimation.toValue = 0.93 //change this to 0.93 for cool effect
    strokeStartAnimation.duration = 3.0
    strokeStartAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
    
    let strokeEndAnimation: CABasicAnimation = CABasicAnimation(keyPath: "strokeEnd")
    strokeEndAnimation.fromValue = 0
    strokeEndAnimation.toValue = 1.0
    strokeEndAnimation.duration = 2.0
    strokeEndAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
    
    let colorAnimation = CABasicAnimation(keyPath: "strokeColor")
    colorAnimation.fromValue = UIColor.systemBlue.cgColor
    colorAnimation.toValue = UIColor.systemRed.cgColor
    
    let strokeAnimationGroup: CAAnimationGroup = CAAnimationGroup()
    strokeAnimationGroup.duration = 3.5
    strokeAnimationGroup.repeatCount = Float.infinity
    strokeAnimationGroup.fillMode = .forwards
    strokeAnimationGroup.isRemovedOnCompletion = false
    strokeAnimationGroup.animations = [strokeStartAnimation, strokeEndAnimation, colorAnimation]
    
    circleLayer.add(strokeAnimationGroup, forKey: nil)
    circleLayer.add(rotationAnimation, forKey: "rotation")
}

private func createCircle(startAngle: CGFloat, endAngle: CGFloat, radius: CGFloat) -> UIBezierPath {
    return UIBezierPath(arcCenter: CGPoint.zero,
                        radius: radius,
                        startAngle: startAngle.toRadians(),
                        endAngle: endAngle.toRadians(),
                        clockwise: true)
}

是这样的吗?

这里没有什么特别的。它与您的初始代码几乎完全相同,但对旋转角度进行了小幅调整。

方法

您的初始动画看起来很棒!就像你说的,动画从 strokeEnd 的 0% 重新开始的“快照”就是它发出的声音。

正如@MadProgrammer 所指出的,从理论上讲,您可以通过从不以 0% 开始或结束笔画来摆脱“快照”。这确保始终有一部分笔画可见。

这是一个很好的开始,但不幸的是 strokeStartstrokeEnd 不允许 [0.0, 1.0] 范围之外的值。所以你不能准确地创建一个动画(没有很多关键帧),以便在每个动画循环中笔划位置重叠(因为你需要使用超出范围的值来覆盖整个圆)。

所以,我所做的就是使用上面的方法,最终得到如下所示的动画。动画开始和结束的笔画弧长相等 - 非常重要.

然后,使用您现有的旋转动画,我 非常轻微地 在描边动画期间旋转整个绘图,以便开始和结束弧看起来彼此重叠。旋转角度计算如下。

0.07 是通过 strokeStartAnimation.toValue 的初始值减去 1.0 得到的。

  • 弧的 标量 长度为 0.07 (S)。
  • 圆的半径会bounds.width / 2 (r).
  • 要获得弧长 (L),我们需要将标量长度乘以周长 (P)。
  • 弧长(L)与旋转角度(theta)的关系为,
2 * Theta * r = L

But L is also equal to S * P, so some substituting around and we get,

theta = 2S (in Radians)

解决方案

那么,就这样吧。解决方案是对您的代码进行以下更改。

  • 标量弧长定义为class变量,startOffset.
  • 使用startOffset设置strokeStart动画的toValue
  • 使用startOffset设置strokeEnd动画的fromValue
  • rotationAnimationto 值设置为 2 * theta
  • 将旋转动画持续时间与笔触动画持续时间相匹配。

最终的旋转动画是这样的:

var rotationAnimation: CAAnimation{
    get{
        let radius = Double(bounds.width) / 2.0
        let perimeter = 2 * Double.pi * radius
        let theta = perimeter * startOffset / (2 * radius)
        let animation = CABasicAnimation(keyPath: "transform.rotation.z")
        animation.fromValue = 0
        animation.toValue = theta * 2 + Double.pi * 2
        animation.duration = 3.5
        animation.repeatCount = MAXFLOAT
        return animation
    }
}

还有笔画:

let strokeStartAnimation: CABasicAnimation = CABasicAnimation(keyPath: "strokeStart")
strokeStartAnimation.beginTime = 0.5
strokeStartAnimation.fromValue = 0
strokeStartAnimation.toValue = 1.0 - startOffset
strokeStartAnimation.duration = 3.0
strokeStartAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)

let strokeEndAnimation: CABasicAnimation = CABasicAnimation(keyPath: "strokeEnd")
strokeEndAnimation.fromValue = startOffset
strokeEndAnimation.toValue = 1.0
strokeEndAnimation.duration = 2.0
strokeEndAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)

我对您现有的代码进行了 pull request。试试看,告诉我进展如何。