我在视图中无休止地循环CABasicAnimation
重复的图像拼贴:
a = [CABasicAnimation animationWithKeyPath:@"position"];
a.timingFunction = [CAMediaTimingFunction
functionWithName:kCAMediaTimingFunctionLinear];
a.fromValue = [NSValue valueWithCGPoint:CGPointMake(0, 0)];
a.toValue = [NSValue valueWithCGPoint:CGPointMake(image.size.width, 0)];
a.repeatCount = HUGE_VALF;
a.duration = 15.0;
[a retain];
我试图按照Technical Q&A QA1673中的描述“暂停并恢复”图层动画。
当应用程序进入后台时,动画将从图层中删除。
为了补偿,我听取UIApplicationDidEnterBackgroundNotification
并致电stopAnimation
并回复UIApplicationWillEnterForegroundNotification
来电startAnimation
。
- (void)startAnimation
{
if ([[self.layer animationKeys] count] == 0)
[self.layer addAnimation:a forKey:@"position"];
CFTimeInterval pausedTime = [self.layer timeOffset];
self.layer.speed = 1.0;
self.layer.timeOffset = 0.0;
self.layer.beginTime = 0.0;
CFTimeInterval timeSincePause =
[self.layer convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime;
self.layer.beginTime = timeSincePause;
}
- (void)stopAnimation
{
CFTimeInterval pausedTime =
[self.layer convertTime:CACurrentMediaTime() fromLayer:nil];
self.layer.speed = 0.0;
self.layer.timeOffset = pausedTime;
}
问题是它在开始时再次启动,并且从上一个位置开始出现丑陋的跳跃,如应用程序进入后台时系统所采用的应用程序快照所示,回到动画循环的开始。
当我重新添加动画时,我无法弄清楚如何让它从最后位置开始。坦率地说,我只是不明白QA1673的代码是如何工作的:在resumeLayer
中它将layer.beginTime设置两次,这似乎是多余的。但是当我删除第一个设置为零时,它没有恢复暂停的动画。这是通过简单的点击手势识别器测试的,它确实切换了动画 - 这与我从后台恢复的问题并不严格相关。
在移除动画之前我应该记住什么状态?当我稍后重新添加动画时,如何从该状态恢复动画?
答案 0 :(得分:49)
嘿我在游戏中偶然发现了同样的事情,最终找到了一个与你不同的解决方案,你可能会喜欢:)我想我应该分享我找到的解决方法...
我的情况是使用UIView / UIImageView动画,但它基本上仍然是CAAnimations的核心...我的方法的要点是我在视图上复制/存储当前动画,然后让Apple的暂停/恢复仍然工作,但在恢复之前,我重新添加动画。让我来介绍一下这个简单的例子:
假设我有一个名为 movingView 的UIView。 UIView的中心通过标准[ UIView animateWithDuration ... ]调用动画。使用上面提到的 QA1673 代码,它可以很好地暂停/恢复(当不退出应用程序时)...但无论如何,我很快意识到退出时,无论是否暂停,动画都被完全删除......而且我在这里。
所以在这个例子中,这就是我所做的:
当应用退出后台时,我这样做:
animationViewPosition = [[movingView.layer animationForKey:@"position"] copy]; // I know position is the key in this case...
[self pauseLayer:movingView.layer]; // this is the Apple method from QA1673
现在在我的 UIApplicationWillEnterForegroundNotification 处理程序中:
if (animationViewPosition != nil)
{
[movingView.layer addAnimation:animationViewPosition forKey:@"position"]; // re-add the core animation to the view
[animationViewPosition release]; // since we 'copied' earlier
animationViewPosition = nil;
}
[self resumeLayer:movingView.layer]; // Apple's method, which will resume the animation at the position it was at when the app exited
这就是它!到目前为止它对我有用:)
只需为每个动画重复这些步骤,即可轻松扩展它以获得更多动画或视图。它甚至适用于暂停/恢复UIImageView动画,即标准[ imageView startAnimating ]。 (顺便说一下)它的图层动画关键字是“内容”。
清单1暂停和恢复动画。
-(void)pauseLayer:(CALayer*)layer
{
CFTimeInterval pausedTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil];
layer.speed = 0.0;
layer.timeOffset = pausedTime;
}
-(void)resumeLayer:(CALayer*)layer
{
CFTimeInterval pausedTime = [layer timeOffset];
layer.speed = 1.0;
layer.timeOffset = 0.0;
layer.beginTime = 0.0;
CFTimeInterval timeSincePause = [layer convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime;
layer.beginTime = timeSincePause;
}
答案 1 :(得分:17)
经过大量搜索和与iOS开发专家的讨论后,QA1673似乎无法帮助暂停,后台处理,然后转移到前台。我的实验甚至表明,从动画中发起的委托方法(例如animationDidStop
)变得不可靠。
有时候他们会开火,有时却不会开火。
这会产生很多问题,因为这意味着,您不仅会看到暂停时的不同屏幕,而且还会中断当前运动中的事件序列。
到目前为止我的解决方案如下:
当动画开始时,我得到开始时间:
mStartTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil];
当用户点击暂停按钮时,我会从CALayer
:
[layer removeAnimationForKey:key];
我使用CACurrentMediaTime()
获得绝对时间:
CFTimeInterval stopTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil];
使用mStartTime
和stopTime
我计算了偏移时间:
mTimeOffset = stopTime - mStartTime;
我还将对象的模型值设置为presentationLayer
的模型值。所以,我的stop
方法如下所示:
//--------------------------------------------------------------------------------------------------
- (void)stop
{
const CALayer *presentationLayer = layer.presentationLayer;
layer.bounds = presentationLayer.bounds;
layer.opacity = presentationLayer.opacity;
layer.contentsRect = presentationLayer.contentsRect;
layer.position = presentationLayer.position;
[layer removeAnimationForKey:key];
CFTimeInterval stopTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil];
mTimeOffset = stopTime - mStartTime;
}
在恢复时,我会根据mTimeOffset
重新计算暂停动画的剩余部分。这有点乱,因为我正在使用CAKeyframeAnimation
。我根据mTimeOffset
找出了哪些关键帧是未完成的。此外,我考虑到暂停可能发生在帧的中间,例如在f1
和f2
之间。该时间从该关键帧的时间开始扣除。
然后我重新将这个动画添加到图层:
[layer addAnimation:animationGroup forKey:key];
另一件需要记住的事情是,您需要检查animationDidStop
中的标记,并且如果标记为removeFromSuperlayer
,则仅使用YES
从父级删除动画图层。这意味着在暂停期间图层仍然可见。
这种方法似乎非常费力。它确实有效!我希望能够使用QA1673简单地完成此操作。但目前为背景,它不起作用,这似乎是唯一的解决方案。
答案 2 :(得分:14)
令人惊讶的是,这并不是那么简单。我创建了一个基于cclogg方法的类别,它应该使它成为一个单行。
CALayer+MBAnimationPersistence
在设置所需的动画后,只需在图层上调用MB_setCurrentAnimationsPersistent
。
[movingView.layer MB_setCurrentAnimationsPersistent];
或指定应明确保留的动画。
movingView.layer.MB_persistentAnimationKeys = @[@"position"];
答案 3 :(得分:7)
我无法评论,所以我会将其添加为答案
我使用了cclogg的解决方案,但是当动画的视图从他的超级视图中删除,再次添加,然后转到后台时,我的应用程序崩溃了。
通过将animation.repeatCount
设置为Float.infinity
,动画变得无限
我的解决方案是将animation.removedOnCompletion
设置为false
它的工作原理非常奇怪,因为动画永远不会完成。如果有人有解释,我想听听
另一个提示:如果从超级视图中删除视图。不要忘记通过拨打NSNotificationCenter.defaultCenter().removeObserver(...)
来删除观察者。
答案 4 :(得分:4)
我根据@cclogg和@Matej Bukovinski的答案编写了一个 Swift 4 版本扩展。您只需拨打layer.makeAnimationsPersistent()
全部要点:CALayer+AnimationPlayback.swift, CALayer+PersistentAnimations.swift
核心部分:
public extension CALayer {
static private var persistentHelperKey = "CALayer.LayerPersistentHelper"
public func makeAnimationsPersistent() {
var object = objc_getAssociatedObject(self, &CALayer.persistentHelperKey)
if object == nil {
object = LayerPersistentHelper(with: self)
let nonatomic = objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC
objc_setAssociatedObject(self, &CALayer.persistentHelperKey, object, nonatomic)
}
}
}
public class LayerPersistentHelper {
private var persistentAnimations: [String: CAAnimation] = [:]
private var persistentSpeed: Float = 0.0
private weak var layer: CALayer?
public init(with layer: CALayer) {
self.layer = layer
addNotificationObservers()
}
deinit {
removeNotificationObservers()
}
}
private extension LayerPersistentHelper {
func addNotificationObservers() {
let center = NotificationCenter.default
let enterForeground = NSNotification.Name.UIApplicationWillEnterForeground
let enterBackground = NSNotification.Name.UIApplicationDidEnterBackground
center.addObserver(self, selector: #selector(didBecomeActive), name: enterForeground, object: nil)
center.addObserver(self, selector: #selector(willResignActive), name: enterBackground, object: nil)
}
func removeNotificationObservers() {
NotificationCenter.default.removeObserver(self)
}
func persistAnimations(with keys: [String]?) {
guard let layer = self.layer else { return }
keys?.forEach { (key) in
if let animation = layer.animation(forKey: key) {
persistentAnimations[key] = animation
}
}
}
func restoreAnimations(with keys: [String]?) {
guard let layer = self.layer else { return }
keys?.forEach { (key) in
if let animation = persistentAnimations[key] {
layer.add(animation, forKey: key)
}
}
}
}
@objc extension LayerPersistentHelper {
func didBecomeActive() {
guard let layer = self.layer else { return }
restoreAnimations(with: Array(persistentAnimations.keys))
persistentAnimations.removeAll()
if persistentSpeed == 1.0 { // if layer was playing before background, resume it
layer.resumeAnimations()
}
}
func willResignActive() {
guard let layer = self.layer else { return }
persistentSpeed = layer.speed
layer.speed = 1.0 // in case layer was paused from outside, set speed to 1.0 to get all animations
persistAnimations(with: layer.animationKeys())
layer.speed = persistentSpeed // restore original speed
layer.pauseAnimations()
}
}
答案 5 :(得分:3)
以防任何人需要针对此问题的Swift 3解决方案:
您所要做的就是从这个类中继承您的动画视图。 它始终保持并恢复其层上的所有动画。
class ViewWithPersistentAnimations : UIView {
private var persistentAnimations: [String: CAAnimation] = [:]
private var persistentSpeed: Float = 0.0
override init(frame: CGRect) {
super.init(frame: frame)
self.commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.commonInit()
}
func commonInit() {
NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActive), name: NSNotification.Name.UIApplicationWillEnterForeground, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(willResignActive), name: NSNotification.Name.UIApplicationDidEnterBackground, object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
func didBecomeActive() {
self.restoreAnimations(withKeys: Array(self.persistentAnimations.keys))
self.persistentAnimations.removeAll()
if self.persistentSpeed == 1.0 { //if layer was plaiyng before backgorund, resume it
self.layer.resume()
}
}
func willResignActive() {
self.persistentSpeed = self.layer.speed
self.layer.speed = 1.0 //in case layer was paused from outside, set speed to 1.0 to get all animations
self.persistAnimations(withKeys: self.layer.animationKeys())
self.layer.speed = self.persistentSpeed //restore original speed
self.layer.pause()
}
func persistAnimations(withKeys: [String]?) {
withKeys?.forEach({ (key) in
if let animation = self.layer.animation(forKey: key) {
self.persistentAnimations[key] = animation
}
})
}
func restoreAnimations(withKeys: [String]?) {
withKeys?.forEach { key in
if let persistentAnimation = self.persistentAnimations[key] {
self.layer.add(persistentAnimation, forKey: key)
}
}
}
}
extension CALayer {
func pause() {
if self.isPaused() == false {
let pausedTime: CFTimeInterval = self.convertTime(CACurrentMediaTime(), from: nil)
self.speed = 0.0
self.timeOffset = pausedTime
}
}
func isPaused() -> Bool {
return self.speed == 0.0
}
func resume() {
let pausedTime: CFTimeInterval = self.timeOffset
self.speed = 1.0
self.timeOffset = 0.0
self.beginTime = 0.0
let timeSincePause: CFTimeInterval = self.convertTime(CACurrentMediaTime(), from: nil) - pausedTime
self.beginTime = timeSincePause
}
}
On Gist: https://gist.github.com/grzegorzkrukowski/a5ed8b38bec548f9620bb95665c06128
答案 6 :(得分:2)
我能够通过保存当前动画的副本并将其添加回简历来恢复动画(但不是动画位置)。我在加载时调用startAnimation,在进入前景时调用startAnimation,在进入后台时调用暂停。
- (void) startAnimation {
// On first call, setup our ivar
if (!self.myAnimation) {
self.myAnimation = [CABasicAnimation animationWithKeyPath:@"transform"];
/*
Finish setting up myAnimation
*/
}
// Add the animation to the layer if it hasn't been or got removed
if (![self.layer animationForKey:@"myAnimation"]) {
[self.layer addAnimation:self.spinAnimation forKey:@"myAnimation"];
}
}
- (void) pauseAnimation {
// Save the current state of the animation
// when we call startAnimation again, this saved animation will be added/restored
self.myAnimation = [[self.layer animationForKey:@"myAnimation"] copy];
}
答案 7 :(得分:1)
我使用cclogg的解决方案效果很好。我还想分享一些可能对其他人有帮助的其他信息,因为它让我感到沮丧了一段时间。
在我的应用程序中,我有许多动画,有些动画永远循环,有些只运行一次并随机生成。 cclogg的解决方案对我有用,但是当我添加一些代码
时- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag
为了在只有一次性动画完成时执行某些操作,只要这些特定的一次性动画在暂停时运行,当我恢复我的应用程序(使用cclogg的解决方案)时,此代码将会触发。所以我添加了一个标志(我的自定义UIImageView类的成员变量)并在你恢复所有图层动画的部分中将其设置为YES(cclogg的resumeLayer
,类似于Apple解决方案QA1673)以防止这种情况发生。我为每个正在恢复的UIImageView执行此操作。然后,在animationDidStop
方法中,仅当该标志为NO时才运行一次性动画处理代码。如果是,则忽略处理代码。无论如何都要将标志切换回NO。这样,当动画真正完成时,您的处理代码将会运行。像这样:
- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag
if (!resumeFlag) {
// do something now that the animation is finished for reals
}
resumeFlag = NO;
}
希望能有所帮助。
答案 8 :(得分:0)
我正在像这样识别手势状态:
// Perform action depending on the state
switch gesture.state {
case .changed:
// Some action
case .ended:
// Another action
// Ignore any other state
default:
break
}
我要做的就是将.ended
的大小写更改为.ended, .cancelled
。
答案 9 :(得分:0)
当视图从可见区域消失时,iOS将删除所有动画(不仅当应用程序进入后台时)。为了解决这个问题,我创建了自定义CALayer
子类并覆盖了2个方法,因此系统不会删除动画-removeAnimation
和removeAllAnimations
:
class CustomCALayer: CALayer {
override func removeAnimation(forKey key: String) {
// prevent iOS to clear animation when view is not visible
}
override func removeAllAnimations() {
// prevent iOS to clear animation when view is not visible
}
func forceRemoveAnimation(forKey key: String) {
super.removeAnimation(forKey: key)
}
}
在要将此层用作主层的视图中,覆盖layerClass
属性:
override class var layerClass: AnyClass {
return CustomCALayer.self
}
要暂停和恢复动画,请执行以下操作:
extension CALayer {
func pause() {
guard self.isPaused() == false else {
return
}
let pausedTime: CFTimeInterval = self.convertTime(CACurrentMediaTime(), from: nil)
self.speed = 0.0
self.timeOffset = pausedTime
}
func resume() {
guard self.isPaused() else {
return
}
let pausedTime: CFTimeInterval = self.timeOffset
self.speed = 1.0
self.timeOffset = 0.0
self.beginTime = 0.0
self.beginTime = self.convertTime(CACurrentMediaTime(), from: nil) - pausedTime
}
func isPaused() -> Bool {
return self.speed == 0.0
}
}