MENU

交互动画之 UIViewPropertyAnimator

本文将介绍新增的交互动画类型 UIViewPropertyAnimator ,以及为什么我们要开始使用它。在 iOS10 之前,animateWithDuration:animations: 函数是 UIView 层级系统动画交互的默认选项。不过这套系统 API 远不能满足当下越来越复杂的交互设计,而 Facebook’s POP 类型的交互框架则成功上位填补了缺口。不过好在 Apple 也注意到了这个问题,并在新系统中带来了更强大的动画 API 。

UIViewPropertyAnimator 与之前的 UIView animations 有很多差异。其中最明显的差异就是它允许你持有对象实例。另外,除了能够对动画过程进行暂停和恢复外,你还以对进行逆转和销毁操作。这些特性极大的拓展了视图动画交互的想象空间。下面我们看个示例:

Example

示例模仿了 Youtube 视频播放时的浮动交互效果,视频既可以全屏也可以在右下角进行播放。用户只需要上下进行拖动就能实现状态的切换。这个动画也是 UIViewPropertyAnimator 一个非常好的应用示例,接下来看看它是如何实现的。

为了弄清楚 UIViewPropertyAnimator 的原理,首先需要了解动画过程中的各种状态:

  • Inactive:对象初始化或者动画结束后所处的状态。

  • Active:在调用 startAnimation() 、 pauseAnimation() 方法后对象就处于激活状态,直到动画完成或者手动调用 stopAnimation() 结束动画。

  • Stopped:在 stopAnimation() 被调用之后动画对象就处于停止状态,并且保留当前的所有属性值。如下图所示,停止状态无法反向回到激活态。

State

动画属性的修改只能在 Inactive 状态下进行。

下面通过代码介绍 UIViewPropertyAnimator 的使用,先对视图添加手势操作:

- (void)viewDidLoad {
    [super viewDidLoad]; 
    self.panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];
    [self.view addGestureRecognizer:self.panGestureRecognizer];
}

另外,这里需要一些辅助变量。一个枚举类型用于标记视图的当前状态,并依据状态值决定动画是否反向运行。 创建一个动画示例,并在开始动画之前存储视图的起始 frame 用于反向动画。之所以记录起始 frame 是因为: reversed 属性在一次动画过程中只能设置一次,多次修改会导致动画失效。

typedef NS_ENUM(NSInteger, PlayerState) {
    PlayerStateThumbnail,       // 缩略状态
    PlayerStateFullscreen,          // 全屏状态
};

@interface ViewController ()

@property (weak, nonatomic) IBOutlet UIView *playerView;        
@property (nonatomic) UIViewPropertyAnimator *playerViewAnimator;
@property (nonatomic) PlayerState playerState;
@property (nonatomic) CGRect originalPlayerViewFrame;
@property (nonatomic) UIPanGestureRecognizer *panGestureRecognizer;

@end

接下来需要讲手势的变化传递到视图并使之做出相关的响应。下面是手势开始时的处理:

- (void)panningBegan {
    if (self.playerViewAnimator.isRunning) {
        return;
    }

    CGRect targetFrame;

    switch (self.playerState) {
        case PlayerStateThumbnail:
            self.originalPlayerViewFrame = self.playerView.frame;
            targetFrame = self.view.frame;
            break;
        case PlayerStateFullscreen:
            targetFrame = self.originalPlayerViewFrame;
            break;
    }

    self.playerViewAnimator = [[UIViewPropertyAnimator alloc] initWithDuration:0.5 dampingRatio:0.8 animations:^{
        self.playerView.frame = targetFrame;
    }];
}

上面的代码中,我们首先确保了当前没有动画在运行中,紧接着根据当前视图状态初始化了目标 frame。接下来,我们需要处理手指持续移动时的动画状态:

- (void)panningChangedWithTranslation:(CGPoint)translation {

    if (self.playerViewAnimator.isRunning) {
        return;
    }

    CGFloat translatedY = self.view.center.y + translation.y;

    CGFloat progress = 0.001 ;
    switch (self.playerState) {
        case PlayerStateThumbnail:
            progress = 1 - (translatedY / self.view.center.y);
            break;
        case PlayerStateFullscreen:
            progress = (translatedY / self.view.center.y) - 1;
    }

    progress = MAX(0.001, MIN(0.999, progress));

    self.playerViewAnimator.fractionComplete = progress;
}

这里计算了不同情形下移动量所占的对应比例,并且将其赋值给动画中的 fractionComplete 属性。

现在,当用户上下移动手指的时视图已经能够顺利的切换到相应的状态。但是,用户松开手指的时候也能的意愿并不一定是状态切换也可能会是取消动画回到起始的状态。所以,这里应该存在一些阀值和属性判断完善用户体验。

下面就使用手势里常见的位移和速度来充当:

- (void)panningEndedWithTranslation:(CGPoint)translation velocity:(CGPoint)velocity {

    self.panGestureRecognizer.enabled = NO;

    CGFloat screenHeight = [[UIScreen mainScreen] bounds].size.height;
    __weak ViewController *weakSelf = self;

    switch (self.playerState) {
        case PlayerStateThumbnail:
            if (translation.y <= -screenHeight / 3 || velocity.y <= -100) {
                self.playerViewAnimator.reversed = NO;
                [self.playerViewAnimator addCompletion:^(UIViewAnimatingPosition finalPosition) {
                    weakSelf.playerState = PlayerStateFullscreen;
                    weakSelf.panGestureRecognizer.enabled = YES;
                }];
            } else {
                self.playerViewAnimator.reversed = YES;
                [self.playerViewAnimator addCompletion:^(UIViewAnimatingPosition finalPosition) {
                    weakSelf.playerState = PlayerStateThumbnail;
                    weakSelf.panGestureRecognizer.enabled = YES;
                }];
            }
            break;
        case PlayerStateFullscreen:
            if (translation.y >= screenHeight / 3 || velocity.y >= 100) {
                self.playerViewAnimator.reversed = NO;
                [self.playerViewAnimator addCompletion:^(UIViewAnimatingPosition finalPosition) {
                    weakSelf.playerState = PlayerStateThumbnail;
                    weakSelf.panGestureRecognizer.enabled = YES;
                }];
            } else {
                self.playerViewAnimator.reversed = YES;
                [self.playerViewAnimator addCompletion:^(UIViewAnimatingPosition finalPosition) {
                    weakSelf.playerState = PlayerStateFullscreen;
                    weakSelf.panGestureRecognizer.enabled = YES;
                }];
            }
            break;
    }

    CGVector velocityVector = CGVectorMake(velocity.x / 100, velocity.y / 100);
    UISpringTimingParameters *springParameters = [[UISpringTimingParameters alloc] initWithDampingRatio:0.8 initialVelocity:velocityVector];

    [self.playerViewAnimator continueAnimationWithTimingParameters:springParameters durationFactor:1.0];
}

如果当前播放视图的高度超过了 1/3 或者手指松开时的速度够快的话,状态切换动画会继续进行下去。否则,视图动画会反向进行恢复到原有状态。值得注意的是,UIViewPropertyAnimator 实例对象可以动态的添加多个 animation block、completion block ,哪怕此时动画正在运行中。

最后,我们需要将上面的处理函数与手势状态关联起来:

- (void)handlePan:(UIPanGestureRecognizer *)recognizer {
    CGPoint translation = [recognizer translationInView:self.view.superview];

    if (recognizer.state == UIGestureRecognizerStateBegan) {
        [self panningBegan];
    }
    
    if (recognizer.state == UIGestureRecognizerStateEnded) {
        CGPoint velocity = [recognizer velocityInView:self.view];
        [self panningEndedWithTranslation:translation velocity:velocity];     
    } else {
        CGPoint translation = [recognizer translationInView:self.view.superview];
        [self panningChangedWithTranslation:translation];
    }
}

文章到此为止,相信你对这个新交互动画 API 已经有了基本的了解。