前言
蘋果在IOS7以后給導航控制器增加了一個Pop的手勢留凭,只要手指在屏幕邊緣滑動辆毡,當前的控制器的視圖就會跟隨你的手指移動缝驳,當用戶松手后嘹害,系統(tǒng)會判斷手指拖動出來的大小來決定是否要執(zhí)行控制器的Pop操作豪墅。
這個操作的想法非常好到逊,但是系統(tǒng)給我們規(guī)定的范圍必須是屏幕左側(cè)邊緣才可以觸發(fā)铣口,這樣實際使用過程中對于有些產(chǎn)品會產(chǎn)生不便,于是有些app就采取整個屏幕都響應這個手勢并且pop動畫還是用系統(tǒng)原生的觉壶,這樣操作起來確實方便好多脑题。
開始大家一定會有疑問,給控制器的View加個手勢然后拖動控制器的View時改變它的frame不就可以了嗎铜靶?沒錯叔遂,加手勢這個想法是正確的。但是争剿,由我們自己來改變控制器視圖的位置是比較麻煩的已艰,細心的朋友一定發(fā)現(xiàn)了,我們自定義pop手勢上面的導航欄也是在隨著你的手勢拖拽而變動的蚕苇,所以這樣做還需要負責導航欄的動畫哩掺,而且有一個重點問題,如果單獨拖動view涩笤,這個view下面會是黑黑的一片嚼吞,因為控制器的push和pop層級是由系統(tǒng)管理的
所以走這條路雖然可以,但實現(xiàn)起來會比較艱辛辆它。那么誊薄,如何實現(xiàn)這個效果呢?今天就給大家提供兩套實現(xiàn)方案锰茉。
方案一:自定義UIViewControllerInteractiveTransitioning對象呢蔫,實現(xiàn)導航控制器代理方法。
這套方案雖然實現(xiàn)比較麻煩,但是動畫相對靈活片吊,你可以實現(xiàn)這樣的效果
其實這個拖動過程屬于導航控制器的動畫绽昏,所以我們需要重寫UINavigationController的兩個代理方法
navigationController:animationControllerForOperation:fromViewController:toViewController:(名字很長下面就稱為方法1)和
navigationController:interactionControllerForAnimationController:(方法2)。
解釋一下他們的作用
方法1是蘋果提供給我們用來重寫控制器之間轉(zhuǎn)場動畫的(pop或者push)俏脊。
方法2你可以這樣理解全谤,蘋果讓我們返回一個交互的對象,用來實時管理控制器之間轉(zhuǎn)場動畫的完成度爷贫,通過它我們可以讓控制器的轉(zhuǎn)場動畫與用戶交互(注意一點认然,如果方法1返回是nil,方法2是不會調(diào)用的漫萄,也就是說卷员,只有我們自定義的動畫才可以與控制器交互)。
下面我們來看一下實現(xiàn)過程腾务。為了便于大家理解毕骡,我會盡量在Demo中的注釋寫的最清晰明了。
同時岩瘦,我們先用最簡單的代碼實現(xiàn)未巫,在這篇文章的最后我會對本例中的Demo提供一個相對合理的寫法。
首先在方法1中启昧,我們返回一個遵守了UIViewControllerAnimatedTransitioning協(xié)議的對象叙凡,它就是自定義的動畫對象,我們給它起名PopAnimation箫津,在這個類中實現(xiàn)兩個方法來自定義轉(zhuǎn)場動畫狭姨。
方法1:
// 方法1是蘋果公司提供給我們重寫push 和 pop 動畫的方法
- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC {
/**
* 方法1中判斷如果當前執(zhí)行的是Pop操作,就返回我們自定義的Pop動畫對象苏遥。
*/
if (operation == UINavigationControllerOperationPop)
return [[PopAnimation alloc] init];
return nil;
}
自定義轉(zhuǎn)場動畫PopAnimation
@interface PopAnimation ()<UIViewControllerAnimatedTransitioning>
@property (nonatomic, strong) id <UIViewControllerContextTransitioning> transitionContext;
@end
@implementation PopAnimation
- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext {
//這個方法返回動畫執(zhí)行的時間
return 0.25;
}
/**
* transitionContext你可以看作是一個工具饼拍,用來獲取一系列動畫執(zhí)行相關的對象,并且通知系統(tǒng)動畫是否完成等功能田炭。
*/
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
/**
* 獲取動畫來自的那個控制器
*/
UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
/**
* 獲取轉(zhuǎn)場到的那個控制器
*/
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
/**
* 轉(zhuǎn)場動畫是兩個控制器視圖時間的動畫师抄,需要一個containerView來作為一個“舞臺”,讓動畫執(zhí)行教硫。
*/
UIView *containerView = [transitionContext containerView];
[containerView insertSubview:toViewController.view belowSubview:fromViewController.view];
NSTimeInterval duration = [self transitionDuration:transitionContext];
/**
* 執(zhí)行動畫叨吮,我們讓fromVC的視圖移動到屏幕最右側(cè)
*/
[UIView animateWithDuration:duration animations:^{
fromViewController.view.transform = CGAffineTransformMakeTranslation([UIScreen mainScreen].bounds.size.width, 0);
}completion:^(BOOL finished) {
/**
* 當你的動畫執(zhí)行完成,這個方法必須要調(diào)用瞬矩,否則系統(tǒng)會認為你的其余任何操作都在動畫執(zhí)行過程中茶鉴。
*/
[transitionContext completeTransition:!transitionContext.transitionWasCancelled];
}];
}
- (void)animationDidStop:(CATransition *)anim finished:(BOOL)flag {
[_transitionContext completeTransition:!_transitionContext.transitionWasCancelled];
}
@end
再來看方法2,我們需要返回一個遵守了UIViewControllerInteractiveTransitioning協(xié)議的對象(提示一下景用,這兩個協(xié)議容易混淆涵叮,要注意區(qū)分,一個是負責動畫,一個是負責交互過程)割粮,蘋果已經(jīng)有一個類專門處理這個功能盾碗,它叫UIPercentDrivenInteractiveTransition,當然你也可以自定義一個這樣的類舀瓢。我們可以這樣理解它的作用:
前面在方法1中返回的動畫廷雅,會在執(zhí)行的過程中被系統(tǒng)分解以用于用戶交互,這個交互過程的動畫完成度就由它來調(diào)控京髓。下面我們來看一下如何使用它航缀。(為了讓控制器視圖拖動,我們給控制器的視圖加了一個拖動手勢朵锣,在拖動方法里我們對這個對象進行操作)
/**
* 我們把用戶的每次Pan手勢操作作為一次pop動畫的執(zhí)行
*/
- (void)handleControllerPop:(UIPanGestureRecognizer *)recognizer {
/**
* interactivePopTransition就是我們說的方法2返回的對象谬盐,我們需要更新它的進度來控制Pop動畫的流程,我們用手指在視圖中的位置與視圖寬度比例作為它的進度诚些。
*/
CGFloat progress = [recognizer translationInView:recognizer.view].x / recognizer.view.bounds.size.width;
/**
* 穩(wěn)定進度區(qū)間,讓它在0.0(未完成)~1.0(已完成)之間
*/
progress = MIN(1.0, MAX(0.0, progress));
if (recognizer.state == UIGestureRecognizerStateBegan) {
/**
* 手勢開始皇型,新建一個監(jiān)控對象
*/
self.interactivePopTransition = [[UIPercentDrivenInteractiveTransition alloc] init];
/**
* 告訴控制器開始執(zhí)行pop的動畫
*/
[self.vc popViewControllerAnimated:YES];
}
else if (recognizer.state == UIGestureRecognizerStateChanged) {
/**
* 更新手勢的完成進度
*/
[self.interactivePopTransition updateInteractiveTransition:progress];
}
else if (recognizer.state == UIGestureRecognizerStateEnded || recognizer.state == UIGestureRecognizerStateCancelled) {
/**
* 手勢結(jié)束時如果進度大于一半诬烹,那么就完成pop操作,否則重新來過弃鸦。
*/
if (progress > 0.5) {
[self.interactivePopTransition finishInteractiveTransition];
}
else {
[self.interactivePopTransition cancelInteractiveTransition];
}
self.interactivePopTransition = nil;
}
}
最后在視圖控制器里重寫導航欄的兩個方法绞吁。
// 方法1是蘋果公司提供給我們重寫push 和 pop 動畫的方法
- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC {
/**
* 方法1中判斷如果當前執(zhí)行的是Pop操作,就返回我們自定義的Pop動畫對象唬格。
*/
if (operation == UINavigationControllerOperationPop)
return [[PopAnimation alloc] init];
return nil;
}
// 方法2是你可以這樣理解家破,蘋果讓我們返回一個交互的對象,用來實時管理控制器之間轉(zhuǎn)場動畫的完成度购岗,通過它我們可以讓控制器的轉(zhuǎn)場動畫與用戶交互(注意一點汰聋,如果方法1返回是nil,方法2是不會調(diào)用的喊积,也就是說烹困,只有我們自定義的動畫才可以與控制器交互)
- (id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController {
/**
* 方法2會傳給你當前的動畫對象animationController,判斷如果是我們自定義的Pop動畫對象乾吻,那么就返回interactivePopTransition來監(jiān)控動畫完成度髓梅。
*/
if ([animationController isKindOfClass:[PopAnimation class]])
return self.interactivePopTransition;
return nil;
}
有兩點不要忘記:
設置導航控制器的代理為當前控制器。
給控制器加手勢绎签。
OK枯饿,這樣我們就完成了這個過程。
方案二:Runtime+KVC
要了解這樣的做法诡必,需要有Runtime的一些知識奢方,會涉及到私有變量、私有方法的獲取,但是這樣做比較簡單也比較有趣袱巨,如果你感興趣就繼續(xù)看下去吧阁谆。關于Runtime的知識鉴逞,今后我會分享到博客里意推,朋友們敬請期待。
為了方便大家閱讀下面的代碼灼擂,我們需要先了解系統(tǒng)的這個手勢嫉入。
前面我們了解到焰盗,這個手勢屬于UINavigationController,我們就跳到它的頭文件里看看能不能找到線索咒林。這個思路是正確的熬拒,確實有一個手勢叫做interactivePopGestureRecognizer。屬性為readonly垫竞,就是說我們不能給他換成自定義的手勢澎粟,但是可以設置enable=NO。ok欢瞪,既然找到了它活烙,就打印一下看看它到底是一個什么手勢。
<UIScreenEdgePanGestureRecognizer: 0x7ff77d801a10; state = Possible; delaysTouchesBegan = YES; view = <UILayoutContainerView 0x7ff77b7014f0>; target= <(action=handleNavigationTransition:, target=<_UINavigationInteractiveTransition 0x7ff77d8018d0>)>>
通過log遣鼓,我們看到他屬于UIScreenEdgePanGestureRecognizer這個類(之前我是沒有用到過)啸盏,它繼承自UIPanGestureRecognizer,出現(xiàn)在IOS7以后骑祟,是專門處理在屏幕邊緣觸發(fā)的手勢類型回懦,并且只有一個屬性叫edges,用來設置它的觸發(fā)邊緣(上次企、下怯晕、左、右抒巢、全部)贫贝。看到這里一些朋友會想蛉谜,直接改它的edges為全部可不可以稚晚?經(jīng)過試驗了解到,改這個屬性是沒用的型诚,它只能用來觸發(fā)邊緣客燕,設為全部的意思是四個方向的邊緣會觸發(fā),而且用來做控制器POP手勢的只有左邊緣狰贯。
我們繼續(xù)看它的log也搓∩屠控制臺除了打印了它的類,還打印了它的觸發(fā)target:_UINavigationInteractiveTransition(這是一個私有類傍妒,看來是專門用來做導航控制器交互動畫的)幔摸,和action:handleNavigationTransition(這是它的一個私有方法),我們要做的就是新建一個UIPanGestureRecognizer颤练,讓它的觸發(fā)和系統(tǒng)的這個手勢相同既忆,這就需要利用runtime獲取系統(tǒng)手勢的target和action。
那么如何獲取這個target呢嗦玖?一開始我用kvc想直接獲取這個手勢的target患雇,程序崩潰了,原來它根本沒有這樣一個屬性宇挫。所以我能想到的是苛吱,先利用runtime遍歷它的所有成員變量,看看系統(tǒng)是怎么存儲這個屬性的器瘪,
unsigned int count = 0;
// 獲取UIGestureRecognizer的所有成員變量
Ivar *ivar = class_copyIvarList([UIGestureRecognizer class], &count);
for (int i = 0; i < count; i ++) {
Ivar _var = *(ivar + i);
NSLog(@"%s",ivar_getTypeEncoding(_var));
NSLog(@"%s",ivar_getName(_var));
}
通過log我們可以看到翠储,UIGestureRecognizer有一個叫_targets的屬性,它的類型為NSMutableArray娱局。
2016-12-28 17:38:11.298 popaAnimation[3669:199104] @"NSMutableArray"
2016-12-28 17:38:11.298 popaAnimation[3669:199104] _targets
2016-12-28 17:38:11.298 popaAnimation[3669:199104] @"NSMutableArray"
2016-12-28 17:38:11.298 popaAnimation[3669:199104] _delayedTouches
2016-12-28 17:38:11.299 popaAnimation[3669:199104] @"NSMutableArray"
2016-12-28 17:38:11.299 popaAnimation[3669:199104] _delayedPresses
2016-12-28 17:38:11.299 popaAnimation[3669:199104] @"UIView"
2016-12-28 17:38:11.299 popaAnimation[3669:199104] _view
2016-12-28 17:38:11.299 popaAnimation[3669:199104] d
2016-12-28 17:38:11.300 popaAnimation[3669:199104] _lastTouchTimestamp
2016-12-28 17:38:11.300 popaAnimation[3669:199104] q
2016-12-28 17:38:11.300 popaAnimation[3669:199104] _state
2016-12-28 17:38:11.300 popaAnimation[3669:199104] q
2016-12-28 17:38:11.301 popaAnimation[3669:199104] _allowedTouchTypes
2016-12-28 17:38:11.301 popaAnimation[3669:199104] q
2016-12-28 17:38:11.301 popaAnimation[3669:199104] _initialTouchType
2016-12-28 17:38:11.306 popaAnimation[3669:199104] @"NSMutableSet"
2016-12-28 17:38:11.307 popaAnimation[3669:199104] _internalActiveTouches
2016-12-28 17:38:11.307 popaAnimation[3669:199104] @"_UIForceLevelClassifier"
2016-12-28 17:38:11.307 popaAnimation[3669:199104] _forceClassifier
2016-12-28 17:38:11.307 popaAnimation[3669:199104] q
2016-12-28 17:38:11.307 popaAnimation[3669:199104] _requiredPreviewForceState
2016-12-28 17:38:11.307 popaAnimation[3669:199104] @"_UITouchForceObservable"
2016-12-28 17:38:11.308 popaAnimation[3669:199104] _touchForceObservable
2016-12-28 17:38:11.309 popaAnimation[3669:199104] @"NSObservation"
2016-12-28 17:38:11.309 popaAnimation[3669:199104] _touchForceObservableAndClassifierObservation
2016-12-28 17:38:11.309 popaAnimation[3669:199104] @"NSMutableArray"
2016-12-28 17:38:11.309 popaAnimation[3669:199104] _forceTargets
2016-12-28 17:38:11.310 popaAnimation[3669:199104] Q
2016-12-28 17:38:11.310 popaAnimation[3669:199104] _forcePressCount
2016-12-28 17:38:11.310 popaAnimation[3669:199104] @"NSObservationSource"
2016-12-28 17:38:11.310 popaAnimation[3669:199104] _beganObservable
2016-12-28 17:38:11.310 popaAnimation[3669:199104] @"NSMutableSet"
2016-12-28 17:38:11.311 popaAnimation[3669:199104] _failureRequirements
2016-12-28 17:38:11.311 popaAnimation[3669:199104] @"NSMutableSet"
2016-12-28 17:38:11.311 popaAnimation[3669:199104] _failureDependents
2016-12-28 17:38:11.311 popaAnimation[3669:199104] @"<UIGestureRecognizerDelegate>"
2016-12-28 17:38:11.311 popaAnimation[3669:199104] _delegate
2016-12-28 17:38:11.312 popaAnimation[3669:199104] @"NSArray"
2016-12-28 17:38:11.312 popaAnimation[3669:199104] _allowedPressTypes
2016-12-28 17:38:11.312 popaAnimation[3669:199104] @"UIGestureEnvironment"
2016-12-28 17:38:11.312 popaAnimation[3669:199104] _gestureEnvironment
它是用數(shù)組來存儲每一個target-action彰亥,所以可以動態(tài)的增加手勢觸發(fā)對象。那么又是怎么存儲每一個target-action呢衰齐?為了了解這個我們拿到這個屬性的名字"_targets"通過kvc獲取它,接著打印出來继阻。
UIGestureRecognizer *gesture = self.interactivePopGestureRecognizer;
NSMutableArray *_targets = [gesture valueForKey:@"_targets"];
NSLog(@"%@",_targets);
NSLog(@"&@",_targets[0]);
打印結(jié)果如下:
2016-12-28 17:56:00.073 popaAnimation[3757:211127] (
"(action=handleNavigationTransition:, target=<_UINavigationInteractiveTransition 0x7faa4a60a4f0>)"
)
2016-12-28 17:56:04.677 popaAnimation[3757:211127] &@
可以看到耻涛,由于系統(tǒng)重寫了它的description方法,所以我們沒辦法通過打印獲取這個對象是什么類型瘟檩。既然不能打印抹缕,那么我們就用斷點調(diào)試,來看它的真實類型墨辛,
我們看到卓研,原來每一個target-action是用UIGestureRecognizerTarget這樣一個類來存儲的,它也是一個私有類睹簇。
蘋果把許多的類做私有化也是有原因所在奏赘,其實在平時我們拿到這個類也是沒有用的,他們的目的之一是避免對開發(fā)者公開無用的類太惠,影響了封裝性磨淌。所以在類的設計上,還是要向蘋果學習凿渊。
下面直接看代碼梁只。
我們在控制器的ViewDidLoad加上這段代碼缚柳,并且它只需要執(zhí)行一次。
UIGestureRecognizer *gesture = self.interactivePopGestureRecognizer;
gesture.enabled = NO;
UIView *gestureView = gesture.view;
UIPanGestureRecognizer *popRecognizer = [[UIPanGestureRecognizer alloc] init];
popRecognizer.delegate = self;
popRecognizer.maximumNumberOfTouches = 1;
[gestureView addGestureRecognizer:popRecognizer];
/**
* 獲取系統(tǒng)手勢的target數(shù)組
*/
NSMutableArray *_targets = [gesture valueForKey:@"_targets"];
/**
* 獲取它的唯一對象搪锣,我們知道它是一個叫UIGestureRecognizerTarget的私有類秋忙,它有一個屬性叫_target
*/
id gestureRecognizerTarget = [_targets firstObject];
/**
* 獲取_target:_UINavigationInteractiveTransition,它有一個方法叫handleNavigationTransition:
*/
id navigationInteractiveTransition = [gestureRecognizerTarget valueForKey:@"_target"];
/**
* 通過前面的打印构舟,我們從控制臺獲取出來它的方法簽名灰追。
*/
SEL handleTransition = NSSelectorFromString(@"handleNavigationTransition:");
/**
* 創(chuàng)建一個與系統(tǒng)一模一樣的手勢,我們只把它的類改為UIPanGestureRecognizer
*/
[popRecognizer addTarget:navigationInteractiveTransition action:handleTransition];
優(yōu)化
這個demo我會提供給大家旁壮,下面簡單說下程序的優(yōu)化思路监嗜。
優(yōu)化點一:對于方案一,其實不應該把導航控制器的代理方法以及手勢處理的方法交給視圖控制器抡谐,因為這段代碼不是屬于某一個視圖控制器裁奇,而是全局的導航控制器,所以我們應該參考蘋果的設計思想:新建一個專門管理交互過程的對象麦撵,這個類我們叫做NavigationInteractiveTransition刽肠。
優(yōu)化點二:再來看之前的ViewDidLoad中只執(zhí)行一次的代碼,其實寫在這里也不夠妥當免胃,同樣的音五,這段代碼也不屬于某一個Controller,優(yōu)化方案是新建一個導航控制器羔沙,在這個導航控制器的viewDidLoad中寫上這些代碼躺涝,這樣也并不需要dispatch once。
優(yōu)化點三:由于我們自定義的手勢是加在一個私有view上扼雏,這個view是一個全局的坚嗜,所以當這個控制器為根控制器時,我們的手勢還是在起作用诗充,這就相當于對根控制器做了pop操作苍蔬,這會出現(xiàn)一個錯誤nested pop animation can result in corrupted navigation bar。導致這個錯誤的原因還有一個蝴蜓,如果我們pop的動畫正在執(zhí)行碟绑,再去觸發(fā)一次手勢,會導致導航控制器和導航條的動畫混亂茎匠。為了避免問題出現(xiàn)我們需要成為手勢的代理格仲,判斷當前控制器是否為根控制器并且pop或者push動畫是否在執(zhí)行(這個變量是私有的,需要用kvc來獲绕А)抓狭。
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
/**
* 這里有兩個條件不允許手勢執(zhí)行,1造烁、當前控制器為根控制器否过;2午笛、如果這個push、pop動畫正在執(zhí)行(私有屬性)
*/
return self.viewControllers.count != 1 && ![[self valueForKey:@"_isTransitioning"] boolValue];
}
經(jīng)過最后的優(yōu)化苗桂,視圖控制器可以什么都不寫药磺,想使用這個效果,只要使用我們自定義的導航控制器就可以了煤伟,這樣的好處是手勢動畫與控制器完全解耦癌佩,并且不用給每一個控制器都addGesture。
文/J_雨(簡書作者)
原文鏈接:http://www.reibang.com/p/d39f7d22db6c
著作權歸作者所有便锨,轉(zhuǎn)載請聯(lián)系作者獲得授權围辙,并標注“簡書作者”。