UIView如何在底层使用block进行动画处理

25

我是Objective-C/iOS编程的新手,我正在尝试理解UIView动画的工作原理。

假设我有以下代码:

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

作为animations参数传递的是一个Objective-C块(类似于其他语言中的lambda /匿名函数),可以执行并将labelalpha属性从当前值更改为1.0。但是,该块不接受动画进度参数(例如从0.0到1.0或从0到1000)。我的问题是动画框架如何使用此块来了解中间帧,因为该块仅指定最终状态。
编辑: 我的问题更多地关于animateWithDuration方法的操作方式,而不是使用它的方式。
我对animateWithDuration代码工作方式的假设如下:
  1. animateWithDuration激活了所有视图对象的特殊状态,其中更改不会实际执行,而只是注册。
  2. 它执行块并注册更改。
  3. 它查询视图对象以获取更改状态,并返回初始和目标值,因此知道要更改哪些属性以及在什么范围内执行更改。
  4. 基于持续时间和初始/目标值计算中间帧,并触发动画。

有人能否解释一下animateWithDuration是否真的按照这种方式工作?

3个回答

25
当然,我并不知道 UIKit 在底层做了什么,因为它不是开源的,而且我也不在苹果工作。但是以下是我的一些想法:
在引入基于块的 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使用它来在动画块内完成对其(可动画)属性的更改。这必须是某种堆栈,因为您可以有嵌套的动画块。

实际的动画由核心动画执行,它在层级别上工作-每个UIView都有一个后备CALayer实例,负责动画和合成,而视图主要处理触摸事件和坐标系转换。

我不会在这里详细介绍Core Animation的工作原理,您可能需要阅读Core Animation Programming Guide了解更多信息。基本上,它是一种在图层树中动画更改的系统,而无需明确计算每个关键帧(从Core Animation中获取中间值实际上相当困难,通常只需指定起始值和结束值、持续时间等,让系统处理细节即可)。

因为UIView基于CALayer,所以它的许多属性实际上是在底层层中实现的。例如,当您设置或获取view.center时,这与view.layer.location相同,并且更改其中任何一个也会更改另一个。
图层可以使用CAAnimation(它是一个抽象类,具有许多具体实现,例如用于简单事物的CABasicAnimation和用于更复杂的东西的CAKeyframeAnimation)进行显式动画化。
那么,UIView属性设置器可能如何完成“神奇”地在动画块内部动画化更改?为了简单起见,让我们看看是否可以重新实现其中一个方法,例如setCenter:
首先,这是上面的my_animateWithDuration:animations:方法的修改版本,它使用全局的CATransaction,以便我们可以在setCenter:方法中找到动画应该花费的时间:
- (void)my_animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations
{
    [CATransaction begin];
    [CATransaction setAnimationDuration:duration];

    animations();

    [CATransaction commit];
}

请注意,我们不再使用beginAnimations:...commitAnimations,因此如果不做任何其他操作,将不会进行任何动画。
现在,让我们在UIView子类中覆盖setCenter:
@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

在这里,我们使用核心动画设置一个显式动画,该动画可以通过动画底层的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 动画系统的确切工作方式,只是为了展示原理上它是如何运作的。


1
你认为为什么它被设计成如此神奇的方式,而不是更直接的方式? - Novellizator

1
中间帧的值未指定;在动画块内,值的动画(例如 alpha、颜色、位置等)会在之前设置的值和目标值之间自动生成。您可以使用 animateWithDuration:delay:options:animations:completion: 指定选项来影响曲线(默认为 UIViewAnimationOptionCurveEaseInOut,即值变化的速度将加速和减速)。
请注意,任何先前设置的值的动画更改都将先完成,即每个动画块都会从上一个结束值到新值指定一个新动画。您可以指定 UIViewAnimationOptionBeginFromCurrentState 从已经进行的动画状态开始新的动画。

0

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

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

在幕后,UIView 负责计算需要多少动画帧来完成更改。

您还可以通过指定缓动曲线作为 UIViewAnimationOption 来更改更改发生的速率。同样,UIView 会为您处理缓动。


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