【问题标题】:Circle animation iOS UIKit behaviour with tail not completing fully带有尾巴的圆圈动画iOS UIKit行为未完全完成
【发布时间】:2021-11-08 07:43:22
【问题描述】:

所以我正在尝试学习如何在 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)
}

【问题讨论】:

  • 没有花很多时间查看代码,“想法”是开始和结束角度不完整(即不是 0 和 1),相反,你只让他们填满几乎所有的圆圈,比如从 0.1 到 0.9 什么的。这意味着你需要改变你的计算来弥补这一点,所以你只填充了 0.8% 的圆圈......从概念上讲

标签: ios swift xcode animation cashapelayer


【解决方案1】:

像这样?

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

方法

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

正如@MadProgrammer 指出的那样,理论上您可以通过从不以 0% 开始或结束笔划来摆脱“snap”。这确保了笔画的某些部分始终可见。

这是一个很好的开始,但不幸的是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)

解决方案

所以,不碍事。解决方案是对您的代码进行以下更改。

  • 标量弧长定义为类变量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。试试看,让我知道结果如何。

【讨论】:

  • 哇,这太棒了,非常感谢您的解决方案和详细描述!
  • 不客气!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-10
相关资源
最近更新 更多