如何停止并反转UIView动画?

16

我已经为一个UIView添加了动画效果,使得当用户触摸切换按钮时它会缩小,再次触摸按钮时可以回到原始大小。目前一切正常。问题在于动画需要一些时间 - 例如3秒钟。在此期间,我仍希望用户能够与界面进行交互。因此,当用户在动画仍在进行时再次触摸按钮时,动画应该停止在当前位置并反向播放。

在Apple的问答中,我找到了一种立即暂停所有动画的方法:

https://developer.apple.com/library/ios/#qa/qa2009/qa1673.html

但我没有看到从这里反转动画(并省略其余部分的初始动画)的方法。我该如何实现这个目标?


返回内容: 我已经为一个UIView添加了动画效果,使得当用户触摸切换按钮时它会缩小,再次触摸按钮时可以回到原始大小。目前一切正常。问题在于动画需要一些时间 - 例如3秒钟。在此期间,我仍希望用户能够与界面进行交互。因此,当用户在动画仍在进行时再次触摸按钮时,动画应该停止在当前位置并反向播放。
在Apple的问答中,我找到了一种立即暂停所有动画的方法:
https://developer.apple.com/library/ios/#qa/qa2009/qa1673.html
但我没有看到从这里反转动画(并省略其余部分的初始动画)的方法。我该如何实现这个目标?
- (IBAction)toggleMeter:(id)sender {
    if (self.myView.hidden) {        
        self.myView.hidden = NO;
        [UIView animateWithDuration:3 animations:^{
            self.myView.transform = expandMatrix;
        } completion:nil];
    } else {
        [UIView animateWithDuration:3 animations:^{
            self.myView.transform = shrinkMatrix;
        } completion:^(BOOL finished) {
            self.myView.hidden = YES;
        }];
    }
}
3个回答

44
除了下面的方法(我们从展示层获取当前状态,停止动画,从保存的展示层重置当前状态,并启动新动画),还有一个更简单的解决方案。
如果在iOS 8.0之前的版本中进行基于块的动画,如果您想停止动画并启动新动画,您可以简单地使用UIViewAnimationOptionBeginFromCurrentState选项。(在iOS 8中, 默认行为不仅从当前状态开始,而且以反映当前位置和当前速度的方式进行,因此基本上不必担心这个问题。请参阅WWDC 2014视频Building Interruptible and Responsive Interactions了解更多信息。)
[UIView animateWithDuration:3.0
                      delay:0.0
                    options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction
                 animations:^{
                     // specify the new `frame`, `transform`, etc. here
                 }
                 completion:NULL];

你可以通过停止当前动画并从当前位置开始新的动画来实现这一点。使用Quartz 2D可以做到这一点:
  1. Add QuartzCore.framework to your project if you haven't already. (In contemporary versions of Xcode, it is often unnecessary to explicitly do this as it is automatically linked to the project.)

  2. Import the necessary header if you haven't already (again, not needed in contemporary versions of Xcode):

    #import <QuartzCore/QuartzCore.h>
    
  3. Have your code stop the existing animation:

    [self.subview.layer removeAllAnimations];
    
  4. Get a reference to the current presentation layer (i.e. the state of the view as it is precisely at this moment):

    CALayer *currentLayer = self.subview.layer.presentationLayer;
    
  5. Reset the transform (or frame or whatever) according to the current value in the presentationLayer:

    self.subview.layer.transform = currentLayer.transform;
    
  6. Now animate from that transform (or frame or whatever) to the new value:

    [UIView animateWithDuration:1.0
                          delay:0.0
                        options:UIViewAnimationOptionAllowUserInteraction
                     animations:^{
                         self.subview.layer.transform = newTransform;
                     }
                     completion:NULL];
    

将所有内容结合起来,这是一个可以将我的变换比例从2.0x切换到标识并返回的例程:

- (IBAction)didTouchUpInsideAnimateButton:(id)sender
{
    CALayer *currentLayer = self.subview.layer.presentationLayer;

    [self.subview.layer removeAllAnimations];

    self.subview.layer.transform = currentLayer.transform;

    CATransform3D newTransform;

    self.large = !self.large;

    if (self.large)
        newTransform = CATransform3DMakeScale(2.0, 2.0, 1.0);
    else
        newTransform = CATransform3DIdentity;

    [UIView animateWithDuration:1.0
                          delay:0.0
                        options:UIViewAnimationOptionAllowUserInteraction
                     animations:^{
                         self.subview.layer.transform = newTransform;
                     }
                     completion:NULL];
}

或者,如果你想要切换frame的大小从100x100到200x200再切回来:

- (IBAction)didTouchUpInsideAnimateButton:(id)sender
{
    CALayer *currentLayer = self.subview.layer.presentationLayer;

    [self.subview.layer removeAllAnimations];

    CGRect newFrame = currentLayer.frame;

    self.subview.frame = currentLayer.frame;

    self.large = !self.large;

    if (self.large)
        newFrame.size = CGSizeMake(200.0, 200.0);
    else
        newFrame.size = CGSizeMake(100.0, 100.0);

    [UIView animateWithDuration:1.0
                          delay:0.0
                        options:UIViewAnimationOptionAllowUserInteraction
                     animations:^{
                         self.subview.frame = newFrame;
                     }
                     completion:NULL];
}

顺便提一下,对于非常快速的动画,这通常并不重要,但对于您这样的慢速动画,您可能希望将反转动画的持续时间设置为您当前动画的进度(例如,如果您进行了3.0秒的动画中的0.5秒,当您反转时,您可能不想花费3.0秒来反转您已完成的小部分动画,而只需0.5秒)。因此,代码应该是这样的:
- (IBAction)didTouchUpInsideAnimateButton:(id)sender
{
    CFTimeInterval duration = kAnimationDuration;             // default the duration to some constant
    CFTimeInterval currentMediaTime = CACurrentMediaTime();   // get the current media time
    static CFTimeInterval lastAnimationStart = 0.0;           // media time of last animation (zero the first time)

    // if we previously animated, then calculate how far along in the previous animation we were
    // and we'll use that for the duration of the reversing animation; if larger than
    // kAnimationDuration that means the prior animation was done, so we'll just use
    // kAnimationDuration for the length of this animation

    if (lastAnimationStart)
        duration = MIN(kAnimationDuration, (currentMediaTime - lastAnimationStart));

    // save our media time for future reference (i.e. future invocations of this routine)

    lastAnimationStart = currentMediaTime;

    // if you want the animations to stay relative the same speed if reversing an ongoing
    // reversal, you can backdate the lastAnimationStart to what the lastAnimationStart
    // would have been if it was a full animation; if you don't do this, if you repeatedly
    // reverse a reversal that is still in progress, they'll incrementally speed up.

    if (duration < kAnimationDuration)
        lastAnimationStart -= (kAnimationDuration - duration);

    // grab the state of the layer as it is right now

    CALayer *currentLayer = self.subview.layer.presentationLayer;

    // cancel any animations in progress

    [self.subview.layer removeAllAnimations];

    // set the transform to be as it is now, possibly in the middle of an animation

    self.subview.layer.transform = currentLayer.transform;

    // toggle our flag as to whether we're looking at large view or not

    self.large = !self.large;

    // set the transform based upon the state of the `large` boolean

    CATransform3D newTransform;

    if (self.large)
        newTransform = CATransform3DMakeScale(2.0, 2.0, 1.0);
    else
        newTransform = CATransform3DIdentity;

    // now animate to our new setting

    [UIView animateWithDuration:duration
                          delay:0.0
                        options:UIViewAnimationOptionAllowUserInteraction
                     animations:^{
                         self.subview.layer.transform = newTransform;
                     }
                     completion:NULL];
}

谢谢Rob。这是一个非常好的和全面的解决方案。但它只能与UIViewAnimationOptionCurveLinear完全精确地工作。如果我使用UIViewAnimationOptionCurveEaseInOut,反向动画将不会与原始动画完全对称,因为它会缓慢开始并加速,而原始动画会突然停止。是否有一种方法可以以某种方式记住动画的状态,例如“速度”和“加速度”,即获取动画曲线中的当前点? - Mischa
@Mischa 有方法可以做到这一点,但那时你就不再处于漂亮、简单的基于块的动画世界中了(因为没有办法做出部分,比如87.529%的动画曲线)。这将是可怕的工作(例如自定义核心动画代码),对于可能不可观察的东西(请参见下面我的替代建议,它可能接近您的请求),但肯定可以完成。我建议,如果动画完成度低于90%,则在反向上进行EaseOut(即假设当时您正在快速移动),否则进行EaseInOut - Rob
@Mischa 说实话,我甚至怀疑完美对称的想法是否正确,因为如果你正在使用 EaseInOut(这意味着你正在寻找平滑的动画效果),那么说你要突然反转已经完成了50%的动画(因此以最快的速度移动),并立即将其转向并以不太明显的方式以最快的速度朝另一个方向移动,那么,哇,这将与原始的 EaseInOut 意图背道而驰,不是吗? - Rob
1
Rob,你的两个评论都是正确的。在大多数情况下,完美的对称并不是必需的,特别是在我的情况下。我已经实现了你的建议,它运行良好。但我想补充一点,这种解决方案会产生一个副作用: 如果您在动画仍在进行中时反复翻转几次 - 每个新动画将比上一个动画更快。这是因为每次反转动画时,新动画持续时间只是前一个动画持续时间的一小部分(因子0 ... 1)。但实际上我喜欢这种效果。 - Mischa
1
@Mischa,你发现了时间问题,干得好。我已经解决了这个问题。我一直在思考“如果你已经进行了三分之一的动画,我们不想花费三秒钟来反转它”的问题,甚至没有考虑到“好吧,我们要反转之前仍在进行中的反转”这个问题,但现在已经解决了。坦白地说,我的大多数动画都非常快,所以我根本不用担心时间问题,但是因为你的动画太慢了,所以我才加入了这个功能。很高兴能帮到你。 - Rob
谢谢你的回复,Rob。这个代码片段和解释非常棒,最近我遇到了一个类似的情况,不得不实现类似的功能。有一点我学到了,如果你使用completion块并取消了动画,那么completion块仍然会执行,如果你有连续的动画,下一个动画可能会触发,所以我们必须利用completion块中的finished布尔值来区分成功完成和失败或停止。 - serge-k

1
有一个常见的技巧可以用来做到这一点,但需要编写一个单独的方法来缩小(以及另一个类似的扩展方法):
- (void) shrink {  
    [UIView animateWithDuration:0.3
                     animations:^{
                        self.myView.transform = shrinkALittleBitMatrix;
                     }
                     completion:^(BOOL finished){
                          if (continueShrinking && size>0) {
                              size=size-1;
                              [self shrink];      
                           }
                     }];
}

现在的关键是将3秒的缩小动画分解为10个(或更多)0.3秒的动画,每个动画中你都要缩小1/10的整个动画:shrinkALittleBitMatrix。每完成一个动画后,只有当bool变量continueShrinking为true且int变量size为正数时(完整大小的视图为size=10,最小大小的视图为size=0),才会调用相同的方法。当你按下按钮时,将ivar continueShrinking更改为FALSE,然后调用expand。这将在不到0.3秒的时间内停止动画。
好吧,你需要填写细节,但我希望这可以帮助你。

这是一个很好的解决方案,可以弥补框架中应该包含的功能。但是这里的问题基本上与Rob的解决方案相同:它仅支持线性动画,因为每个部分动画的持续时间都相同。当然,我可以通过根据ivar size的值更改持续时间来模仿UIViewAnimationOptionCurveEaseInOut的行为,从而模拟加速和减速。但那将是另一种解决方法,使事情变得更加复杂和不整洁。 - Mischa

0

首先:如何在视图中移除或取消动画?

[view.layer removeAllAnimations] 

如果视图有许多动画,例如,一个动画从上到下移动,另一个动画从左到右移动;
您可以像这样取消或删除特定的动画:
[view.layer removeAnimationForKey:@"someKey"];
// the key is you assign when you create a animation 
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"someKey"]; 

当你这样做时,动画将停止,它会调用它的代理:
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag

如果flag等于1,表示动画已完成。 如果flag等于0,则表示动画未完成,可能被取消或移除。

第二点:因此,在这个委托方法中,您可以做任何想做的事情。

如果您想在删除代码执行时获取视图的框架,可以这样做:

currentFrame = view.layer.presentationlayer.frame;

注意:

当你获取当前帧并移除动画时,视图也会在一段时间内进行动画,因此currentFrame不是设备屏幕上的最后一帧。

我现在无法解决这个问题。如果有一天我能解决,我会更新这个问题。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接