iOS 动画篇 - UIKit动画(二)

UIViewPropertyAnimator 的使用介绍

Posted by LOLITA0164 on November 27, 2018

简单使用篇

简介

iOS10带来了很多新特性,其中有个 UIViewPropertyAnimator 类,光从名字上就可以看出,这是一个操作属性动画的类。实际上,这个类能够让我们对视图进行动画控制,我们除了可进行正常的运行动画,如开始、暂停、重启等操作动画,还可以将动画转换为交互式动画,任意的控制时间。

它可以对视图的可动画属性进行操作,例如frame,center,alpha 和 transform等,并且可以任意的添加多个动画块和完成块,相比于之前的 UIView 动画,它改变了我们习惯的动画流程,变得更加灵活。

简单例子

改变一个视图的 center 动画:

// 创建动画器
UIViewPropertyAnimator* animator = [[UIViewPropertyAnimator alloc] initWithDuration:1.0
                                                                              curve:UIViewAnimationCurveEaseOut
                                                                         animations:^{
    self.contentView.center = self.view.center;
}];
// 开始动画
[animator startAnimation];

移动位置的动画

在使用 UIViewPropertyAnimator 做动画时,需要关注下面几个点:

  • 包含改变一个或多个视图属性的动画块
  • 用于定义动画运行过程中的时间速率曲线
  • 动画的持续时间(以秒为单位)
  • 动画完成块(可选)

在上面的简单实例中,我们在1秒的时间内,改变了视图的中心位置,其中动画块即为 animations 的代码块,在此代码块中我们可以针对可动动规划进行新增改变。对于运行的动画时间速率,动画器 animator 支持 UIKit 动画中的时间速率函数,即linear、ease-in、ease-out等。

一般来说,我们所创建的动画器都是处于非活跃状态,需要手动调用-startAnimation将其变为活跃状态执行动画。

初始化动画器

UIViewPropertyAnimator 为我们提供了多个快捷创建动画器的方法。

  • 使用内置时间速率函数

-initWithDuration:curve:animations:

这种方式就是我们节例子中的使用到的创建方法,curve 参数即时间速率函数,其所支持的以下几种:

UIViewAnimationCurveEaseInOut //缓进缓出
UIViewAnimationCurveEaseIn //缓进
UIViewAnimationCurveEaseOut //缓出
UIViewAnimationCurveLinear //线性匀速

四种内置时间速率

如果所说 UIKit 提供的速率曲线函数不能够满足你的执行动画的速率要求,你还可以通过自定义来创建自己的速度曲线。

  • 使用三次贝塞尔曲线

-initWithDuration:controlPoint1:controlPoint2:animations:

三次贝塞尔曲线的起点为(0,0)且其终点为(1,1),因此两个控制点的取值范围是(0,1)。

  • 使用基于弹簧的弹性

-initWithDuration:dampingRatio:animations:

dampingRatio:所对应的参数叫做阻尼,一般去值为(0,1)较低的阻尼值对应较小阻力和在静止之前更多更大的振荡。反之则阻力大,振荡少而小。例如你想不振荡的情况下平滑的减速动画,就可以指定值为1。

// 创建动画器
UIViewPropertyAnimator* animator = [[UIViewPropertyAnimator alloc] initWithDuration:1.0
                                                                       dampingRatio:0.35
                                                                         animations:^{
    self.contentView.center = CGPointMake(self.view.center.x+100, self.view.center.y);
}];
// 开始动画
[animator startAnimation];

弹性动画

  • 使用自定义时间速率对象

-initWithDuration:timingParameters:

该方法需要你提供支持 UITimingCurveProvider 协议的对象,如果你要自定义实现此协议,必须提供所有属性的实现。

系统有两个遵循该协议的类

UICubicTimingParameters 
UISpringTimingParameters

如果你查看UICubicTimingParameters类时,你会发现,这个类也只是提供了支持 UIKit 内置的时间速率曲线和三次贝塞尔曲线。类似的UISpringTimingParameters 也提供了CASpringAnimation中的几个物理参数。

示例:我们通过该方法实现一下和上一个方法类似的效果

// 弹性的时间速率
UISpringTimingParameters* parameters = [[UISpringTimingParameters alloc] initWithDampingRatio:0.35];
// 创建动画器
UIViewPropertyAnimator* animator = [[UIViewPropertyAnimator alloc] initWithDuration:1.0 timingParameters:parameters];
// 由于该创建方法没有动画块,因此需要自行追加
[animator addAnimations:^{
    self.contentView.center = CGPointMake(self.view.center.x+100, self.view.center.y);
}];
// 开始动画
[animator startAnimation];

弹性的时间速率

如果说,你受不了每次都需要主动调用-startAnimation方法来启动视图动画,还是习惯 UIView 的快捷使用!👌,苹果似乎注意到了这一点,为了适应开发者的习惯,除了上述几种创建动画器的方式,还有一种可以启动开启动画并能返回当前动画器的方法。

  • 类方法便捷

+ runningPropertyAnimatorWithDuration:delay:options:animations:completion:

该方法提供了动画的几个相对比较重要的参数,如动画执行时间、延迟时间、时间速率、动画块、完成块。该方法兼容了 UIView 动画块的形式。

[UIViewPropertyAnimator runningPropertyAnimatorWithDuration:1.0
                                                      delay:0
                                                    options:UIViewAnimationCurveEaseOut
                                                 animations:^{
    self.contentView.center = CGPointMake(self.view.center.x+100, self.view.center.y);
}
                                                 completion:^(UIViewAnimatingPosition finalPosition) {}];

快捷使用

控制动画

UIViewPropertyAnimator 遵守了 UIViewImplicitlyAnimating 协议,而UIViewImplicitlyAnimating 协议是 UIViewAnimating 协议的子类,该类定义了如何控制动画的协议。除了上一节中使用到的-startAnimation方法,还有其他几个控制动画的方法。

  • 开始执行动画

-startAnimation:方法可以启动动画或者在暂停动画后恢复动画。 -startAnimationAfterDelay::和上面方法类似,不过可以指定延迟执行的时间

  • 暂停动画

-pauseAnimation:暂停动画,当使用该方法后,动画会停留在“当前位置”,会保持当前的状态。暂停后可以使用-startAnimation恢复,恢复的动画会从“当前位置”继续剩余的动画,包括剩余的时间。

示例:我们执行一个2秒时长的动画,在1秒处停止,延迟1秒后恢复动画,让其继续执行。

UIViewPropertyAnimator* animator = [UIViewPropertyAnimator runningPropertyAnimatorWithDuration:2.0 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
    self.contentView.center = CGPointMake(self.view.center.x+100, self.view.center.y);
} completion:nil];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    // 暂停当前动画
    [animator pauseAnimation];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        // 恢复动画
        [animator startAnimation];
    });
});

动画的暂停和恢复

  • -stopAnimation::停止动画

停止动画有多种情况,由于动画状态的机制(进阶篇会讲)的存在,当我们停止动画后,这些动画状态信息何去何从?苹果给出了两种的去处,一种时清除所有状态信息,动画器重置为初始的非活跃状态,以等待下一个动画;另外一种是保留所有状态信息,等待下一步操作。这里的 withoutFinishing 参数就是用来指明去处。

参数 withoutFinishing,表示是否应执行任何最终操作。如果值为 YES,则会清除任何动画并将动画器重置为非活跃状态,并且不会执行完成块的回调。

示例:我们执行一个2秒的动画,在一秒处停止当前动画,并且在完成块中将视图的背景色更改为红色。

UIViewPropertyAnimator* animator = [UIViewPropertyAnimator runningPropertyAnimatorWithDuration:2.0 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
    self.contentView.center = CGPointMake(self.view.center.x+100, self.view.center.y);
} completion:^(UIViewAnimatingPosition finalPosition) {
    // 动画的完成回调
    self.contentView.backgroundColor = UIColor.redColor;
}];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    [animator stopAnimation:YES];
});

withoutFinishing为YES的情况

运行结果发现,动画在1秒处停止了,但是并没变成红色背景,这说明,此时的动画器并不会执行完成块。

当参数为 NO 时,动画器状态为 stopped,此时通常会配合finishAnimationAtPosition:使用,该方法可以帮助动画器执行最终的完成块的内容,当然,这两个方法的目的是停下当前动画,让你完成此刻需要完成的内容,如其他动画,之后,你再使用finishAnimationAtPosition:完成动画的回调以及动画需要停止的位置。

在演示示例之前,我们来介绍一下finishAnimationAtPosition:

  • -finishAnimationAtPosition::结束动画

该方法可以将处于 stopped 状态的动画重置为非活跃状态,并执行动画的完成块。

此方法通常配合 -stopAnimation: 使用,并且该方法必须在动画器状态为 stopped 状态才可以,否则会出现错误。该方法的 UIViewAnimatingPosition 参数有一下三种:

UIViewAnimatingPositionEnd //动画的终点位置
UIViewAnimatingPositionStart //动画的开头位置
UIViewAnimatingPositionCurrent //动画当前位置

指定 UIViewAnimatingPositionCurrent 以使视图属性与其当前值保持不变。

示例:我们继续之前的例子,这次我们配合 -finishAnimationAtPosition: 方法使用。

UIViewPropertyAnimator* animator = [UIViewPropertyAnimator runningPropertyAnimatorWithDuration:2.0 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
    self.contentView.center = CGPointMake(self.view.center.x+100, self.view.center.y);
} completion:^(UIViewAnimatingPosition finalPosition) {
    // 动画的完成回调
    self.contentView.backgroundColor = UIColor.redColor;
}];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    [animator stopAnimation:NO];
    // 动画器的状态必须是stopped
    if (animator.state==UIViewAnimatingStateStopped) {
        [animator finishAnimationAtPosition:UIViewAnimatingPositionCurrent];
    }
});

withoutFinishing为NO的情况

我们发现,动画运行1秒后停止了,并且背景色被填充为红色,这说明-finishAnimationAtPosition:触发完成块,这一点和之前的例子是不同的。另外,我们看到视图停下来之后就保持在了当前位置,这是因为我们给的结束位置就是 Current。下图演示了位置的不同参数的效果。

start、current、end三种不同位置

交互式动画

fractionComplete 属性

UIViewPropertyAnimator 类中有一个fractionComplete属性,这个属性表示当前动画的完成的百分比,并且这个属性不是只读的属性,这说明我们可以精准的控制动画的整个过程。利用它,我们可以制作交互式动画。交互式动画的好处是:对于多个视图、非常复杂的视图变化加以控制变得简单。

示例:

@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIView *blueView;
@property (weak, nonatomic) IBOutlet UIView *redView;
@property (weak, nonatomic) IBOutlet UISlider *slider;
@property (strong, nonatomic) UIViewPropertyAnimator* animator;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // 初始化动画器
    
    self.animator = [[UIViewPropertyAnimator alloc] initWithDuration:2.0 curve:UIViewAnimationCurveLinear animations:^{
        // 红色视图
        
        CGRect fram = CGRectMake(self.slider.center.x - 50/2.0, self.slider.center.y - 100, 50, 50);
        self.redView.frame = fram;
        self.redView.transform = CGAffineTransformMakeRotation(M_PI);
        self.redView.backgroundColor = UIColor.blueColor;
        // 蓝色视图
        
        self.blueView.frame = fram;
        self.blueView.transform = self.redView.transform;
        self.blueView.backgroundColor = UIColor.redColor;
    }];
    [self.slider addTarget:self action:@selector(change:) forControlEvents:UIControlEventValueChanged];
}

-(void)change:(UISlider*)slider{
    CGFloat value = slider.value;
    // 更改动画完成度
    
    self.animator.fractionComplete = value;
}

控制两个视图之间的动画

需要注意的是,在使用fractionComplete之前,最好调用-pauseAnimation暂停当前动画,此时动画处于活跃状态,但非isRunning

修改动画

正如前面简介中提到过,UIViewPropertyAnimator 可以修改动画,甚至是在动画处于运行状态。我们可以添加多个动画块、完成块,设置是暂停掉正在执行的动画,并且修改它的剩余时间,这让我们更加的精准的控制视图的动画行为。

  • -addAnimations::为视图添加动画块

我们之前在使用自定义时间速率对象初始化动画器时,曾经使用到过该方法,此方法可以让我们对视图的动画追加多个动画块。

示例:我们为正在运动的视图添加渐变动画

UIViewPropertyAnimator* animator = [UIViewPropertyAnimator runningPropertyAnimatorWithDuration:1.0 delay:0 options:UIViewAnimationCurveLinear animations:^{
    self.contentView.center = CGPointMake(self.view.center.x+100, self.view.center.y);
} completion:nil];
// 追加动画块
[animator addAnimations:^{
    self.contentView.backgroundColor = UIColor.redColor;
}];

追加动画块

我们追加的动画块会和其他动画共享动画器剩余的时间。

示例:延迟追加动画

为了明显的看出效果,我们给予更长的动画时间,并在运行一段时间后,追加动画。

UIViewPropertyAnimator* animator = [UIViewPropertyAnimator runningPropertyAnimatorWithDuration:4.0 delay:0 options:UIViewAnimationCurveLinear animations:^{
    self.contentView.center = CGPointMake(self.view.center.x+100, self.view.center.y);
} completion:nil];
// 追加动画块
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    [animator addAnimations:^{
        self.contentView.backgroundColor = UIColor.redColor;
    }];
});

共享时间

我们看到2秒后追加的渐变动画在剩余的2秒内完成了渐变效果。

当然,苹果已经给出了类似的方法,无需我们主动写延迟方法。就像下面的演示。

  • -addAnimations:delayFactor::延迟追加动画块

参数delayFactor是指时间因子,即动画的进度,取值区间为(0,1)。比如,0.5表示动画执行一半的时候执行。

示例:我们使用该方法完成之前的例子

UIViewPropertyAnimator* animator = [UIViewPropertyAnimator runningPropertyAnimatorWithDuration:4.0 delay:0 options:UIViewAnimationCurveLinear animations:^{
    self.contentView.center = CGPointMake(self.view.center.x+100, self.view.center.y);
} completion:nil];
// 延迟追加动画块
[animator addAnimations:^{
    self.contentView.backgroundColor = UIColor.redColor;
} delayFactor:0.5];

延迟加载动画

  • -addCompletion::追加完成块

既然动画块都可以追加修改,那么完成块也应该相应的有追加方法呀!

在初始化以动画器一节中,我们发现大部分都是不带有完成块回调的,苹果似乎考虑到开发过程中很少会关心动画的完成事件吧,因此为了方法的简洁性,就让其变成了可选特性,又或者这样设计会让动画变得更加的灵活,因为这样,动画的完成事件就无需紧跟在初始化方法上了。

示例:我们在动画执行完成时执行一些事情

这里为了方便看到效果,我们就直接来改变视图的颜色。

UIViewPropertyAnimator* animator = [[UIViewPropertyAnimator alloc] initWithDuration:1.0 curve:UIViewAnimationCurveLinear animations:^{
    self.contentView.center = CGPointMake(self.view.center.x+100, self.view.center.y);
}];
[animator startAnimation];
// 追加完成块
[animator addCompletion:^(UIViewAnimatingPosition finalPosition) {
    self.contentView.backgroundColor = UIColor.redColor;
}];

追加完成块

在【控制动画】一节中,我们提到过,我们可以调用-startAnimation:来恢复暂停后的动画,但是这样做的话,动画的形式依旧是之前设置好的情况,它并不会发生变化。那么,如果想要暂定动画后,执行其他时间速率的动画该怎么办呢?别急,既然说了 UIViewPropertyAnimator 可以让我们任意的控制动画,必然会提供该类方法。

  • -continueAnimationWithTimingParameters:durationFactor::暂停后修改动画方式继续执行

该类方法只会在调用-pauseAnimation方法之后起到作用,此时的动画状态为,活跃但非isRunning

参数durationFactor是时间因子,表示动画的进度。通常可以取fractionComplete属性。

示例:我们将匀速运行中的视图中途改为弹性运动

UIViewPropertyAnimator* animator = [[UIViewPropertyAnimator alloc] initWithDuration:2 curve:UIViewAnimationCurveLinear animations:^{
    self.contentView.center = CGPointMake(self.view.center.x+100, self.view.center.y);
}];
[animator startAnimation];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    // 暂停动画之后
    [animator pauseAnimation];
    // 暂停动画之后,修改动画时间速率后继续动画
    UISpringTimingParameters* timing = [[UISpringTimingParameters alloc] initWithDampingRatio:0.2];
    [animator continueAnimationWithTimingParameters:timing durationFactor:animator.fractionComplete];
});

中途更改动画

上图演示了中途更改的动画情况,其中,底下的视图为参考视图,即未改变的匀速运动。

以上就是快速入门使用的所有教程了,相信经过一系列的介绍之后,你能够快速的使用新的动画方式了。

接下来的进阶篇,会讲解一些 UIViewPropertyAnimator 的一些细节部分。

进阶篇

动画协议

其实要介绍 UIViewPropertyAnimator 类前,应该先介绍其遵循的动画协议–UIViewAnimating 和 UIViewImplicitlyAnimating 。前者是后者的父类,我们先来解释 UIViewAnimating 协议。该协议定义了操作动画的基本方法,包括启动、停止、暂停动画的能力。另外还有几个属性用于反映动画的当前状态信息。

UIViewPropertyAnimator 遵循并实现了 UIViewAnimating 协议的所有方法,因此我们可以用 UIViewPropertyAnimator 来实现动画的控制。如果你想在自定义类中也遵循此协议,最好实现所有的协议方法和属性。

动画的状态

UIViewPropertyAnimator 有着一套完整的状态机制。在动画器处理一组动画时,都会伴随着这一系列的动画状态。这些状态定义了动画器的行为,包括它是如何处理变化的。如果你在实现自定义动画器,必须遵循这些状态的转换并准确的更新状态属性。下图显示了发生的状态和状态转换关系。

状态转换关系

Inactive非活跃状态是动画器的初始状态。每个新创建的动画器都会处于非活跃状态下启动。相对的,动画正常完成后会返回到非活跃状态。

当我们调用-startAnimation -pauseAnimation方法时,此时动画器会变为Active活跃状态。此状态下的动画器正在运行或暂停状态。如果是动画被暂停,我们此时还可以修改动画时间速率曲线,让后让其继续运行到预期结束,结束后的动画器状态依旧是Inactive非活跃状态,等待我们使用一组新的动画重新配置它,以便开始新的动画。

当我们开启动画之后,调用-stopAnimation:方法会停止正在运行的动画,此时视图会被保留在停止的那一刻的值。此方法的参数值决定了当前的动画信息是否被擦除。如果参数 withoutFinishing 是 YES,则表示擦除当前动画的信息,动画器进入Inactive状态,需要注意,这种情况下,动画器是不会执行完成块的,换句话说你无法在完成块中得到动画结束信息,此时如果你需要知道动画结束的事件,你可以使用 KVO 的方法监听属性isRunning获得。如果参数 withoutFinishing 是 NO,则表示保留当前动画的信息,动画器进入Stopped状态,此时我们可以去完成其他的操作,如执行其他动画。然后我们调用方法-finishAnimationAtPosition:以结束此次动画,动画器顺理成章的进入到Inactive状态。注意,这情情况下,动画器可以顺利的执行完成块内容。

动画状态的几个枚举:

typedef NS_ENUM(NSInteger, UIViewAnimatingState)
{
    UIViewAnimatingStateInactive, // The animation is not executing.
    UIViewAnimatingStateActive,   // The animation is executing.
    UIViewAnimatingStateStopped,  // The animation has been stopped and has not transitioned to inactive.
} NS_ENUM_AVAILABLE_IOS(10_0) ;

协议内容

方法

  • -startAnimation 开始动画

不可以在动画器调用方法-stopAnimation:直接结束动画后再次调用-startAnimation,换句话说,使用过程中,出现下面情况会出错:

错误

错误

我们发现,在我们-stopAnimation:指定参数为 YES时,动画器状态由活跃状态Active 转变为非活跃状态Inactive,此时再次调用-startAnimation时,系统抛出了异常。

而我们指定参数为 NO时,

正常

此时动画器状态为stopped,程序并未出错。

正常

这一点和官方文档的说明并不一致,目前还不是很清楚原因。

It is a programmer error to call this method while the state of the animator is set to UIViewAnimatingStateStopped.

⚠️:11-29,以上结论基于10.3.2系统,但是笔者使用11以上的系统发现,结论和上述相反,却和官方文档一致,即动画器状态为stopped下,不能使用-startAnimation。这一点让我更加凌乱了,难道后面的系统修正了?

  • -startAnimationAfterDelay: 延迟后开始动画

上面的开启动画一样的注意点同样适用。(请注意上面 11-29 的说明)

另外经测试发现,-pauseAnimation之后调用-startAnimationAfterDelay:会发生程序错误。

  • -pauseAnimation 暂停动画

暂停动画后,可以使用-startAnimation方法重新恢复动画,另外你也可以使用协议UIViewImplicitlyAnimating中的continueAnimationWithTimingParameters:durationFactor:恢复动画。如果动画已经暂停,则再次调用-pauseAnimation不会执行任何操作。

经测试发现如果动画器从未启动过,直接调用-pauseAnimation方法,如果紧接着调用-startAnimation或者continueAnimationWithTimingParameters:durationFactor:是无法恢复动画的,之间需要大于千分之一秒的时间,就像下面的情况:

无法恢复动画的情况:

[self.animator pauseAnimation];
[self.animator startAnimation];
//[self.animator continueAnimationWithTimingParameters:[[UICubicTimingParameters alloc] initWithAnimationCurve:UIViewAnimationCurveLinear] durationFactor:0.5];

如果之前调用过开启动画,则可以恢复动画

[self.animator startAnimation];
...
[self.animator pauseAnimation];
[self.animator startAnimation];
//[self.animator continueAnimationWithTimingParameters:[[UICubicTimingParameters alloc] initWithAnimationCurve:UIViewAnimationCurveLinear] durationFactor:0.5];

又或者添加延迟

// 从未开启过,暂停动画
[self.animator pauseAnimation];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.002 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    [self.animator startAnimation];
//    [self.animator continueAnimationWithTimingParameters:[[UICubicTimingParameters alloc] initWithAnimationCurve:UIViewAnimationCurveLinear] durationFactor:0.5];
});

至于为什么会时这种情况,官方文档并未指出,或许只有苹果自己清楚吧。

另外,官方文档指出动画器为stopped状态时,调用-pauseAnimation会出现程序错误,但并没有。

⚠️:11-29,以上结论基于10.3.2系统,使用11以上的系统发现并不会出现【无法启动暂停动画】的情况,并且,动画器为stopped状态时,调用-pauseAnimation时,确实出现程序错误。

  • -stopAnimation: 停止动画

需要注意的是,不可以在动画器状态由Active转为为stopped的时候再调用该方法。下面的使用会发生程序错误:

错误使用

上图中的调用-pauseAnimation将动画器转为Active也会出现错误。

这一点,官方文档却并未提及。why?

  • -finishAnimationAtPosition: 结束动画

该方法通常会和上面的停止方法相结合,用来结束当前停止下来(状态为stopped)动画顺利回到Inactive状态。经测试,该方法只认stopped状态,其他两个状态都会发生错误。

属性

  • fractionComplete动画执行的进度

描述了当前动画的进度,可被更改,当动画处于停止时,可配合手势等实现交互式动画。

  • reversed是否可以反转动画

关于反转动画,目前还未知如果实现反转动画。

  • state动画器的状态

  • running动画的运行状态,支持KVO

修改动画协议

UIViewImplicitlyAnimating 继承自 UIViewAnimating 协议,在后者协议的基础上又添加了一些额外的修改动画的方法。而我们使用的 UIViewPropertyAnimator 动画器就遵循了这个相对完善的协议,并实现了所有的方法。

方法

  • -addAnimations: 添加动画块

使用此方法可以将新的动画块添加到自定义动画对象。新的动画会与先前的动画一起运行,并从当前时间开始并与任何原始动画同时结束。

  • -addAnimations:delayFactor:添加延迟动画块

同上,不过会从指定的延迟开始并与任何原始动画同时结束。

参数 delayFactor:用于延迟动画开始的时间因子。该值必须介于0.0和1.0之间。将此值乘以动画剩余持续时间,作为实际延迟。例如,如果值0.5、动画器的持续时间为2.0,则延迟一秒执行动画。

  • -addCompletion:添加动画完成块

回调动画完成的事件,你可以在该 block 中完成其他操作。

参数 withoutFinishing 有三种,表示最后动画结束的位置。

typedef NS_ENUM(NSInteger, UIViewAnimatingPosition) {
    UIViewAnimatingPositionEnd,
    UIViewAnimatingPositionStart,
    UIViewAnimatingPositionCurrent,
} NS_ENUM_AVAILABLE_IOS(10_0);

如果动画正常完成结束,位置参数为UIViewAnimatingPositionEnd,即最终的期望位置;

如果动画执行过程中,调用了-stopAnimation:,并且制定的参数为 YES,动画器则不会调用完成块;

如果动画执行过程中,调用了-stopAnimation:,并且制定的参数为 NO,此时需要调用finishAnimationAtPosition:配置结束动画,此时完成块中的位置参数由方法finishAnimationAtPosition:决定。

  • -continueAnimationWithTimingParameters:durationFactor:调整暂停的动画的时间速率曲线和持续时间

参数 parameters 是指时间速率曲线,系统提供了两种,兼容了 UIKit 内置的四种时间速率曲线、三次贝塞尔曲线、弹簧式的弹性动画。

UICubicTimingParameters
UISpringTimingParameters

参数 durationFactor 是指动画原始持续时间的因子,取值为(0,1),将此值乘以动画的原始持续时间,作为新的持续时间。

UIViewPropertyAnimator* animator = [[UIViewPropertyAnimator alloc] initWithDuration:2 curve:UIViewAnimationCurveLinear animations:^{
    self.contentView.center = CGPointMake(self.view.center.x+100, self.view.center.y);
}];
[animator startAnimation];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    [animator pauseAnimation];
    [animator continueAnimationWithTimingParameters:[[UICubicTimingParameters alloc] initWithAnimationCurve:UIViewAnimationCurveLinear] durationFactor:animator.fractionComplete];
});

修改动画持续时间

如果我们将 durationFactor 传递为当前动画器的进度值 fractionComplete ,你会发现执行的动画并没有什么变化,这是因为 fractionComplete 的值乘以原始持续时间就等于动画剩余的时间。

但是我们将值放大,比 fractionComplete 的值要大,那么动画的剩余时间就会被拉长,剩下的动画会在新的时间内完成。

示例:我们将动画1秒后,将剩余的时间缩短为0.1倍

UIViewPropertyAnimator* animator = [[UIViewPropertyAnimator alloc] initWithDuration:2 curve:UIViewAnimationCurveLinear animations:^{
    self.contentView.center = CGPointMake(self.view.center.x+100, self.view.center.y);
}];
[animator startAnimation];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    [animator pauseAnimation];
    [animator continueAnimationWithTimingParameters:[[UICubicTimingParameters alloc] initWithAnimationCurve:UIViewAnimationCurveLinear] durationFactor:0.1];
});

修改剩余动画的持续时间

当前就像之前介绍的,你也可以改换当前动画的时间速率曲线,或者更换为弹性动画。

动画器

介绍完 UIViewPropertyAnimator 的两种协议之后,我们来看下 UIViewPropertyAnimator 中的一些其小细节。

三次贝塞尔曲线

在构建动画器的方法中,有一个之前我们一提而过的方法:

-initWithDuration:controlPoint1:controlPoint2:animations:

这个方法可以让我们自定义时间速率曲线,采用的是三次贝塞尔曲线,可能有些人并不清楚什么是三次贝塞尔曲线。三次贝塞尔曲线在绘制图形时经常出现,是有起点、终点以及两个控制点产生的曲线。文字描述比较抽象,我们来看下图:

三次贝塞尔曲线

该曲线的起点为(0,0),其终点为(1,1)。point1 和 point2 参数是定义生成的贝塞尔曲线形状的控制点。其中,起点和控制点1的连线为曲线的切线,终点和控制点2的连线也是曲线的切线,控制点1和控制点2的连写也是曲线的切线,这样,产生的曲线就是三次贝塞尔曲线。

该曲线的斜率定义了动画的不同时间速率,斜率越大,速度越快,斜率越小速度越慢。上图显示了一个速率曲线,其中动画快速启动并快速完成,但在中间部分运行得相对较慢。

属性

  • duration 只读

动画的持续时间,只有在初始化动画器时指定该值,稍后添加的动画仅在剩余的时间内运行。剩余时间由公式(1.0 - fractionComplete)* 持续时间确定。

  • delay 只读

延迟动画时间,默认值为0。如果要为此属性设置值,启动动画时则需要使用startAnimationAfterDelay:方法

  • timingParameters 只读

描述速度的曲线,和duration一样,只有在初始化 animator 指定该值。可以使用此属性稍后获取这些参数。

  • interruptible

动画中是否可被打断。当此属性的值为 YES 时,我们可以使用-pauseAnimation-stopAnimation:方法来中断动画并进行更改。当此属性的值为 NO 时,在调用startAnimation方法后,动画将运行至完成(并且不会中断)。

如果使用动画器来实现可中断的视图控制器转换,则此属性必须为 YES。

  • userInteractionEnabled

动画中用户是否可交互。默认值为 YES。当此属性的值为 YES 时,触摸事件将正常传递给视图,否则在动画持续时间内会忽略用户的触摸事件。

  • manualHitTestingEnabled

动画中点击测试的能力。默认为 NO。

  • scrubsLinearly

暂停的动画是否使用线性擦除或者使用指定的时间速率曲线。iOS11之后可用。

  • pausesOnCompletion

动画完成后是否保持活动状态。默认值为 NO。iOS11之后可用。

当此属性的值为 YES 时,动画器完成后动画后将保持Active状态,并且不会执行完成块。此时我们可以撤消动画。当此属性的值为 NO 时,动画完成后,动画器执行完成块,自动转换为Inactive状态,从而结束动画。

注:由于 YES 的情况下,动画器并且不会执行完成块,因此如果你想要知道动画的结束事件,你需要监听动画器的running属性。

补充

  • 设置同一可动画属性

-addAnimations:方法可以让我们添加多个属性动画块,那么,如果两个或多个动画需要同时改变相同的属性会发生什么呢?苹果采用的是“后者优先”原则。即:后添加的动画效果会覆盖之前的动画效果。但有趣的是,这将导致卡顿,因为需要组合新旧动画,在旧动画淡出的同时会隐约看见新动画。

示例:

UIViewPropertyAnimator* animator = [UIViewPropertyAnimator runningPropertyAnimatorWithDuration:1.0 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
    self.contentView.transform = CGAffineTransformMakeScale(1.5, 1.5);
[animator addAnimations:^{
    self.contentView.transform = CGAffineTransformMakeScale(0.5, 0.5);
}];

设置同一动画属性

我们发现,后添加的缩小为0.5的动画效果覆盖了之前的放大为1.5的动画效果。但这似乎看不出所为卡顿的效果,那我们来看下填充背景色会发生什么。

UIViewPropertyAnimator* animator = [UIViewPropertyAnimator runningPropertyAnimatorWithDuration:1.0 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
    self.contentView.backgroundColor = UIColor.redColor;
} completion:nil];
[animator addAnimations:^{
    self.contentView.backgroundColor = UIColor.yellowColor;
}];

设置同一动画属性

该示例中,视图的最初颜色为蓝色,在第一个动画块中,我们将其设置为红色,后又设置为黄色。在该动画运行过程中,我们发现,视图立刻被设置为红色,然后由红色渐变为黄色。因此,在多个动画块中设置同一个动画属性并不可控,我们应该尽可能的避免这种情况的出现。


总结

UIViewPropertyAnimator 类让我们能够精准的控制视图动画的每个细节。我们可以使用该类的示例完成各种动画的设置,中途修改动画,甚至可以便捷的完成交互性的动画,这彻底改变了我们设置视图动画的习惯,这些改变令人惊喜万分。

在进阶篇中,我们发现了很多异常的情况,并且在不同的系统上有着不同的表现,甚至是完全相反的情况,这一点让人非常的疑惑,笔者猜想可能是苹果在 iOS11 系统之后改变了 UIViewPropertyAnimator 的一些实现细节部分,导致了前后不一致的情况,但是在这种情况下,想要使用该类需要异常谨慎。

但是,不能够因噎废食,如果我们开发过程中能够注意到这些异常的情况,避免这些异常操作,UIViewPropertyAnimator 不失为一个较为良好的动画类。

根据之前的问题,有几点建议:

  • 创建完动画器之后,请使用-startAnimation-startAnimationAfterDelay:方法开启动画,或者直接使用+ runningPropertyAnimatorWithDuration:delay:options:animations:completion:

  • stopped情况下,请配合-finishAnimationAtPosition:方法结束后续动画,而非其他方法

  • 在暂停动画的情况下,请使用-startAnimation-continueAnimationWithTimingParameters:durationFactor:方法恢复动画,可能的话,请保证动画是由运行中暂停的,或者延迟大于千分之秒的时间恢复动画


相关阅读