圆形动画 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% 开始或结束笔画来摆脱“快照”。这确保始终有一部分笔画可见。
这是一个很好的开始,但不幸的是 strokeStart
和 strokeEnd
不允许 [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
。
- 将
rotationAnimation
的 to
值设置为 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。试试看,告诉我进展如何。
所以我正在尝试学习如何在 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% 开始或结束笔画来摆脱“快照”。这确保始终有一部分笔画可见。
这是一个很好的开始,但不幸的是 strokeStart
和 strokeEnd
不允许 [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
。 - 将
rotationAnimation
的to
值设置为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。试试看,告诉我进展如何。