简体   繁体   English

是否有一种方法可以在 Swift 中对多个动画进行计时功能?

[英]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.这是为那些不知道的人提供的图像。

Apple Progress Ring.苹果进度环。

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.我现在需要处理三个动画。

  1. The first 75% of the ring环的前 75%
  2. The last 25% of the ring戒指的最后 25%
  3. Rotating the ring if it surpasses 100%如果超过 100%,则旋转环

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所以我的时间设置如下

  1. If the ring is filled at 75% or less, it takes 2.25 seconds to fill with a timing function of ease out如果环填充在 75% 或更少,则需要 2.25 秒填充具有缓出的计时功能
  2. If the ring is filled between 75 and 100%, the first 75% takes 1 second to fill and the last 25% takes 1.25 seconds to fill with a timing function of ease out.如果环填充在 75% 和 100% 之间,则前 75% 需要 1 秒填充,最后 25% 需要 1.25 秒填充,并带有缓出的计时功能。
  3. If the ring is filled over 100%, the first 75% takes 1 second, the last 25% takes 1 second and the rotation for the ring also takes 1 second with a timing function of ease out.如果环被填充超过 100%,前 75% 需要 1 秒,最后 25% 需要 1 秒,环的旋转也需要 1 秒,带有缓出计时功能。

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同步动画多个图层。

  1. Create a container CALayer subclass for layers that should be animated synchronously.为应该同步动画的图层创建一个容器CALayer子类。 Let's call it TestLayer我们称之为TestLayer
  2. Define the property for every layer you need to animate.为您需要设置动画的每个图层定义属性。
  3. Override needsDisplay(forKey:) in the TestLayer to receive the display call when animation modifies the particular layer via keyPath .当动画通过keyPath修改特定层时,覆盖TestLayer中的TestLayer needsDisplay(forKey:)以接收display调用。
  4. Override 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM