停止无限旋转图像的正确方法?以及如何实现 removeAllAnimations?

Proper way to stop an infinitely rotating image? and how does one implement removeAllAnimations?

我想使用一个按钮作为切换按钮 – 单击一次,图像会无限期地旋转。再次点击,图像停止,再次点击,图像重新开始。

我发现这个答案有助于让动画继续:

但是,我不清楚如何停止。我已经实现了下面的代码并且它似乎可以工作,但我很好奇这是否是停止动画的正确方法,或者是否有另一种首选方法。另外 - 我的旋转一直持续到完成,但我想知道我是否可以在按下按钮时将旋转冻结在某个位置(我在下面的第二次尝试中尝试了 .removeAllAnimations() ,但这似乎不起作用全部.

    @IBOutlet weak var imageView: UIImageView!
    var stopRotation = true

    func rotateView(targetView: UIView, duration: Double = 1.0) {
        if !stopRotation {
            UIView.animate(withDuration: duration, delay: 0.0, options: .curveLinear, animations: {
                targetView.transform = targetView.transform.rotated(by: CGFloat(Double.pi))
            }) { finished in
                self.rotateView(targetView: targetView, duration: duration)
            }
        }
    }
    
    @IBAction func spinPressed(_ sender: UIButton) {
        stopRotation = !stopRotation
        if !stopRotation {
            rotateView(targetView: imageView)
        }
    }

这确实有效。我还想知道是否有可能在旋转过程中停止动画。按照它的设置方式,动画在停止前旋转了 180 度。我还尝试在 spinPressed 操作中添加一个 removeAnimation,并在 rotateView 中删除 stopRotation 检查,但这似乎不起作用 - 旋转继续并且如果再次按下 spinPressed 只会变得更快(见下文):

    @IBOutlet weak var imageView: UIImageView!
    var stopRotation = true
    
    func rotateView(targetView: UIView, duration: Double = 1.0) {
        UIView.animate(withDuration: duration, delay: 0.0, options: .curveLinear, animations: {
            targetView.transform = targetView.transform.rotated(by: CGFloat(Double.pi))
        }) { finished in
            self.rotateView(targetView: targetView, duration: duration)
        }
    }
    
    @IBAction func spinPressed(_ sender: UIButton) {
        stopRotation = !stopRotation
        if stopRotation {
            imageView.layer.removeAllAnimations()
        } else {
            rotateView(targetView: imageView)
        }
    }

欢迎确认第一种方法是否合理。如果有办法在旋转中途停止旋转,那也很受欢迎(同时让我直截了当地思考我对 removeAllAnimations 的错误想法)。

谢谢! JG

第一种方法似乎是合理的。如果您想要对停止位置进行细粒度控制,则将动画持续时间和旋转弧度除以一个常数。将更频繁地调用您的完成处理程序以检查是否应继续轮换。

let animationFrequency = 30.0
 func rotateView(targetView: UIView, duration: Double = 1.0 / animationFrequency) {
        UIView.animate(withDuration: duration, delay: 0.0, options: .curveLinear, animations: {
            targetView.transform = targetView.transform.rotated(by: CGFloat(Double.pi / animationFrequency))
        }) { finished in
            self.rotateView(targetView: targetView, duration: duration)
        }
    }

我有无限 "flip" 个标签(这是一个水印),我需要在显示共享选项时 "turn off" 到一个特定标签(或水印)。我通过计时器让所有这一切发生——在翻转之前保持特定标签被查看几秒钟。当您这样做时,只需关闭定时器即可。

public class FlipView:UIView {

    private var labelWatermark:FlipLabel!
    private var labelTap:FlipLabel!

    let transitionOptions: UIViewAnimationOptions = [.transitionFlipFromRight, .showHideTransitionViews]
    var isLabel1 = true
    var timer:Timer!

    convenience public init(appName:String) {

        self.init(frame: CGRect.zero)

        labelTap = FlipLabel("Tap to remove", "Watermark")
        self.addSubview(labelTap)

        labelWatermark = FlipLabel("created with", appName)
        self.addSubview(labelWatermark)

        timer = Timer.scheduledTimer(
            timeInterval: 3.0, target: self, selector: #selector(flipViews),
            userInfo: nil, repeats: true)
        timer.fire()

    }

    internal func startFlip() {
        timer = Timer.scheduledTimer(
            timeInterval: 3.0, target: self, selector: #selector(flipViews),
            userInfo: nil, repeats: true)
        timer.fire()
    }

    internal func stopFlip() {
        timer.invalidate()
        UIView.transition(from: labelTap, to: labelWatermark, duration: 1, options: transitionOptions, completion: nil)
        isLabel1 = true
    }

    @objc func flipViews() {
        if (isLabel1) {
            UIView.transition(from: labelWatermark, to: labelTap, duration: 1, options: transitionOptions, completion: nil)
            isLabel1 = false
        } else {
            UIView.transition(from: labelTap, to: labelWatermark, duration: 1, options: transitionOptions, completion: nil)
            isLabel1 = true
        }
    }
}

有几种方法可以满足您的要求:

  1. 如果支持iOS 10+,你可以使用UIViewPropertyAnimator,它的动画你可以暂停和重新开始(从暂停的地方恢复):

     private var animator: UIViewPropertyAnimator?
    
     deinit {
         animator?.stopAnimation(true)
     }
    
     @IBAction func didTapButton(_ sender: Any) {
         guard let animator = animator else {
             createAnimation()
             return
         }
    
         if animator.isRunning {
             animator.pauseAnimation()
         } else {
             animator.startAnimation()
         }
     }
    
     /// Create and start 360 degree animation
     ///
     /// This will fire off another animation when one 360° rotation finishes.
    
     private func createAnimation() {
         animator = UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 4, delay: 0, options: .curveLinear) { [self] in
             UIView.animateKeyframes(withDuration: 4, delay: 0) {
                 UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 1.0 / 3.0) {
                     animatedView.transform = .init(rotationAngle: .pi * 2 * 1 / 3)
                 }
                 UIView.addKeyframe(withRelativeStartTime: 1.0 / 3.0, relativeDuration: 1.0 / 3.0) {
                     animatedView.transform = .init(rotationAngle: .pi * 2 * 2 / 3)
                 }
                 UIView.addKeyframe(withRelativeStartTime: 2.0 / 3.0, relativeDuration: 1.0 / 3.0) {
                     animatedView.transform = .identity
                 }
             }
         } completion: { [weak self] _ in
             self?.createAnimation()
         }
     }
    
  2. 您也可以使用 UIKit Dynamics 来旋转项目。然后,您可以删除正在执行旋转的 UIDynamicItemBehavior,它就停在原处。它会自动将视图 transform 留在原处。然后,要恢复旋转,只需再次为旋转添加一个UIDynamicItemBehavior

     private lazy var animator = UIDynamicAnimator(referenceView: view)
     private var rotate: UIDynamicItemBehavior!
    
     @IBAction func didTapButton(_ sender: Any) {
         if let rotate = rotate {
             animator.removeBehavior(rotate)
             self.rotate = nil
         } else {
             rotate = UIDynamicItemBehavior(items: [animatedView])
             rotate.allowsRotation = true
             rotate.angularResistance = 0
             rotate.addAngularVelocity(1, for: animatedView)
             animator.addBehavior(rotate)
         }
     }
    

    这不会让您轻松地控制时间方面的旋转速度,而是由 angularVelocity 决定的,但这是一个很好的简单方法(并支持 iOS 7.0 和稍后)。

  3. 停止动画并将其留在停止位置的老式方法是捕获动画的 presentationLayer(显示它在飞行途中的位置)。然后您可以获取当前状态,停止动画,并将变换设置为 presentationLayer 报告的内容。

     private var isAnimating = false
    
     @IBAction func didTapButton(_ sender: Any) {
         if isAnimating {
             let transform = animatedView.layer.presentation()!.transform
             animatedView.layer.removeAllAnimations()
             animatedView.layer.transform = transform
         } else {
             let rotate = CABasicAnimation(keyPath: "transform.rotation")
             rotate.byValue = 2 * CGFloat.pi
             rotate.duration = 4
             rotate.repeatCount = .greatestFiniteMagnitude
             animatedView.layer.add(rotate, forKey: nil)
         }
    
         isAnimating = !isAnimating
     }
    
  4. 如果你想使用 UIView 基于块的动画,你必须捕捉你停止动画的角度,这样你就知道从哪里重新开始动画。诀窍是抓住 CATransform3D:

    m12m11
     angle = atan2(transform.m12, transform.m11)
    

    因此,这会产生:

     private var angle: CGFloat = 0
     private var isAnimating = false
    
     @IBAction func didTapButton(_ sender: Any) {
         if isAnimating {
             let transform = animatedView.layer.presentation()!.transform
             angle = atan2(transform.m12, transform.m11)
             animatedView.layer.removeAllAnimations()
             animatedView.layer.transform = transform
         } else {
             UIView.animate(withDuration: 4, delay: 0, options: .curveLinear) { [self] in
                 UIView.animateKeyframes(withDuration: 4, delay: 0, options: .repeat) {
                     UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 1.0 / 3.0) {
                         animatedView.transform = .init(rotationAngle: self.angle + .pi * 2 * 1 / 3)
                     }
                     UIView.addKeyframe(withRelativeStartTime: 1.0 / 3.0, relativeDuration: 1.0 / 3.0) {
                         animatedView.transform = .init(rotationAngle: self.angle + .pi * 2 * 2 / 3)
                     }
                     UIView.addKeyframe(withRelativeStartTime: 2.0 / 3.0, relativeDuration: 1.0 / 3.0) {
                         animatedView.transform = .init(rotationAngle: self.angle)
                     }
                 }
             }
         }
    
         isAnimating = !isAnimating
     }
    
  5. 您可以使用 CADisplayLink 自行旋转对象,将角度更新为某个计算值。然后停止旋转就像使显示无效 link 一样简单,从而将其留在停止时的位置。然后,您可以通过简单地将显示 link 添加回您的运行循环来恢复动画。

    这种技术为您提供了很大的控制权,但却是最不优雅的方法。