22

我是 Objective-C/iOS 编程的新手,我试图了解 UIView 动画是如何在幕后工作的。

假设我有这样的代码:

[UIView animateWithDuration:2.0 animations:^{
    self.label.alpha = 1.0;
}];

作为animations参数传递的东西是一个可以执行的 Objective-C 块(类似于其他语言中的 lambdas/匿名函数),然后它将alpha属性label从当前值更改为 1.0。

但是,该块不接受动画进度参数(例如从 0.0 到 1.0 或从 0 到 1000)。我的问题是动画框架如何使用这个块来了解中间帧,因为块只指定最终状态。

编辑:我的问题是关于方法的幕后操作animateWithDuration而不是使用它的方法

我对animateWithDuration代码如何工作的假设如下:

  1. animateWithDuration为所有视图对象激活某种特殊状态,其中实际上并未执行更改,而只是注册了更改。
  2. 它执行块并注册更改。
  3. 它查询视图对象的更改状态并取回初始值和目标值,因此知道要更改哪些属性以及在什么范围内执行更改。
  4. 它根据持续时间和初始/目标值计算中间帧,并触发动画。

有人可以阐明是否animateWithDuration真的以这种方式工作吗?

4

3 回答 3

24

当然,我不知道幕后到底发生了什么,因为 UIKit 不是开源的,而且我不在 Apple 工作,但这里有一些想法:

在引入基于块的UIView动画方法之前,动画视图看起来像这样,这些方法实际上仍然可用:

[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:duration];
myView.center = CGPointMake(300, 300);
[UIView commitAnimations];

知道了这一点,我们可以像这样实现我们自己的基于块的动画方法:

+ (void)my_animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations
{
    [UIView beginAnimations:nil context:nil];
    [UIView setAnimationDuration:duration];
    animations();
    [UIView commitAnimations];
}

animateWithDuration:animations:...这将与现有方法完全相同。

从等式中取出块,很明显必须有某种全局动画状态,UIView然后当它们在动画块内完成时用于动画对其(可动画的)属性的更改。这必须是某种堆栈,因为您可以嵌套动画块。

实际的动画由 Core Animation 执行,它在层级工作——每个都有一个负责动画和合成UIView的支持实例,而视图主要只处理触摸事件和坐标系转换。CALayer

我不会在这里详细介绍 Core Animation 的工作原理,您可能需要阅读Core Animation Programming Guide来了解它。本质上,它是一个在层树中动画变化的系统,无需显式计算每个关键帧(实际上从核心动画中获取中间值相当困难,您通常只需指定 from 和 to 值、持续时间等,然后让系统注意细节)。

因为UIView是基于 a 的CALayer,所以它的很多属性其实都是在底层实现的。例如,当您设置或获取时view.center,这view.layer.location与更改其中任何一个都会更改另一个。

层可以显式地进行动画处理CAAnimation(这是一个具有许多具体实现的抽象类,例如CABasicAnimation简单的事物和CAKeyframeAnimation更复杂的事物)。

那么UIView属性设置器可以做些什么来完成动画块中的“神奇”动画更改呢?让我们看看是否可以重新实现其中之一,为简单起见,让我们使用setCenter:.

my_animateWithDuration:animations:首先,这是上面使用 global的方法的修改版本CATransaction,以便我们可以在我们的方法中找出setCenter:动画应该花费多长时间:

- (void)my_animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations
{
    [CATransaction begin];
    [CATransaction setAnimationDuration:duration];

    animations();

    [CATransaction commit];
}

请注意,我们不再使用beginAnimations:...and commitAnimations,所以不做任何其他事情,什么都不会被动画化。

现在,让我们setCenter:UIView子类中覆盖:

@interface MyView : UIView
@end

@implementation MyView
- (void)setCenter:(CGPoint)position
{
    if ([CATransaction animationDuration] > 0) {
        CALayer *layer = self.layer;
        CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"];
        animation.fromValue = [layer valueForKey:@"position"];
        animation.toValue = [NSValue valueWithCGPoint:position];
        layer.position = position;
        [layer addAnimation:animation forKey:@"position"];
    }
}
@end

在这里,我们使用 Core Animation 设置了一个显式动画,它为底层的location属性设置动画。动画的持续时间将自动从CATransaction. 让我们试一试:

MyView *myView = [[MyView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
myView.backgroundColor = [UIColor redColor];
[self.view addSubview:myView];

[self my_animateWithDuration:4.0 animations:^{
    NSLog(@"center before: %@", NSStringFromCGPoint(myView.center));
    myView.center = CGPointMake(300, 300);
    NSLog(@"center after : %@", NSStringFromCGPoint(myView.center));
}];

我并不是说这正是UIView动画系统的工作方式,只是为了展示在原则上是如何工作的。

于 2013-03-03T18:38:53.870 回答
1

未指定中间帧的值;值的动画(在这种情况下为 alpha,还有颜色、位置等)在先前设置的值和动画块内设置的目标值之间自动生成。您可以通过指定选项使用来影响曲线animateWithDuration:delay:options:animations:completion:(默认为UIViewAnimationOptionCurveEaseInOut,即值变化的速度将加速和减速)。

请注意,任何先前设置的动画值更改都将首先完成,即每个动画块都指定一个从前一个结束值到新值的新动画。您可以指定UIViewAnimationOptionBeginFromCurrentState从已经进行中的动画状态开始新动画。

于 2013-03-02T15:58:52.093 回答
0

这会将块内指定的属性从当前值更改为您提供的任何值,并将在持续时间内线性执行。

因此,如果原始 alpha 值是 0,这将在 2 秒内淡入标签。如果原始值已经是 1.0,那么您根本看不到任何效果。

在幕后,UIView负责确定需要进行多少动画帧的更改。

您还可以通过将缓动曲线指定为 来更改更改发生的速率UIViewAnimationOption。再次UIView为您处理补间。

于 2013-03-02T16:01:25.093 回答