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

时光总嘲笑我的痴心妄想 提交于 2019-12-10 18:52:25

问题


I'd like to use a button as a toggle – click once & an image rotates indefinitely. Click again, the image stops, click again, it restarts.

I found this answer helpful in getting the animation to continue: Rotate a view for 360 degrees indefinitely in Swift?

However, I'm unclear on how to stop things. I've implemented the code below & it seems to work, but am curious if this is the proper way to stop an animation, or if there is another, preferred method. Also - my rotation continues until finishing, but I'm wondering if I can freeze the rotation at location when the button is pressed (I've tried .removeAllAnimations() in the second attempt below, but that doesn't seem to work at all.

    @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)
        }
    }

This does work. I was also wondering if it'd be possible to stop the animation mid-spin. The way it's set up, the animation goes the full 180 degrees before stopping. I've also tried adding a removeAnimation in the spinPressed action, and getting rid of the stopRotation check inside rotateView, but that doesn't seem to work – rotation continues & just gets faster if the spinPressed is pressed again (see below):

    @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)
        }
    }

A confirm if first approach is sound is welcome. And if there is a way to stop the rotation mid-spin, that'd also be welcome (as well as setting me straight on my flawed thinking on removeAllAnimations).

Thanks! JG


回答1:


There are a couple of ways to do what you're asking about:

  1. If supporting iOS 10+, you can use UIViewPropertyAnimator, whose animations you can pause and restart (resuming from where it was paused):

    private var animator: UIViewPropertyAnimator?
    
    @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, animations: {
            UIView.animateKeyframes(withDuration: 4, delay: 0, animations: {
                UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 1.0 / 3.0) {
                    self.animatedView.transform = .init(rotationAngle: .pi * 2 * 1 / 3)
                }
                UIView.addKeyframe(withRelativeStartTime: 1.0 / 3.0, relativeDuration: 1.0 / 3.0) {
                    self.animatedView.transform = .init(rotationAngle: .pi * 2 * 2 / 3)
                }
                UIView.addKeyframe(withRelativeStartTime: 2.0 / 3.0, relativeDuration: 1.0 / 3.0) {
                    self.animatedView.transform = .identity
                }
            })
        }, completion: { [weak self] _ in
            self?.createAnimation()
        })
    }
    
  2. You can alternatively use UIKit Dynamics to rotate the item. You can then remove a UIDynamicItemBehavior that was performing the rotation and it just stops where it was. It automatically leaves the view transform where it was. Then, to resume the rotation, just add a UIDynamicItemBehavior for the rotation again:

    private lazy var animator: UIDynamicAnimator = 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!)
        }
    }
    

    This doesn't let you easily control the speed of the rotation in terms of time, but rather it’s dictated by angularVelocity, but it's a nice simple approach (and supports iOS 7.0 and later).

  3. The old-school approach for stopping an animation and leaving it where you stopped it is to capture the presentationLayer of the animation (which shows where it was mid-flight). Then you can grab the current state, stop the animation, and set the transform to what the presentationLayer reported.

    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. If you want to use UIView block based animation, you have to capture the angle at which you stopped the animation, so you know from where to restart the animation. The trick is grab m12 and m11 of the CATransform3D:

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

    Thus, this yields:

    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, animations: {
                UIView.animateKeyframes(withDuration: 4, delay: 0, options: .repeat, animations: {
                    UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 1.0 / 3.0) {
                        self.animatedView.transform = .init(rotationAngle: self.angle + .pi * 2 * 1 / 3)
                    }
                    UIView.addKeyframe(withRelativeStartTime: 1.0 / 3.0, relativeDuration: 1.0 / 3.0) {
                        self.animatedView.transform = .init(rotationAngle: self.angle + .pi * 2 * 2 / 3)
                    }
                    UIView.addKeyframe(withRelativeStartTime: 2.0 / 3.0, relativeDuration: 1.0 / 3.0) {
                        self.animatedView.transform = .init(rotationAngle: self.angle)
                    }
                })
            })
        }
    
        isAnimating = !isAnimating
    }
    
  5. You can rotate the object yourself using CADisplayLink that updates the angle to some calculated value. Then stopping the rotation is as simple as invalidating the display link, thereby leaving it where it was when it stopped. You can then resume animation by simply adding the display link back to your runloop.

    This sort of technique gives you a great deal of control, but is the least elegant of the approaches.




回答2:


This first approach seems reasonable. If you want fine grain control of the stopping position, then divide both the animation duration and the rotation radians by a constant amount. Your completion hander will be called more frequently to check if the rotation should be continued.

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)
        }
    }



回答3:


I have an infinite "flip" of to labels (it's a watermark) that I need to "turn off" to one specific label (or watermark) when the share options are being displayed. I have all of this happening through a timer - to keep a specific label viewed for a few seconds before flipping. When you do this, it's a simple matter of turning the timer 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
        }
    }
}


来源:https://stackoverflow.com/questions/46208916/proper-way-to-stop-an-infinitely-rotating-image-and-how-does-one-implement-remo

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!