[英]Is the a way to have a timing function work on multiple animations in Swift?
I'm trying to recreate the Activity ring that Apple uses in their activity apps.我正在尝试重新创建 Apple 在其活动应用程序中使用的活动环。 Here is an image for those unaware.
这是为那些不知道的人提供的图像。
I've done a decent job of recreating it, something I especially struggled with was the overlapping shadow.我在重新创建它方面做得很好,我特别挣扎的是重叠的阴影。 In the end, the workaround I used was to split the ring into two parts, the first 75% and the last 25% so I could have the last 25% have a shadow and appear to overlap with itself.
最后,我使用的解决方法是将环分成两部分,前 75% 和最后 25%,这样我就可以让最后 25% 有阴影并且看起来与自身重叠。
Now that I have done this, the animation timing has become more difficult.现在我已经这样做了,动画计时变得更加困难。 I now have three animations that I need to take care of.
我现在需要处理三个动画。
Here is a video demonstrating this.这是一个演示这一点的视频。 Streamable Link.
流媒体链接。
For illustrative purposes, here is the last 25% coloured differently so you can visualise it.出于说明目的,这里是最后 25% 的不同颜色,以便您可以对其进行可视化。
As you can see, the timing of the animation is a bit janky.如您所见,动画的时间安排有点混乱。 So I have my timings set as follows
所以我的时间设置如下
My question is, is it possible to link these seperate CABasicAnimations so I can set a total time of 2.25 seconds as well as set a timing function for that group so timing is calculated dynamically for each animation and a timing function affects all three?我的问题是,是否可以将这些单独的 CABasicAnimations 链接起来,以便我可以设置 2.25 秒的总时间并为该组设置计时功能,以便为每个动画动态计算计时,并且计时功能会影响所有三个?
Here is my code so far, it consists of 3 animation functions.到目前为止,这是我的代码,它由 3 个动画函数组成。
percent = How much to fill the ring百分比 = 填充环的数量
gradientMaskPart1 = first 75% ring layer gradientMaskPart1 = 第一个 75% 环层
gradientMaskPart2 = last 25% ring layer gradientMaskPart2 = 最后 25% 的环层
containerLayer = layer that holds both gradientMaskParts and is rotated to simute the ring overlapping itself. containerLayer = 包含两个gradientMaskParts 并旋转以模拟环重叠的层。
private func animateRing() { let needsMultipleAnimations = percent <= 0.75 ? false : true CATransaction.begin() if needsMultipleAnimations { CATransaction.setCompletionBlock(ringEndAnimation) } let basicAnimation = CABasicAnimation(keyPath: "strokeEnd") basicAnimation.fromValue = currentFill basicAnimation.toValue = percent > 0.75 ? 0.75 : percent currentFill = Double(percent) basicAnimation.fillMode = .forwards basicAnimation.isRemovedOnCompletion = false basicAnimation.duration = needsMultipleAnimations ? 1 : 2.25 basicAnimation.timingFunction = needsMultipleAnimations ? .none : CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut) gradientMaskPart1.add(basicAnimation, forKey: "basicAnimation") CATransaction.commit() }
private func ringEndAnimation() {
let needsMultipleAnimations = percent <= 1 ? false : true
CATransaction.begin()
if needsMultipleAnimations { CATransaction.setCompletionBlock(rotateRingAnimation) }
let duration = needsMultipleAnimations ? 1 : 1.25
let timingFunction: CAMediaTimingFunction? =
needsMultipleAnimations ? .none : CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.fromValue = 0
basicAnimation.toValue = percent <= 1 ? (percent-0.75)*4 : 1
basicAnimation.duration = duration
basicAnimation.fillMode = .forwards
basicAnimation.isRemovedOnCompletion = false
basicAnimation.timingFunction = timingFunction
self.gradientMaskPart2.isHidden = false
self.gradientMaskPart2.add(basicAnimation2, forKey: "basicAnimation")
CATransaction.commit()
}
private func rotateRingAnimation() {
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotationAnimation.fromValue = 2*CGFloat.pi
rotationAnimation.toValue = 2*CGFloat.pi+((2*(percent-1)*CGFloat.pi))
rotationAnimation.duration = 1
rotationAnimation.fillMode = CAMediaTimingFillMode.forwards
rotationAnimation.isRemovedOnCompletion = false
rotationAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
self.containerLayer.add(rotationAnimation, forKey: "rotation")
}
This solution demonstrates how to animate several layers synchronously with their dedicated CABasicAnimation
by using CAAnimationGroup
.此解决方案演示了如何使用
CAAnimationGroup
为其专用CABasicAnimation
同步动画多个图层。
CALayer
subclass for layers that should be animated synchronously.CALayer
子类。 Let's call it TestLayer
TestLayer
needsDisplay(forKey:)
in the TestLayer
to receive the display
call when animation modifies the particular layer via keyPath
.keyPath
修改特定层时,覆盖TestLayer
中的TestLayer
needsDisplay(forKey:)
以接收display
调用。init(layer:)
in the TestLayer
to assign your sublayers to the testLayer.presentation()
layer that renders the animation of the TestLayer
.TestLayer
中的init(layer:)
以将您的子层分配给呈现TestLayer
动画的testLayer.presentation()
层。 Here is the whole code with TestView
that hosts TestLayer
.这是托管
TestLayer
TestView
的完整代码。
class TestLayer: CALayer {
@NSManaged @objc dynamic var layer1: CAGradientLayer
@NSManaged @objc dynamic var layer2: CAGradientLayer
override init() {
super.init()
let gradientLayer1 = CAGradientLayer()
let gradientLayer2 = CAGradientLayer()
gradientLayer1.colors = [UIColor.red.cgColor, UIColor.green.cgColor]
gradientLayer2.colors = [UIColor.green.cgColor, UIColor.red.cgColor]
gradientLayer1.startPoint = CGPoint(x: 0.5, y: 0.5)
gradientLayer1.endPoint = CGPoint(x: 1.0, y: 0.5)
gradientLayer2.startPoint = CGPoint(x: 0, y: 0.5)
gradientLayer2.endPoint = CGPoint(x: 0.5, y: 0.5)
addSublayer(gradientLayer1)
addSublayer(gradientLayer2)
layer1 = gradientLayer1
layer2 = gradientLayer2
}
override init(layer: Any) {
super.init(layer: layer)
if let testLayer = layer as? TestLayer {
layer1 = testLayer.layer1
layer2 = testLayer.layer2
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSublayers() {
var bounds1 = self.bounds
bounds1.size.height /= 2.0
var bounds2 = bounds1
bounds2.origin.y = bounds1.maxY
super.layoutSublayers()
layer1.frame = bounds1
layer2.frame = bounds2
}
override class func needsDisplay(forKey key: String) -> Bool {
if key == "layer1" || key == "layer2" {
return true
}
return super.needsDisplay(forKey: key)
}
}
-- ——
class TestView: UIView {
override class var layerClass: AnyClass {
get {
return TestLayer.self
}
}
var testLayer: TestLayer {
get {
return layer as! TestLayer
}
}
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func startAnimation() {
let animLayer1 = CABasicAnimation(keyPath: "layer1.startPoint")
animLayer1.fromValue = CGPoint(x: 0.5, y: 0.5)
animLayer1.toValue = CGPoint(x: 0.0, y: 0.5)
animLayer1.duration = 1.0
animLayer1.autoreverses = true
let animLayer2 = CABasicAnimation(keyPath: "layer2.endPoint")
animLayer2.fromValue = CGPoint(x: 0.5, y: 0.5)
animLayer2.toValue = CGPoint(x: 1.0, y: 0.5)
animLayer2.duration = 1.0
animLayer2.autoreverses = true
let group = CAAnimationGroup()
group.duration = 1.0
group.autoreverses = true
group.repeatCount = Float.greatestFiniteMagnitude
group.animations = [animLayer1, animLayer2]
testLayer.add(group, forKey: "TestAnim")
}
}
If you want to test it, create a dummy view controller, and override its viewWillAppear
like this:如果你想测试它,创建一个虚拟的视图控制器,并像这样覆盖它的
viewWillAppear
:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let testView = TestView()
testView.frame = view.bounds
testView.translatesAutoresizingMaskIntoConstraints = true
testView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(testView)
testView.startAnimation()
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.