隨著技術(shù)的迭代,現(xiàn)在對App的效果要求越來越高,那么在這篇文章里面我們一起討論一下如何在控制器做跳轉(zhuǎn)的時候?qū)?dǎo)航欄做平滑過渡的轉(zhuǎn)場
構(gòu)思:
- 首先要獲取到導(dǎo)航欄里的子控件來設(shè)置其透明度(實現(xiàn)透明度變化)
- 為所有控制器添加一個導(dǎo)航欄透明度屬性,用于記錄當前控制器的導(dǎo)航欄透明度(記錄透明度值)
- 通過監(jiān)聽手勢滑動來獲取源和目的控制器,計算從源到目的控制器的透明度變化,來改變導(dǎo)航欄的透明度(實現(xiàn)平滑過渡)

1、實現(xiàn)透明度變化
要想實現(xiàn)透明度變化,得先獲取到導(dǎo)航欄里的子控件,然后設(shè)置其alpha值。但是如何獲取呢? 首先考慮使用KVC,通過導(dǎo)航欄的'valueForKey:'方法來獲取子控件對象,但是在不同的系統(tǒng)上,導(dǎo)航欄里的子控件布局排布也是有所不同, 意味著key值并非固定,通過key值拿子控件對象的方法在不同的系統(tǒng)上就很容易拋異常。

考慮到這一點,我采用了最直接的方式:遍歷導(dǎo)航欄的所有子控件,拿到首個子控件給其設(shè)置透明度。這樣不但不需要再去考慮系統(tǒng)的問題了,同時也能滿足帶顏色的導(dǎo)航欄或者是帶背景圖的導(dǎo)航欄透明度的變化。
- (void)xa_changeNavBarAlpha:(CGFloat)navBarAlpha{
NSMutableArray *barSubviews = [NSMutableArray array];
//將導(dǎo)航欄的子控件添加到數(shù)組當中,取首個子控件設(shè)置透明度(防止導(dǎo)航欄上存在非導(dǎo)航欄自帶的控件)
for (UIView * view in self.navigationBar.subviews) {
if(![view isMemberOfClass:[UIView class]]){
[barSubviews addObject:view];
}
}
UIView *barBackgroundView = [barSubviews firstObject];
barBackgroundView.alpha = navBarAlpha;
}
2、記錄透明度
每個控制器都應(yīng)該有自己的導(dǎo)航欄透明度且當透明度發(fā)生變化后我們都應(yīng)該把值保存下來,以方便下次的使用,這里我們就給UIViewController添加一個分類并加一個navBarAlpha的屬性,這樣我們就可以直接通過控制器去設(shè)置導(dǎo)航欄的透明度啦~
- (CGFloat)xa_navBarAlpha{
return [objc_getAssociatedObject(self, _cmd)floatValue] ;
}
- (void)setXa_navBarAlpha:(CGFloat)xa_navBarAlpha{
if(xa_navBarAlpha > 1){
xa_navBarAlpha = 1;
}
if(xa_navBarAlpha < 0){
xa_navBarAlpha = 0;
}
objc_setAssociatedObject(self, @selector(xa_navBarAlpha), @(xa_navBarAlpha), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
[self.navigationController xa_changeNavBarAlpha:xa_navBarAlpha];
}
3、實現(xiàn)平滑過渡
現(xiàn)在存在的問題就是我在某個頁面設(shè)置了導(dǎo)航欄的透明度,back回上一個界面,導(dǎo)航欄的透明度值仍然是上個界面的

這里做過渡有兩種情況,一種是手勢滑動back回上一個界面,還有一種情況是直接點擊了back按鈕回到上一個界面的。根據(jù)這兩種情況我們分別做一下處理。
3.1、手勢滑動back
這種情況我們需要去監(jiān)聽導(dǎo)航控制器的手勢滑動,導(dǎo)航控制器有個方法'_updateInteractiveTransition:',該方法可以監(jiān)聽手勢滑動以及當前轉(zhuǎn)場的進度,我們可以通過swizzing來交換方法實現(xiàn),來接手'_updateInteractiveTransition:'方法調(diào)用的監(jiān)聽
+ (void)load{
//交換導(dǎo)航控制器的手勢進度轉(zhuǎn)場方法,來監(jiān)聽手勢滑動的進度
SEL originalSEL = NSSelectorFromString(@"_updateInteractiveTransition:");
SEL swizzledSEL = NSSelectorFromString(@"xa_updateInteractiveTransition:");
Method originalMethod = class_getInstanceMethod(self, originalSEL);
Method swizzledMethod = class_getInstanceMethod(self, swizzledSEL);
BOOL success = class_addMethod(self, originalSEL, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if(success){
class_replaceMethod(self, swizzledSEL, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
}else{
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
然后通過轉(zhuǎn)場的上下文信息,拿到源和目的的控制器navBarAlpha值,再根據(jù)percentComplete轉(zhuǎn)場進度參數(shù)計算并設(shè)置導(dǎo)航欄透明度值,這樣就完成了手勢滑動的back
- (void)xa_updateInteractiveTransition:(CGFloat)percentComplete{
[self xa_updateInteractiveTransition:percentComplete];
UIViewController *topVC = self.topViewController;
if(topVC){
//通過transitionCoordinator拿到轉(zhuǎn)場的兩個控制器上下文信息
id <UIViewControllerTransitionCoordinator> coordinator = topVC.transitionCoordinator;
if(coordinator != nil){
//拿到源控制器和目的控制器的透明度(每個控制器都單獨保存了一份)
CGFloat fromVCAlpha = [coordinator viewControllerForKey:UITransitionContextFromViewControllerKey].xa_navBarAlpha;
CGFloat toVCAlpha = [coordinator viewControllerForKey:UITransitionContextToViewControllerKey].xa_navBarAlpha;
//再通過源,目的控制器的導(dǎo)航條透明度和轉(zhuǎn)場的進度(percentComplete)計算轉(zhuǎn)場時導(dǎo)航條的透明度
CGFloat newAlpha = fromVCAlpha + ((toVCAlpha - fromVCAlpha ) * percentComplete);
//這里不要直接去修改控制器navBarAlpha屬性,會影響目的控制器的navBarAlpha的數(shù)值
[self xa_changeNavBarAlpha:newAlpha];
}
}
}
3.2、按鈕點擊back
當點擊buttonItem back控制器的時候我們可以在'viewWillAppear:'的時候設(shè)置回當前控制器的透明度值,所以我們同樣要交換'viewWillAppear:'方法的實現(xiàn),那么每當控制器要顯示的時候,我們總是要將它重置回當前控制器應(yīng)有的透明度值。
這里另外還需要做兩個邏輯判斷:
- 一個是判斷手勢是否正在滑動。如果是YES表示當前的狀態(tài)是手勢滑動back的狀態(tài)則不需要處理。
- 另外一個邏輯是判斷當前控制器是否設(shè)置過navBarAlpha的屬性值。如果有設(shè)置過,那么每次控制器要顯示的時候都要將導(dǎo)航欄透明度設(shè)置成控制器儲存的透明值。反之,我們給這個控制器設(shè)置一個默認的透明度值
- (void)xa_viewWillAppear:(BOOL)animated{
[self xa_viewWillAppear:animated];
//當前控制器父控制器是導(dǎo)航控制器并且不是通過手勢滑動顯示的
if([self.parentViewController isKindOfClass:[UINavigationController class]] &&
(!self.navigationController.xa_isGrTransitioning)){
//如果在控制器初始化的時候用戶設(shè)置過導(dǎo)航欄的值,那么我們直接設(shè)置該導(dǎo)航欄應(yīng)有的透明度值,沒有設(shè)置過的話默認透明度給1
if(self.xa_didSetBarAlpha){
[self.navigationController xa_changeNavBarAlpha:self.xa_navBarAlpha];
}else{
self.xa_navBarAlpha = 1;
}
}
}
4、細節(jié)優(yōu)化與調(diào)整
在手勢滑動的時候,如果滑動到了一半就松手了,那么導(dǎo)航欄就可能自動完成或者取消返回操作了,導(dǎo)致剩下的導(dǎo)航欄的透明度將無法計算,可以看到firstViewController的導(dǎo)航欄透明度并非是1

對于這一點的話,我們可以添加手勢滑動的交互的狀態(tài),如果當前的滑動的過程中中斷,那么判斷是取消操作還是完成操作,然后再完成剩余的動畫效果。
首先我們要去監(jiān)聽導(dǎo)航控制器的'popViewControllerAnimated:'的方法調(diào)用
+ (void)load{
//交換導(dǎo)航控制器的popViewControllerAnimated:方法,來監(jiān)聽什么時候當前控制被back
SEL popOriginalSEL = @selector(popViewControllerAnimated:);
SEL popSwizzledSEL = NSSelectorFromString(@"xa_popViewControllerAnimated:");
Method popOriginalMethod = class_getInstanceMethod(self, popOriginalSEL);
Method popSwizzledMethod = class_getInstanceMethod(self, popSwizzledSEL);
BOOL popSuccess = class_addMethod(self, popOriginalSEL, method_getImplementation(popSwizzledMethod), method_getTypeEncoding(popSwizzledMethod));
if(popSuccess){
class_replaceMethod(self, popSwizzledSEL, method_getImplementation(popOriginalMethod), method_getTypeEncoding(popOriginalMethod));
}else{
method_exchangeImplementations(popOriginalMethod, popSwizzledMethod);
}
}
然后再通過轉(zhuǎn)場協(xié)調(diào)器對象監(jiān)聽手勢滑動交互的改變
- (UIViewController *)xa_popViewControllerAnimated:(BOOL)animated{
UIViewController *popVc = [self xa_popViewControllerAnimated:animated];
if(self.viewControllers.count <= 0){
return popVc;
}
UIViewController *topVC = [self.viewControllers lastObject];
if (topVC != nil) {
id<UIViewControllerTransitionCoordinator> coordinator = topVC.transitionCoordinator;
//監(jiān)聽手勢返回的交互改變,如手勢滑動過程當中松手就會回調(diào)block
if (coordinator != nil) {
if([[UIDevice currentDevice].systemVersion intValue] >= 10){//適配iOS10
[coordinator notifyWhenInteractionChangesUsingBlock:^(id<UIViewControllerTransitionCoordinatorContext> context){
[self dealNavBarChangeAction:context];
}];
}else{
[coordinator notifyWhenInteractionEndsUsingBlock:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
[self dealNavBarChangeAction:context];
}];
}
}
}
return popVc;
}
最后我們通過轉(zhuǎn)場的上下文信息根據(jù)操作(自動完成還是返回操作)來獲取剩余的動畫時長,并完成剩余的動畫
- (void)dealNavBarChangeAction:(id<UIViewControllerTransitionCoordinatorContext>)context {
if ([context isCancelled]) {// 取消了(還在當前頁面)
//根據(jù)剩余的進度來計算動畫時長xa_changeNavBarAlpha
CGFloat animdDuration = [context transitionDuration] * [context percentComplete];
CGFloat fromVCAlpha = [context viewControllerForKey:UITransitionContextFromViewControllerKey].xa_navBarAlpha;
[UIView animateWithDuration:animdDuration animations:^{
[self xa_changeNavBarAlpha:fromVCAlpha];
}];
} else {// 自動完成(pop到上一個界面了)
CGFloat animdDuration = [context transitionDuration] * (1 - [context percentComplete]);
CGFloat toVCAlpha = [context viewControllerForKey:UITransitionContextToViewControllerKey].xa_navBarAlpha;
[UIView animateWithDuration:animdDuration animations:^{
[self xa_changeNavBarAlpha:toVCAlpha];
}];
};
}
最后:
下期會寫一篇全屏pop手勢的文章,可以配合該項目一起使用
demo的地址:https://github.com/aboutlan/XANavBarTransition,歡迎star喔~??