【iOS】今日头条的转场动画设置+手势控制
前言
最近公司有个需求,做一个今日头条的用户动态的进入和退出的动画效果,并且退场时,可以自己点击退出,也可以手势下滑退出。头条的效果如下:
今日头条效果.gif
分析
1、动画转场的实现
首先我们需要实现UINavigationDelegate
的
- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC
此方法返回一个遵守UIViewControllerAnimatedTransitioning
的class,在里面书写我们要实现的动画效果
2、触发pop的手势处理
同样的需要实现UINavigationDelegate
的
- (id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController
此方法返回一个遵守UIViewControllerInteractiveTransitioning
的class,一般会用UIPercentDrivenInteractiveTransition
。这个percent手势处理转场的方式,只要按时机调用以下三个方法
/// 返回这个转场完成的百分比 0~1
- (void)updateInteractiveTransition:(CGFloat)percentComplete;
/// 取消转场
- (void)cancelInteractiveTransition;
/// 完成转场
- (void)finishInteractiveTransition;
而如果我们需要实现下滑退出的话,就需要配合UIPanGestureRecognizer
进行使用了,Demo核心的手势处理代码如下:
- (CGFloat)percentForGesture:(UIPanGestureRecognizer *)gesture{
// 最多只能移动SL_SCREEN_HEIGHT * 0.5
CGFloat maxOffset = ZFPlayer_ScreenHeight * 0.5;
CGFloat y = [gesture locationInView:[UIApplication sharedApplication].keyWindow].y;
// 移动的距离
CGFloat distance = y - self.startOffsetY;
distance = MIN(maxOffset, distance);
double degree = (distance / maxOffset) * M_PI_2;
// 为增量实现一个曲线变化的效果
double x = 1 - (sin(degree));
// 计算增量
CGFloat delta = distance - self.lastOffsetY;
self.lastOffsetY = self.lastOffsetY + x * delta;
self.lastOffsetY = MAX(self.lastOffsetY, 0);
CGFloat percent = self.lastOffsetY / maxOffset;
return percent;
}
- (void)panAction: (UIPanGestureRecognizer *)gestureRecognizer
{
switch (gestureRecognizer.state){
case UIGestureRecognizerStateBegan:
{
self.startOffsetY = [gestureRecognizer locationInView:[UIApplication sharedApplication].keyWindow].y;
[self.navigationController popViewControllerAnimated:YES];
break;
}
case UIGestureRecognizerStateChanged:
// 调用updateInteractiveTransition来更新动画进度
// 里面嵌套定义 percentForGesture 方法计算动画进度
[self.interactiveGes updateInteractiveTransition:[self percentForGesture:gestureRecognizer]];
break;
case UIGestureRecognizerStateEnded:
//判断手势位置,要大于一般,就完成这个转场,要小于一半就取消
if ([self percentForGesture:gestureRecognizer] >= 0.4) {
self.transition.isComplete = YES;
// 完成交互转场
[self.interactiveGes finishInteractiveTransition];
}else {
// 取消交互转场
[self.interactiveGes cancelInteractiveTransition];
}
break;
default:
[self.interactiveGes cancelInteractiveTransition];
break;
}
}
要注意的是,在pan手势触发的时候,需要先调用[self.navigationController popViewControllerAnimated:YES];
,告诉导航控制器,我要执行pop操作
3、手势退出和点击back退出的处理
我们可以仔细观察一下今日头条的Gif,不难发现他点击返回键退出,以及手势退出时,转场动画时不一样的。
- 点击返回键退出时:直接中间一个大的圆形头像,回到上个列表头像位置
- 手势退出时:整个页面下滑,背景透明度改变,松开时,再进入点击返回键退出时的动画效果
因为这里产生了两种动画执行的方式,我这里声明了一个属性,继续用户是点击退出,然后手势退出的
@property (nonatomic, assign) BOOL isInteracting;
那么在点击退出时,设置为NO,请他情况皆为YES,然后在对应的地方做处理即可
/// 若不是手势退出,直接返回nil则不会调用手势操作的相关方法
- (id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController
{
return self.isInteracting ? self.interactiveGes : nil;
}
同时,在转场动画也要做相应的处理,转场动画需要标记手势是否完成,然后再去做对应的动画
/// 关注的用户动态转场
@interface MPUserDynamicTransition : NSObject<UIViewControllerAnimatedTransitioning, CAAnimationDelegate>
/// 是否手势退出
@property (nonatomic, assign) BOOL isInteracting;
/// 是否手势完成
@property (nonatomic, assign) BOOL isComplete;
pop动画的核心动画代码
- (void)startPopAnimation: (nonnull id<UIViewControllerContextTransitioning>)transitionContext
{
UIView *contentView = [transitionContext containerView];
// 获取 fromView 和 toView
UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
UIView *whiteCoverView = [[UIView alloc] init];
whiteCoverView.backgroundColor = [UIColor blackColor];
whiteCoverView.frame = CGRectMake(0, 0, ZFPlayer_ScreenWidth, ZFPlayer_ScreenHeight);
whiteCoverView.alpha = 0;
[contentView addSubview:toView];
[contentView addSubview:whiteCoverView];
[contentView addSubview:fromView];
UIImageView *imageView = [[UIImageView alloc] init];
imageView.contentMode = UIViewContentModeScaleAspectFill;
imageView.clipsToBounds = YES;
imageView.image = self.startImage;
imageView.layer.cornerRadius = ZFPlayer_ScreenWidth * 0.5;
CGFloat top = (ZFPlayer_ScreenHeight - ZFPlayer_ScreenWidth) * 0.5;
CGRect winFrame = CGRectMake(0, top, ZFPlayer_ScreenWidth, ZFPlayer_ScreenWidth);
imageView.frame = winFrame;
imageView.hidden = YES;
[contentView addSubview:imageView];
CGFloat targetCorner = 0;
CGRect targetFrame = CGRectZero;
if (self.startView) {
targetFrame = [self.startView convertRect:self.startView.bounds toView:nil];
targetFrame = CGRectMake(self.endX, targetFrame.origin.y, targetFrame.size.width, targetFrame.size.height);
targetCorner = self.startView.bounds.size.width * 0.5;
}
dispatch_block_t block = dispatch_block_create(0, ^{
imageView.hidden = NO;
toView.alpha = 1.0f;
fromView.transform = CGAffineTransformIdentity;
fromView.alpha = 0.0f;
whiteCoverView.alpha = 0.4;
[UIView animateWithDuration:self.duration delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
whiteCoverView.alpha = 0;
imageView.frame = targetFrame;
imageView.layer.cornerRadius = targetCorner;
} completion:^(BOOL finished) {
[imageView removeFromSuperview];
[whiteCoverView removeFromSuperview];
// 结束动画
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
});
if (self.isInteracting) {
whiteCoverView.alpha = 1;
[UIView animateWithDuration:self.duration animations:^{
whiteCoverView.alpha = 0.4;
fromView.transform = CGAffineTransformScale(fromView.transform, 0.9, 0.9);
fromView.transform = CGAffineTransformTranslate(fromView.transform, 0, ZFPlayer_ScreenHeight * 0.5);
} completion:^(BOOL finished) {
if (self.isComplete) {
block();
}else {
[imageView removeFromSuperview];
[whiteCoverView removeFromSuperview];
// 结束动画
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}
}];
}else {
block();
}
}
注意self.isInteracting
和self.isComplete
这两个Bool控制显示的动画即可
4、完成的效果如下
手势退出转场演示.gif5、总结
这个Demo只是在演示如何用一个Transition,处理点击退出和手势退出时,执行不一样的转场效果。这里还需要完善的地方有
- 用户详情页做成头条的列表页面时,退出pan的手势和tableView的触发时机
- 侧滑处理,这个红色页面是不能侧滑退出的
关于转场动画的书写,可以看以下链接
https://blog.devtang.com/2016/03/13/iOS-transition-guide/