iOS 記一次導航欄平滑過渡的實現

aboutlan發表於2018-09-15

隨著技術的迭代,現在對App的效果要求越來越高,那麼在這篇文章裡面我們一起討論一下如何在控制器做跳轉的時候對導航欄做平滑過渡的轉場

構思:

  • 首先要獲取到導航欄裡的子控制元件來設定其透明度 (實現透明度變化)

  • 為所有控制器新增一個導航欄透明度屬性,用於記錄當前控制器的導航欄透明度 (記錄透明度值)

  • 通過監聽手勢滑動來獲取源和目的控制器,計算從源到目的控制器的透明度變化,來改變導航欄的透明度 (實現平滑過渡)

    bardemo

1、實現透明度變化

要想實現透明度變化,得先獲取到導航欄裡的子控制元件,然後設定其alpha值。但是如何獲取呢? 首先考慮使用KVC,通過導航欄的'valueForKey:'方法來獲取子控制元件物件,但是在不同的系統上,導航欄裡的子控制元件佈局排布也是有所不同, 意味著key值並非固定,通過key值拿子控制元件物件的方法在不同的系統上就很容易拋異常。

左:iOS10                    右:iOS9

考慮到這一點,我採用了最直接的方式: 遍歷導航欄的所有子控制元件,拿到首個子控制元件給其設定透明度。 這樣不但不需要再去考慮系統的問題了,同時也能滿足帶顏色的導航欄或者是帶背景圖的導航欄透明度的變化。

- (void)xa_changeNavBarAlpha:(CGFloat)navBarAlpha{
    NSMutableArray *barSubviews = [NSMutableArray array];
    //將導航欄的子控制元件新增到陣列當中,取首個子控制元件設定透明度(防止導航欄上存在非導航欄自帶的控制元件)
    for (UIView * view in self.navigationBar.subviews) {
        if(![view isMemberOfClass:[UIView class]]){
            [barSubviews addObject:view];
        }
    }
    UIView *barBackgroundView = [barSubviews firstObject];
    barBackgroundView.alpha   = navBarAlpha;
}
複製程式碼

2、記錄透明度

每個控制器都應該有自己的導航欄透明度且當透明度發生變化後我們都應該把值儲存下來,以方便下次的使用,這裡我們就給UIViewController新增一個分類並加一個 navBarAlpha的屬性,這樣我們就可以直接通過控制器去設定導航欄的透明度啦~

- (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、實現平滑過渡

現在存在的問題就是我在某個頁面設定了導航欄的透明度,back回上一個介面,導航欄的透明度值仍然是上個介面的

navbug

這裡做過渡有兩種情況,一種是手勢滑動back回上一個介面,還有一種情況是直接點選了back按鈕回到上一個介面的。根據這兩種情況我們分別做一下處理。

3.1、手勢滑動back

這種情況我們需要去監聽導航控制器的手勢滑動,導航控制器有個方法 _updateInteractiveTransition: ,該方法可以監聽手勢滑動以及當前轉場的進度,我們可以通過swizzing來交換方法實現,來接手 _updateInteractiveTransition: 方法呼叫的監聽

+ (void)load{
    //交換導航控制器的手勢進度轉場方法,來監聽手勢滑動的進度
    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);
    }
}

複製程式碼

然後通過轉場的上下文資訊,拿到源和目的的控制器navBarAlpha值,再根據percentComplete轉場進度引數計算並設定導航欄透明度值,這樣就完成了手勢滑動的back

- (void)xa_updateInteractiveTransition:(CGFloat)percentComplete{
    [self xa_updateInteractiveTransition:percentComplete];
    UIViewController *topVC = self.topViewController;
    if(topVC){
        //通過transitionCoordinator拿到轉場的兩個控制器上下文資訊
        id <UIViewControllerTransitionCoordinator> coordinator =  topVC.transitionCoordinator;
        if(coordinator != nil){
            //拿到源控制器和目的控制器的透明度(每個控制器都單獨儲存了一份)
            CGFloat fromVCAlpha  = [coordinator viewControllerForKey:UITransitionContextFromViewControllerKey].xa_navBarAlpha;
            CGFloat toVCAlpha    = [coordinator viewControllerForKey:UITransitionContextToViewControllerKey].xa_navBarAlpha;
            //再通過源,目的控制器的導航條透明度和轉場的進度(percentComplete)計算轉場時導航條的透明度
            CGFloat newAlpha     = fromVCAlpha + ((toVCAlpha - fromVCAlpha ) * percentComplete);
            //這裡不要直接去修改控制器navBarAlpha屬性,會影響目的控制器的navBarAlpha的數值
            [self xa_changeNavBarAlpha:newAlpha];
        }
    }
}
複製程式碼

3.2、按鈕點選back

當點選buttonItem back控制器的時候我們可以在 viewWillAppear: 的時候設定回當前控制器的透明度值,所以我們同樣要交換 viewWillAppear: 方法的實現,那麼每當控制器要顯示的時候,我們總是要將它重置回當前控制器應有的透明度值。

這裡另外還需要做兩個邏輯判斷:

  • 一個是判斷手勢是否正在滑動。如果是YES表示當前的狀態是手勢滑動back的狀態則不需要處理。
  • 另外一個邏輯是判斷當前控制器是否設定過navBarAlpha的屬性值。如果有設定過,那麼每次控制器要顯示的時候都要將導航欄透明度設定成控制器儲存的透明值。反之,我們給這個控制器設定一個預設的透明度
- (void)xa_viewWillAppear:(BOOL)animated{
    [self xa_viewWillAppear:animated];
    
    //當前控制器父控制器是導航控制器並且不是通過手勢滑動顯示的
    if([self.parentViewController isKindOfClass:[UINavigationController class]] &&
       (!self.navigationController.xa_isGrTransitioning)){
        //如果在控制器初始化的時候使用者設定過導航欄的值,那麼我們直接設定該導航欄應有的透明度值,沒有設定過的話預設透明度給1
        if(self.xa_didSetBarAlpha){
            [self.navigationController xa_changeNavBarAlpha:self.xa_navBarAlpha];
        }else{
            self.xa_navBarAlpha = 1;
        }
    }
}
複製程式碼

4、細節優化與調整

在手勢滑動的時候,如果滑動到了一半就鬆手了,那麼導航欄就可能自動完成或者取消返回操作了,導致剩下的導航欄的透明度將無法計算,可以看到firstViewController的導航欄透明度並非是1

bug

對於這一點的話,我們可以新增手勢滑動的互動的狀態,如果當前的滑動的過程中中斷,那麼判斷是取消操作還是完成操作,然後再完成剩餘的動畫效果。 首先我們要去監聽導航控制器的 popViewControllerAnimated: 的方法呼叫

+ (void)load{
    //交換導航控制器的popViewControllerAnimated:方法,來監聽什麼時候當前控制被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);
    }
    
}
複製程式碼

然後再通過轉場協調器物件監聽手勢滑動互動的改變

- (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;
        //監聽手勢返回的互動改變,如手勢滑動過程當中鬆手就會回撥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;
}
複製程式碼

最後我們通過轉場的上下文資訊根據操作 (自動完成還是返回操作) 來獲取剩餘的動畫時長,並完成剩餘的動畫

- (void)dealNavBarChangeAction:(id<UIViewControllerTransitionCoordinatorContext>)context {
    if ([context isCancelled]) {// 取消了(還在當前頁面)
        //根據剩餘的進度來計算動畫時長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地址,歡迎star喔~?

相關文章