问题
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:
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() }) }
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 viewtransform
where it was. Then, to resume the rotation, just add aUIDynamicItemBehavior
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).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 thepresentationLayer
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 }
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 grabm12
andm11
of theCATransform3D
: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 }
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