简介
现在越来越多的应用有看大图或者进入详情页面,但是,再返回的时候,普通意义上,会点击左上角的返回,这时候你就会发现,还需要将手指移动到左上角,这样,无意给用户增添了麻烦,并且,现在手机屏幕越来越大,这样返回的越来越困难,在体验上特别的差劲.
尽管苹果推出了从左边缘右滑返回,FDFullscreenPopGesture这个很强大的,全屏右滑返回…..尽管现在考虑到用户体验上,已经有了很大的提升,但是,仍然,在大屏上,不是很好操作…因为,你在正常使用手机的时候,大拇指使用的频率要远远大于其他手指,而且,大拇指,上下滑动的体验度是要大于左右滑动的体验度的…..所以,这里就有着下滑返回的需求必要的…
开始
解决完需求的原因,下面我们来看看如何做?
1.转场动画的设置
1. 遵循协议
需要转场的ViewCointroller遵循:UIViewControllerTransitioningDelegate
2. 转场代理设置
// 设置Presented的动画
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
return [[PresentVCAnimation alloc] init];
}
/// 设置Dismiss返回的动画设置
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
return [[DismissVCAnimation alloc] init];
}
/// 设置过场动画
- (id<UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id<UIViewControllerAnimatedTransitioning>)animator {
return (self.interactiveTransition.isInteracting ? self.interactiveTransition : nil);
}
2. PresentVCAnimation
PresentVCAnimation.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface PresentVCAnimation : NSObject<UIViewControllerAnimatedTransitioning>
@end
PresentVCAnimation.m
#import "PresentVCAnimation.h"
@implementation PresentVCAnimation
// 设置动画的时间长度
-(NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext {
return 0.25;
}
-(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
// 前一个ViewController,动画的发起者
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
// 后一个ViewController,动画的结束者
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
// 转场动画的最终的frame
CGRect finalFrameForVC = [transitionContext finalFrameForViewController:toVC];
// 下面敲黑板啦
// 转换的容器view,这里是存放转场动画的容器
UIView *containerView = [transitionContext containerView];
// 这里一般情况下,没有涉及到VC的View放大或者缩小,即可看做是屏幕的尺寸
CGRect bounds = [UIScreen mainScreen].bounds;
// 这是后一个ViewController的frame
toVC.view.frame = CGRectOffset(finalFrameForVC, 0, bounds.size.height);
[containerView addSubview:toVC.view];
// 下面是改变前一个ViewController和后一个ViewController的动画
[UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
fromVC.view.alpha = 0.5;
toVC.view.frame = finalFrameForVC;
} completion:^(BOOL finished) {
[transitionContext completeTransition:YES];
fromVC.view.alpha = 1.0;
}];
}
-(void)animationEnded:(BOOL)transitionCompleted {
}
3.DismissVCAnimation
DismissVCAnimation.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface DismissVCAnimation : NSObject<UIViewControllerAnimatedTransitioning>
@end
DismissVCAnimation.m
#import "DismissVCAnimation.h"
@implementation DismissVCAnimation
-(NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext {
return 0.25;
}
-(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
// 前一个ViewController,动画的发起者
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
// 后一个ViewController,动画的结束者
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
CGRect screenBounds = [UIScreen mainScreen].bounds;
// 获取前一个页面的frame
CGRect initFrame = [transitionContext initialFrameForViewController:fromVC];
// 转场动画的toView的最终的frame
CGRect finalFrame = CGRectOffset(initFrame, 0, screenBounds.size.height);
// 转换的容器view
UIView *containerView = [transitionContext containerView];
// 下面这里是为了让转场动画衔接的更和谐,不然,下滑一点距离就直接看到之前页面的内容,体验不好
UIView *bgView = [[UIView alloc] initWithFrame:fromVC.view.bounds];
bgView.backgroundColor = [UIColor blackColor];
[toVC.view addSubview:bgView ];
[containerView addSubview:toVC.view];
[containerView sendSubviewToBack:toVC.view];
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
fromVC.view.frame = finalFrame;
bgView.alpha = 0;
} completion:^(BOOL finished) {
[bgView removeFromSuperview];
BOOL complate = [transitionContext transitionWasCancelled];
[transitionContext completeTransition:(!complate)];
}];
}
-(void)animationEnded:(BOOL)transitionCompleted {
}
@end
4.SwipeUpInteractiveTransition
这个是针对滑动手势对转场动画的影响的类.
这里面主要处理了滑动行为的状态,需要重点了解的是:
1.判断用户是不是存在返回意图的行为,这里面规定了两种行为,而这两种行为,均为在当前页面手势结束的时候处理
返回意图的行为的判断条件:
- 当下滑距离大于当前屏幕比例的 0.382 (无耻的取了黄金比例的对半,因为,感觉有逼格,可进行相应调整)
- 快速滑动行为,也就是滑动速度 : iOS开发–手势滑动的速度
SwipeUpInteractiveTransition.h
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
@interface SwipeUpInteractiveTransition : UIPercentDrivenInteractiveTransition
/// 手势中...
@property (nonatomic, assign) BOOL isInteracting;
/// 完成动画
@property (nonatomic, assign) BOOL shouldComplete;
// 初始化
- (instancetype)initWithGestureViewController:(UIViewController *)gestureVC;
@end
SwipeUpInteractiveTransition.m
#define KEY_WINDOW [[UIApplication sharedApplication].delegate window]
#import "SwipeUpInteractiveTransition.h"
@interface SwipeUpInteractiveTransition()
// 手势添加的View
@property (nonatomic, strong) UIViewController *gestureVC;
// 记录手势结束前的点击位置
@property (nonatomic, assign) CGPoint oldTranslation;
// 是不是需要返回,这里是需要猜想并判断用户是不是存在返回行为
@property (nonatomic, assign) BOOL isNeedDismiss;
@end
@implementation SwipeUpInteractiveTransition
- (instancetype)initWithGestureViewController:(UIViewController *)gestureVC
{
self = [super init];
if (self) {
_gestureVC = gestureVC;
// 添加手势
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(panGestureHandler:)];
[_gestureVC.view addGestureRecognizer:pan];
}
return self;
}
- (void)panGestureHandler:(UIPanGestureRecognizer *)gesture {
// 获取手势触控的点在作用View的相对位置
CGPoint translation = [gesture translationInView:gesture.view];
// 每次手势触发的时候,重置,也就是用户不存在返回意图
self.isNeedDismiss = NO;
switch (gesture.state) {
case UIGestureRecognizerStateBegan: {
// 手势开始
// 交互动画判断
_isInteracting = YES;
[_gestureVC dismissViewControllerAnimated:YES completion:nil];
break;
}
case UIGestureRecognizerStateChanged: {
// 当前触控点的赋值
self.oldTranslation = translation;
// 触控点的转化,因为updateInteractiveTransition的参数范围是[0.1],所以这里需要左边比例的转换
CGFloat fraction = (translation.y / KEY_WINDOW.bounds.size.height);
// 保证范围
fraction = fmin(fmaxf(fraction, 0.0), 1.0);
// 这里进行滑动中的判断,取的是黄金比例,也就是,如果滑动距离占比约38%,即可判断用户存在返回的行为
_shouldComplete = fraction > 0.382;
// 更新进度
[self updateInteractiveTransition:fraction];
break;
}
case UIGestureRecognizerStateEnded: {
// 这里是重要的判断
// 如果用户存在快速向下滑动的行为(等同于全屏快速向右滑动返回的行为),self.isNeedDismiss为YES
// 而这个判断的快速范围如下
CGPoint speed = [gesture velocityInView:gesture.view];
// 这个数据经过了大量测试和舒适度的数据,大家可以参考,具体的还需要根据实际情况而定.可以看我的量
if (speed.y >= 920) {
self.isNeedDismiss = YES;
_shouldComplete = YES;
}
// 手势交互结束
_isInteracting = NO;
// 下面进行之前判断的处理
// 使用dispatch_source_t定时回调,进行改变self.oldTranslation的数据,进行模拟手势移动,因为如果直接调用[self cancelInteractiveTransition];或者[self updateInteractiveTransition:fraction];就会发现,动画瞬间变化,瞬间复位或者瞬间消失返回.考虑到用户体验,如下
dispatch_queue_t quene = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//定时器模式 事件源
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, quene);
//NSEC_PER_SEC是秒,*1是每秒
dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), NSEC_PER_SEC * 0.0001, 0);
//设置响应dispatch源事件的block,在dispatch源指定的队列上运行
dispatch_source_set_event_handler(timer, ^{
//回调主线程,在主线程中操作UI
dispatch_async(dispatch_get_main_queue(), ^{
if (!_isNeedDismiss && (!_shouldComplete || gesture.state == UIGestureRecognizerStateCancelled)) {
if (self.oldTranslation.y <= 0) {
// 当满足条件,执行取消动画
[self cancelInteractiveTransition];
// 移除时间回调
dispatch_source_cancel(timer);
} else {
// 模拟上滑行为
CGFloat fraction = (self.oldTranslation.y / KEY_WINDOW.bounds.size.height);
fraction = fmin(fmaxf(fraction, 0.0), 1.0);
self.oldTranslation = CGPointMake(self.oldTranslation.x, self.oldTranslation.y - 0.3);
[self updateInteractiveTransition:fraction];
}
} else {
if (self.oldTranslation.y > KEY_WINDOW.bounds.size.height) {
// 当滑出当前屏幕,执行完成动画
[self finishInteractiveTransition];
// 移除时间回调
dispatch_source_cancel(timer);
} else {
// 模拟下滑行为
CGFloat fraction = (self.oldTranslation.y / KEY_WINDOW.bounds.size.height);
fraction = fmin(fmaxf(fraction, 0.0), 1.0);
self.oldTranslation = CGPointMake(self.oldTranslation.x, self.oldTranslation.y + 0.3);
[self updateInteractiveTransition:fraction];
}
}
});
});
//启动源
dispatch_resume(timer);
break;
}
default:
break;
}
}
@end