寫在前面
在簡書寫完第一篇的自定義轉場文章後,已經很久沒有碰過轉場了,畢竟在公司,功能實現才是最重要的,這些轉場的動效,只能是點睛之筆,不太容易被重視,不過我的第一篇文章還是很多人的喜歡和討論,很多人還提出些建議,非常感謝大家,這是我第一篇文章的地址自定義轉場動畫,裡面包含了一些轉場的基礎知識,這篇文章我就不再討論這些基礎知識了。
為什麼會有這第二篇文章,主要原因有如下幾點:
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、選擇你需要的效果器進行根據初始化方法進行初始化,比如下面的小圓點擴散,初始化指定開始圓心和半徑
1 |
XWCircleSpreadAnimator *animator = [XWCircleSpreadAnimator xw_animatorWithStartCenter:self.button.center radius:20]; |
4、通過初始化的效果器轉場,根據分類提供的方法進行push或者present,就完成了!
1 2 3 |
[self.navigationController xw_pushViewController:toVC withAnimator:animator]; 或者 [self xw_presentViewController:toVC withAnimator:animator]; |
手勢驅動
1、在UIViewController+XWTransition.h
分類中提供了兩個方法,用來註冊手勢驅動,在viewDidLoad的時候呼叫註冊手勢就可以了,詳見demo,注意避免迴圈引用,手勢支援邊緣屬性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
/** * 註冊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、事例程式碼
1 2 3 4 5 6 |
__weak typeof(self)weakSelf = self; //註冊一個全屏的back轉場 [self xw_registerBackInteractiveTransitionWithDirection:XWInteractiveTransitionGestureDirectionDown transitonBlock:^{ //pop或者dismiss操作 [weakSelf xw_transiton]; } edgeSpacing:0]; |
關於神奇移動效果
1、在UIViewController+XWTransition.h
分類中提供了三個關於神奇移動的方法,你需要在轉場前和轉場後的控制器中分別註冊神奇移動前後的檢視(用來告知神奇移動前後的frame),然後通過神奇移動效果器就可以觸發神奇移動轉場了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
/** * 註冊神奇移動起始檢視 * * @param group 神奇移動起始檢視陣列 */ - (void)xw_addMagicMoveStartViewGroup:(NSArray *)group; /** * 註冊神奇移動終止檢視 * * @param group 神奇移動終止檢視陣列,注意起始檢視陣列和終止檢視陣列的檢視需要一一對應才能有正確的效果 */ - (void)xw_addMagicMoveEndViewGroup:(NSArray *)group; /** * 改變神奇移動起始檢視,因為在back的時候,有可能不需要再回到原來起始的位置,需要去一個新的檢視位置,所以在back前需要呼叫該方法改變起始檢視陣列 * * @param group 新的起始檢視陣列 */ - (void)xw_changeMagicMoveStartViewGroup:(NSArray *)group; |
2、事例程式碼
1 2 3 4 5 6 7 |
//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,而不截圖,就能解決模糊的問題,程式碼如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
- (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沒有覆蓋的區域新增一個透明檢視,給透明檢視加上點選和拖動手勢,具體程式碼如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
//首先需要設定點選和拖動的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 *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,來達到動畫的效果,具體程式碼如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
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、事例程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
//手勢轉場時的代理事件,animator預設為為其手勢的代理,複寫對應的代理事件可處理一些手勢失敗閃爍的情況 @protocol XWInteractiveTransitionDelegate @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、直接通過列舉初始化就有已經整合的部分效果,具體如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
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產生的一些濾鏡效果,貌似在模擬器無法執行這些效果,請在真機上測試,直接通過列舉初始化就有已經整合的部分效果,具體如下:
1 2 3 4 5 6 7 8 9 10 11 |
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
標頭檔案如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
@interface XWTransitionAnimator : NSObject //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)transitionContext; /** * 配置back過程動畫(pop, dismiss),自定義轉場動畫應該複寫該方法 */ - (void)xw_setBackAnimation:(id)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中新增了代理檢測和切換的方法來達到目的