写在前面
在简书写完第一篇的自定义转场文章后,已经很久没有碰过转场了,毕竟在公司,功能实现才是最重要的,这些转场的动效,只能是点睛之笔,不太容易被重视,不过我的第一篇文章还是很多人的喜欢和讨论,很多人还提出些建议,非常感谢大家,这是我第一篇文章的地址自定义转场动画,里面包含了一些转场的基础知识,这篇文章我就不再讨论这些基础知识了。
为什么会有这第二篇文章,主要原因有如下几点:
1、能不能更简单?当我很久没有使用转场的时候,再次来使用它,感觉还是比较烦琐,有一大堆记不住的长长的代理方法,都要去copy,长长的代理方法也把控制器弄得有点乱,虽然苹果已经将整个过程充分解耦了,我在想,要是能简单的一两句话就能集成转场效果多好,或者通过继承和复写一两个方法就能轻松实现自己的转场效果,无需关注转场逻辑,只需关注动画逻辑
2、闪烁和生硬?在第一篇文章中有人提到的部分的bug,比如小圆点扩散效果,如果手势在中途取消,不会有取消动画,非常生硬,而且会有闪烁的bug,我在想能不能解决这两个问题,强迫症接受不了o(╯□╰)o,我现在找到了一个比较好的方式来解决问题,原理和对比图会在后面给出
3、能不能多添加一些效果?所以我把自己写的效果封装,再参照网络一些效果,总过添加了将近20个效果
4、手势万岁!任何效果我都想能够手势驱动
效果图(图比较多,请手机用户慎重,可下载demo真机运行效果更好)
截图中,右上角的switch开关代表push和present,所有效果都支持手势,我就不一一演示了
1、CircleSpreadTransition 小圆点扩散
2、MagicMoveTransition 神奇移动
3、XWDrawerAnimator 抽屉效果,仿照QQ和淘宝
4、XWCoolAnimator 自定义一些效果
5、XWFilterAnimator 通过CIFilter滤镜自定义一些效果,请在真机上运行
如何使用
1、git地址:几句代码快速集成自定义转场效果+ 全手势驱动,clone后将整个XWTranstion文件夹导入工程
2、导入UINavigationController+XWTransition.h
或者UIViewController+XWTransition.h
两个分类
3、选择你需要的效果器进行根据初始化方法进行初始化,比如下面的小圆点扩散,初始化指定开始圆心和半径
XWCircleSpreadAnimator *animator = [XWCircleSpreadAnimator xw_animatorWithStartCenter:self.button.center radius:20];
4、通过初始化的效果器转场,根据分类提供的方法进行push或者present,就完成了!
[self.navigationController xw_pushViewController:toVC withAnimator:animator];
或者
[self xw_presentViewController:toVC withAnimator:animator];
手势驱动
1、在UIViewController+XWTransition.h
分类中提供了两个方法,用来注册手势驱动,在viewDidLoad的时候调用注册手势就可以了,详见demo,注意避免循环引用,手势支持边缘属性
/**
* 注册to手势(push或者Present手势)
*
* @param direction 手势方向
* @param tansitionConfig 手势触发的block,block中需要包含你的push或者Present的逻辑代码,注意避免循环引用问题
* @param edgeSpacing 手势触发的边缘距离,该值为0,表示在整个控制器视图上都有效,否者这在边缘的edgeSpacing之类有效
*/
- (void)xw_registerToInteractiveTransitionWithDirection:(XWInteractiveTransitionGestureDirection)direction transitonBlock:(dispatch_block_t)tansitionConfig edgeSpacing:(CGFloat)edgeSpacing;
/**
* 注册back手势(pop或者dismiss手势)
*
* @param direction 手势方向
* @param tansitionConfig 手势触发的block,block中需要包含你的pop或者dismiss的逻辑代码,注意避免循环引用问题
* @param edgeSpacing 手势触发的边缘距离,该值为0,表示在整个控制器视图上都有效,否者这在边缘的edgeSpacing之类有效
*/
- (void)xw_registerBackInteractiveTransitionWithDirection:(XWInteractiveTransitionGestureDirection)direction transitonBlock:(dispatch_block_t)tansitionConfig edgeSpacing:(CGFloat)edgeSpacing;
2、事例代码
__weak typeof(self)weakSelf = self;
//注册一个全屏的back转场
[self xw_registerBackInteractiveTransitionWithDirection:XWInteractiveTransitionGestureDirectionDown transitonBlock:^{
//pop或者dismiss操作
[weakSelf xw_transiton];
} edgeSpacing:0];
关于神奇移动效果
1、在UIViewController+XWTransition.h
分类中提供了三个关于神奇移动的方法,你需要在转场前和转场后的控制器中分别注册神奇移动前后的视图(用来告知神奇移动前后的frame),然后通过神奇移动效果器就可以触发神奇移动转场了
/**
* 注册神奇移动起始视图
*
* @param group 神奇移动起始视图数组
*/
- (void)xw_addMagicMoveStartViewGroup:(NSArray<UIView *> *)group;
/**
* 注册神奇移动终止视图
*
* @param group 神奇移动终止视图数组,注意起始视图数组和终止视图数组的视图需要一一对应才能有正确的效果
*/
- (void)xw_addMagicMoveEndViewGroup:(NSArray<UIView *> *)group;
/**
* 改变神奇移动起始视图,因为在back的时候,有可能不需要再回到原来起始的位置,需要去一个新的视图位置,所以在back前需要调用该方法改变起始视图数组
*
* @param group 新的起始视图数组
*/
- (void)xw_changeMagicMoveStartViewGroup:(NSArray<UIView *> *)group;
2、事例代码
//fromVC转场前控制器中注册神奇移动前视图
[self xw_addMagicMoveStartViewGroup:@[imgView, view1, view2]];
//toVC转场后控制器中注册神奇移动前视图
[self xw_addMagicMoveEndViewGroup:@[imgView, view1, view2]];
//初始化神奇移动效果器转场
XWMagicMoveToController *toVC = [XWMagicMoveToController new];
[self xw_presentViewController:toVC withAnimator:animator];
3、转场中存在cell,由于在转场过程中cell还没有加载,所以无法注册cell为神奇移动视图,这种情况需要生产一个零时视图注册为转场视图来使用,具体请参考demo中的九宫格例子
4、关于提供的imageMode
属性:在神奇移动中,有个问题,就是移动中的临时视图一般都是用截图大法截图而来的,但是如果从从小图变成大图,由于截图为小图截图,变大过程中会有模糊的现象,如果设置了该属性,我会对神奇移动视图中的包含了image的视图进行检测,如果能检测到image则直接取image,而不截图,就能解决模糊的问题,代码如下
- (UIView *)_xw_snapshotView:(UIView *)view{
CALayer *layer = view.layer;
UIView *snapView = [UIView new];
snapView.frame = view.frame;
BOOL imgMode = [objc_getAssociatedObject(view, &kXWMagicMovePropertyInViewKey) boolValue] || _imageMode;
UIImage *img = nil;
if (imgMode) {//如果开启imgMode,优先直接获取图片,避免截图时时从小到大造成的模糊
if ([view isKindOfClass:[UIImageView class]]) {//取imageView中的image
img = [(UIImageView *)view image];
}else if ([view isKindOfClass:[UIButton class]]){//取button中的image
img = [(UIButton *)view currentImage];
}
if (!img && [view isKindOfClass:[UIView class]]) {//没取到尝试取content
img = [UIImage imageWithCGImage:(__bridge CGImageRef)view.layer.contents];
}
}
//若都没有取到,则截图
if (!img) {
UIGraphicsBeginImageContextWithOptions(layer.bounds.size, layer.opaque, 0);
CGContextRef context = UIGraphicsGetCurrentContext();
[layer renderInContext:context];
img = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
snapView.layer.contents = (__bridge id)img.CGImage;
return snapView;
}
关于抽屉效果的全屏拖动
1、抽屉效果由于注册的手势都是在控制器的的视图上,如果做QQ设置界面的效果,不可能在toVC之外点击和拖动能够back,我的思路是会在toVC没有覆盖的区域添加一个透明视图,给透明视图加上点击和拖动手势,具体代码如下
//首先需要设置点击和拖动的back操作,block中应该包含你的dismiss或者pop逻辑
/**
* 开启边缘(就是屏幕除开toView所占用的部分)back手势和边缘点击返回效果,类似于QQ设置界面的返回效果
*
* @param backConfig 返回操作,您的dismiss或者pop操作
*/
- (void)xw_enableEdgeGestureAndBackTapWithConfig:(dispatch_block_t)backConfig;
//添加全屏手势代码如下
/**
* 添加全局手势和点击视图
*/
- (void)_xw_addFullGestureAndTapBackViewInContainerView:(UIView *)containerView toView:(UIView *)toView distance:(CGFloat)distance{
CGFloat width = _vertical ? containerView.frame.size.width : containerView.frame.size.width - fabs(distance);
CGFloat height = _vertical ? containerView.frame.size.height - fabs(distance) : containerView.frame.size.height;
//如果toVC是全屏铺满则无需添加全局手势,直接使用toVC的view的手势就好了
if (width == 0 || height == 0)return;
if (!_backConfig) return;
//如果toView注册过手势,我们直接获取这个手势
NSArray<UIGestureRecognizer *> *gestures = toView.gestureRecognizers;
__block id target = nil;
[gestures enumerateObjectsUsingBlock:^(UIGestureRecognizer * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSString *panType = objc_getAssociatedObject(obj, "xw_interactivePanKey");
if ([panType isEqualToString:@"xw_interactiveBackPan"] && obj.delegate) {
target = obj.delegate;
*stop = YES;
}
}];
CGFloat x = _vertical || _direction == XWDrawerAnimatorDirectionRight ? 0 : -distance;
CGFloat y = !_vertical || _direction == XWDrawerAnimatorDirectionBottom ? 0 : -distance;
UIControl *gestureView = [UIControl new];
//添加点击事件
[gestureView addTarget:self action:@selector(_xw_backConfig) forControlEvents:UIControlEventTouchUpInside];
gestureView.frame = CGRectMake(x, y, width, height);
gestureView.backgroundColor = [UIColor clearColor];
//第一种情况,toView已经添加了返回手势,我们直接拿到该手势的target和action
if (target) {
//给containerView添加全局手势
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:target action:NSSelectorFromString(@"_xw_handleGesture:")];
[containerView addGestureRecognizer:pan];
}else{
//第二种情况,toView没有添加手势,我们需要创建一个
__weak typeof(self)weakSelf = self;
XWInteractiveTransition *backTransition = [XWInteractiveTransition xw_interactiveTransitionWithDirection:(XWInteractiveTransitionGestureDirection)_direction config:^{
weakSelf.backConfig();
} edgeSpacing:0];
backTransition.panRatioBaseValue = _vertical ? containerView.frame.size.height : containerView.frame.size.width;
[backTransition xw_addPanGestureForView:gestureView to:NO];
// [self xw_setBackInteractiveTransition:backTransition];
[self setValue:backTransition forKey:@"backTransition"];
}
[containerView addSubview:gestureView];
}
解决动画生硬
1、先看小圆点效果的例子,前面是解决前写的,后面是现在的
未解决
解决后
2、问题原因:在手势结束后该效果不会动画的过渡到成功或者失败,而是整个转场进度会直接update到0或者1,就木有动画了
3、解决:在手指松开的时候,我会开启一个CADisplayLink来不断的刷新整个转场进度到1或者0,来达到动画的效果,具体代码如下
case UIGestureRecognizerStateEnded:{//转场结束后
//判断是否需要timer
if (!_timerEable) {
_percent >= 0.5 ? [self _xw_finish] : [self _xw_cancle];
return;
}
//判断此时是否已经转场完成,大于1或者小于0
BOOL canEnd = [self _xw_canEndInteractiveTransitionWithPercent:_percent];
if (canEnd) return;
//开启timer
[self _xw_setEndAnimationTimerWithPercent:_percent];
//设置开启timer
- (void)_xw_setEndAnimationTimerWithPercent:(CGFloat)percent{
_percent = percent;
//根据失败还是成功设置刷新间隔
if (percent > 0.5) {
_timeDis = (1 - percent) / ((1 - percent) * 60);
}else{
_timeDis = percent / (percent * 60);
}
//开启timer
[self _xw_startTimer];
}
//开启timer
- (void)_xw_startTimer{
if (_timer) {
return;
}
_timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(_xw_timerEvent)];
[_timer addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}
//timer 事件
- (void)_xw_timerEvent{
if (_percent > 0.5) {
_percent += _timeDis;
}else{
_percent -= _timeDis;
}
//通过timer不断刷新转场进度,达到动画效果
[self _xw_updatingWithPercent:_percent];
//判断进度是否达到0和1,达到则结束timer,结束转场
BOOL canEnd = [self _xw_canEndInteractiveTransitionWithPercent:_percent];
if (canEnd) {
[self _xw_stopTimer];
}
}
解决闪烁问题
1、闪烁原因:在不使用UIView的动画block时,我们直接为layer添加一个CAAnimtion,此时会先设置modelLayer为转场成功的状态,比如小圆点效果会设置path为大圆的path,但是如果转场失败,presentLayer依然会先变为modelLayer设置的成功值,然后动画才结束,走我们的转场失败逻辑,所以就会闪烁
2、解决:我把手势改变的一些关键状态通过代理传出来,在手势结束前,我们如果检查到失败,可以先将modelLayer的值标记为失败时候的值,也就是初始值,就解决了该问题
3、事例代码
//手势转场时的代理事件,animator默认为为其手势的代理,复写对应的代理事件可处理一些手势失败闪烁的情况
@protocol XWInteractiveTransitionDelegate <NSObject>
@optional
/**手势转场即将开始时调用*/
- (void)xw_interactiveTransitionWillBegin:(XWInteractiveTransition *)interactiveTransition;
/**手势转场中调用*/
- (void)xw_interactiveTransition:(XWInteractiveTransition *)interactiveTransition isUpdating:(CGFloat)percent;
/**如果开始了转场手势timer,会在松开手指,timer开始的时候调用*/
- (void)xw_interactiveTransitionWillBeginTimerAnimation:(XWInteractiveTransition *)interactiveTransition;
/**手势转场结束的时候调用*/
- (void)xw_interactiveTransition:(XWInteractiveTransition *)interactiveTransition willEndWithSuccessFlag:(BOOL)flag percent:(CGFloat)percent;
@end
//我在小圆点扩散效果中处理的如下
- (void)xw_interactiveTransition:(XWInteractiveTransition *)interactiveTransition willEndWithSuccessFlag:(BOOL)flag percent:(CGFloat)percent{
if (!flag) {
//防止失败后的闪烁,如果失败将遮罩的path设置为其实的小圆path
_maskLayer.path = _startPath.CGPath;
}
_containerView.userInteractionEnabled = YES;
}
关于coolTransiton
1、直接通过枚举初始化就有已经集成的部分效果,具体如下:
typedef NS_ENUM(NSUInteger, XWCoolTransitionAnimatorType){
//全屏翻页
XWCoolTransitionAnimatorTypePageFlip,
//中间翻页
XWCoolTransitionAnimatorTypePageMiddleFlipFromLeft,
XWCoolTransitionAnimatorTypePageMiddleFlipFromRight,
XWCoolTransitionAnimatorTypePageMiddleFlipFromTop,
XWCoolTransitionAnimatorTypePageMiddleFlipFromBottom,
//开窗
XWCoolTransitionAnimatorTypePortal,
//折叠
XWCoolTransitionAnimatorTypeFoldFromLeft,
XWCoolTransitionAnimatorTypeFoldFromRight,
//爆炸
XWCoolTransitionAnimatorTypeExplode,
//酷炫线条效果
XWCoolTransitionAnimatorTypeHorizontalLines,
XWCoolTransitionAnimatorTypeVerticalLines,
//扫描效果
XWCoolTransitionAnimatorTypeScanningFromLeft,
XWCoolTransitionAnimatorTypeScanningFromRight,
XWCoolTransitionAnimatorTypeScanningFromTop,
XWCoolTransitionAnimatorTypeScanningFromBottom,
};
2、 cool转场效果中的Portal、Fold、Explode效果的部分代码逻辑来源于ColinEberhardt/VCTransitionsLibrary,非常感谢作者,我只是将其进行了部分改动,以便对手势的支持更加完善,里面还有许多其他效果,本人经历有限就没有再集成进来了,大家可以自行查看;cool转场效果的Lines的想法来自于cinkster/HUAnimator, 非常感谢作者,但是由于作者在对toVC截图采用了延迟的方式来处理,导致了不好处理的bug和一些手势上的bug,对此我采用了另一种方式来解决截图的问题,使用了layer的contentRect属性,解决了发现的问题,相关代码请自行查看
关于FilterTransition
1、XWFilterAnimator 全都是基于不同的CIFilter产生的一些滤镜效果,貌似在模拟器无法运行这些效果,请在真机上测试,直接通过枚举初始化就有已经集成的部分效果,具体如下:
typedef NS_ENUM(NSUInteger, XWFilterAnimatorType) {
XWFilterAnimatorTypeBoxBlur,//模糊转场,对应CIBoxBlur
XWFilterAnimatorTypeSwipe,//滑动过渡转场,对应CISwipeTranstion
XWFilterAnimatorTypeBarSwipe,//对应CIBarSwipeTranstion
XWFilterAnimatorTypeMask,//按指定遮罩图片转场,对应CIDisintegrateWithMaskTransition
XWFilterAnimatorTypeFlash,//闪烁转场,对应CIFlashTransition
XWFilterAnimatorTypeMod,//条纹转场 对应CIModTransition
XWFilterAnimatorTypePageCurl,//翻页转场 对应CIPageCurlWithShadowTransition
XWFilterAnimatorTypeRipple,//波纹转场,对应CIRippleTransition
XWFilterAnimatorTypeCopyMachine, //效果和XWCoolAnimator中的Scanning效果类似,对应CICopyMachineTransition
};
2、如果想要添加其他滤镜转场,可以尝试我的FilterTransition中书写分类的方式,只需要指定CIFilter和相关逻辑即可
关于自定义转场效果
1、你只需要继承于XWTransitionAnimator
,就像我上面所有的效果器一样,然后复写需要的属性和两个必须的方法即可,然后你就可以使用你自定义的效果器转场,XWTransitionAnimator
头文件如下:
@interface XWTransitionAnimator : NSObject<UIViewControllerTransitioningDelegate, UINavigationControllerDelegate, UITabBarControllerDelegate, XWInteractiveTransitionDelegate>
//to转场时间 默认0.5
@property (nonatomic, assign) NSTimeInterval toDuration;
//back转场时间 默认0.5
@property (nonatomic, assign) NSTimeInterval backDuration;
//是否需要开启手势timer,某些转场如果在转成过程中所开手指,不会有动画过渡,显得很生硬,开启timer后,松开手指,会用timer不断的刷新转场百分比,消除生硬的缺点
@property (nonatomic, assign) BOOL needInteractiveTimer;
/**
* 配置To过程动画(push, present),自定义转场动画应该复写该方法
*/
- (void)xw_setToAnimation:(id<UIViewControllerContextTransitioning>)transitionContext;
/**
* 配置back过程动画(pop, dismiss),自定义转场动画应该复写该方法
*/
- (void)xw_setBackAnimation:(id<UIViewControllerContextTransitioning>)transitionContext;
@end
2、这样就只需要关心动画的逻辑,其余的事情就不用管了,不过如果遇到闪烁问题,你只需要复写相关的手势代理方法,就像我在小圆点转场中一样,因为XWTransitionAnimator
默认是手势管理者的代理,所以直接实现代理方法就好了
写在最后
陆陆续续的就这些了,东西比较多,可能我的叙述也还有一定问题,某些内容可能描述的不太清楚,请大家多多参考demo,希望本文能让大家以后再设计到自定义转场的时候能够迅速解决问题,再次复习一下地址几句代码快速集成自定义转场效果+ 全手势驱动 ,如果对您有帮助欢迎给予star支持!
更新 2016-06-24
今天早上思考了一下,优化了一下DrawerAnimator,之前的toVC的frame不会随着设置的distance改变,默认一般都是屏幕的宽和高,也就是说显示之后,toVC的有一部分实际是在屏幕外面的,这对于后续的布局是不太方便的,所以我修改了一下,现在toVC的frame是和设置的distance相关的,所看见的toVC的部分就是toVC的全部
更新 2016-07-05
1、今天发现了一个问题,就是在进行不同的效果多次push的时候,在pop的时候,之前的效果会失效,我修复了这个问题,请看截图,上面是修复前,下面是修复后
可以看见,修复前,在最后一次back的时候,那个爆炸的效果已经失效了,
2、问题原因:在每次push时我会切换navigationController的delegate为当前效果器,从而能完成转场效果的逻辑,所以多次push后,代理始终是最后一个效果器,而在pop的时候那个效果器随着对应的pop操作已经被销毁了,而代理并没有切换为之前的爆炸效果器,所以自定义转场就无法触发了
3、解决:由于我每一个效果器是和被push出的VC绑定的,所以当被pushVC被销毁的时候,效果器就会销毁,此刻,应该去检测一下代理,如果上一个VC存在效果器,则需要切换回该效果器,所以需要在pushVC的dealloc方法中需要对代理进行检测和切换,为了达到目的,需要对VC的dealloc方法进行调剂,调剂的方法稍微有点复杂,具体请看我另一篇简书文章:一句代码,更加优雅的调用KVO和通知中关于调剂dealloc方法的相关代码,在dealloc中添加了代理检测和切换的方法来达到目的