停止无限旋转图像的正确方法?以及如何实现 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
}
}
}
有几种方法可以满足您的要求:
如果支持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()
}
}
您也可以使用 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 和稍后)。
停止动画并将其留在停止位置的老式方法是捕获动画的 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
}
如果你想使用 UIView
基于块的动画,你必须捕捉你停止动画的角度,这样你就知道从哪里重新开始动画。诀窍是抓住 CATransform3D
:
的 m12
和 m11
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
}
您可以使用 CADisplayLink
自行旋转对象,将角度更新为某个计算值。然后停止旋转就像使显示无效 link 一样简单,从而将其留在停止时的位置。然后,您可以通过简单地将显示 link 添加回您的运行循环来恢复动画。
这种技术为您提供了很大的控制权,但却是最不优雅的方法。
我想使用一个按钮作为切换按钮 – 单击一次,图像会无限期地旋转。再次点击,图像停止,再次点击,图像重新开始。
我发现这个答案有助于让动画继续:
但是,我不清楚如何停止。我已经实现了下面的代码并且它似乎可以工作,但我很好奇这是否是停止动画的正确方法,或者是否有另一种首选方法。另外 - 我的旋转一直持续到完成,但我想知道我是否可以在按下按钮时将旋转冻结在某个位置(我在下面的第二次尝试中尝试了 .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
}
}
}
有几种方法可以满足您的要求:
如果支持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() } }
您也可以使用 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 和稍后)。停止动画并将其留在停止位置的老式方法是捕获动画的
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 }
如果你想使用
的UIView
基于块的动画,你必须捕捉你停止动画的角度,这样你就知道从哪里重新开始动画。诀窍是抓住CATransform3D
:m12
和m11
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 }
您可以使用
CADisplayLink
自行旋转对象,将角度更新为某个计算值。然后停止旋转就像使显示无效 link 一样简单,从而将其留在停止时的位置。然后,您可以通过简单地将显示 link 添加回您的运行循环来恢复动画。这种技术为您提供了很大的控制权,但却是最不优雅的方法。