神乎其技的導(dǎo)航欄透明度漸變

一、寫(xiě)在前面

好久沒(méi)有更新文章了,因?yàn)樽陨頃r(shí)間安排的原因,而且當(dāng)你著手寫(xiě)的時(shí)候才發(fā)現(xiàn),要把一系列不那么簡(jiǎn)單的邏輯用文字表述明白,真的很難。就這篇文章浮夸的標(biāo)題,其實(shí)我就想了大概2分鐘??~

二、屌絲的自定義導(dǎo)航欄實(shí)現(xiàn)

在項(xiàng)目中,很多時(shí)候某些頁(yè)面導(dǎo)航欄是不顯示的,并且類(lèi)似于個(gè)人信息頁(yè)面,滑動(dòng)過(guò)程中導(dǎo)航欄還會(huì)隨之顯示或隱藏。大多數(shù)童鞋都是這樣處理的:

  • viewWillAppear:方法中設(shè)置導(dǎo)航欄隱藏
  • viewWillDisappear:方法中設(shè)置導(dǎo)航欄顯示
  • 重寫(xiě)一個(gè)與系統(tǒng)導(dǎo)航欄等高的view,操作其alpha值

一切都是那么的完美,舒舒服服~

三、情景再現(xiàn)

直到某一天,產(chǎn)品大大心血來(lái)潮,新需求應(yīng)聲而到:你們iOS系統(tǒng)不是支持側(cè)滑的嗎?為什么咱們的App不能側(cè)滑,趕緊加上,今天發(fā)個(gè)新包
對(duì)于一個(gè)看似簡(jiǎn)單的需求,很多屌絲會(huì)立馬說(shuō)一句:so easy~,于是咔咔咔一通code。打包測(cè)試的時(shí)候,測(cè)試妹紙會(huì)很快發(fā)現(xiàn)下面這樣的一個(gè)bug

bug1[圖片上傳中...(2.gif-5575aa-1541423903119-0)]

bug2

總之很屌絲
以上情景純屬虛構(gòu),如若雷同,必是巧合

四、分析bug產(chǎn)生原因

比較簡(jiǎn)單,大伙兒結(jié)合上面處理的方式進(jìn)行分析,就能明白了。

五、問(wèn)題解決思路

方案一

把導(dǎo)航控制器的根控制器導(dǎo)航欄隱藏,使用自定義view作為導(dǎo)航欄。這樣側(cè)滑返回的時(shí)候,就不會(huì)有在viewWillAppear與viewWillDisAppear中操作系統(tǒng)導(dǎo)航欄是否隱藏的邏輯。從而避免了上面兩種bug的產(chǎn)生。
缺點(diǎn): 太過(guò)于屌絲,為了解決這一個(gè)bug,需要將根控制器到有此需求的控制器之間的所有導(dǎo)航欄都隱藏,并針對(duì)每個(gè)頁(yè)面畫(huà)偽導(dǎo)航欄。并且完全舍棄了系統(tǒng)側(cè)滑時(shí)導(dǎo)航欄動(dòng)畫(huà),效果僵硬。

方案二

最近的一篇文章也有介紹過(guò)自定義轉(zhuǎn)場(chǎng)動(dòng)畫(huà)的實(shí)現(xiàn)。我們?cè)谙到y(tǒng)SDK提供的轉(zhuǎn)場(chǎng)協(xié)議方法中,針對(duì)fromVC與toVC中的控件,做自定義的轉(zhuǎn)場(chǎng)動(dòng)畫(huà)實(shí)現(xiàn)。

其實(shí)導(dǎo)航側(cè)滑返回也是系統(tǒng)的轉(zhuǎn)場(chǎng)的一種。但是對(duì)于同一個(gè)導(dǎo)航控制器下的視圖控制器,導(dǎo)航欄透明度屬性都是全局的,并不屬于fromVC與toVC的任何一個(gè)。所以首先要做的就是針對(duì)試圖控制器,添加一個(gè)導(dǎo)航欄透明度屬性。

1. 通過(guò)VC的alpha,控制導(dǎo)航欄透明度

在runtime實(shí)現(xiàn)setter方法時(shí),我們背地里實(shí)際上是操作視圖控制器所在的導(dǎo)航控制器的導(dǎo)航欄的透明度。代碼如下:

- (void)setNavAlpha:(CGFloat)navAlpha
{
    objc_setAssociatedObject(self, @selector(navAlpha), @(navAlpha), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    [self.navigationController setNavigationBackgroundAlpha:navAlpha];
}

- (void)setNavigationBackgroundAlpha:(CGFloat)alpha
{
    //1.找到導(dǎo)航bar上第一個(gè)背景視圖
    UIView *barView = [[self.navigationBar subviews] firstObject];
    //2.kvc獲取陰影視圖
    UIView *shadowView = [barView valueForKey:@"_shadowView"];
    //3.如果能夠獲取到設(shè)置透明度
    if (shadowView) {
        shadowView.alpha = alpha;
    }
    //4.如果導(dǎo)航欄默認(rèn)沒(méi)有設(shè)置半透明,背景視圖透明度也進(jìn)行改變
    if (!self.navigationBar.isTranslucent) {
        barView.alpha = alpha;
        return;
    }
    
    if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 10.0) {
        
        UIView *backEffectView = [barView valueForKey:@"_backgroundEffectView"];
        if (backEffectView && [self.navigationBar backgroundImageForBarMetrics:UIBarMetricsDefault] == nil) {
            
            backEffectView.alpha = alpha;
        }
    } else {
        
        UIView *daptiveBackdrop = [barView valueForKey:@"_adaptiveBackdrop"];
        UIView *backdropEffectView = [daptiveBackdrop valueForKey:@"_backdropEffectView"];
        if (daptiveBackdrop != nil && backdropEffectView != nil ) {
            backdropEffectView.alpha = alpha;
        }
    }
}
2.手動(dòng)Pop,動(dòng)態(tài)改變導(dǎo)航欄透明度

重寫(xiě)UINavigationController的協(xié)議方法,在pop前,偷偷地操作導(dǎo)航欄。

- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item
{
    UIViewController *topVC = self.topViewController;
    id <UIViewControllerTransitionCoordinator> transitionCtx = topVC.transitionCoordinator;
    if (topVC && transitionCtx && transitionCtx.initiallyInteractive) {
        
        if ([[UIDevice currentDevice].systemVersion floatValue]>=10.0) {
            
            [transitionCtx notifyWhenInteractionChangesUsingBlock:^(id<UIViewControllerTransitionCoordinatorContext>  _Nonnull context) {
                //因?yàn)閭?cè)滑返回也會(huì)執(zhí)行此協(xié)議方法,所以在這里處理手勢(shì)專(zhuān)場(chǎng)取消的情況
                [self dealInteractionChanges:context];
            }];
        } else {
            
            [transitionCtx notifyWhenInteractionEndsUsingBlock:^(id<UIViewControllerTransitionCoordinatorContext>  _Nonnull context) {
                //因?yàn)閭?cè)滑返回也會(huì)執(zhí)行此協(xié)議方法,所以在這里處理手勢(shì)專(zhuān)場(chǎng)取消的情況
                [self dealInteractionChanges:context];
            }];
        }
        return YES;
    }
    UIViewController *popToVc = self.viewControllers[self.viewControllers.count - 2];
    [self popToViewController:popToVc animated:YES];
    return YES;
}

- (void)dealInteractionChanges:(id <UIViewControllerTransitionCoordinatorContext>)context
{
    void(^animations)(NSString *) = ^(NSString *key) {
      
        CGFloat nowAlpha = [context viewControllerForKey:key].navAlpha;
        [self setNavigationBackgroundAlpha:nowAlpha];
        self.navigationBar.tintColor = [context viewControllerForKey:key].navTintColor;
//        self.navigationBar.barTintColor = [context viewControllerForKey:key].navBarTintColor;
    };
    if (context.isCancelled) {
        
        //拖動(dòng)取消,使用toVC的屬性相關(guān)
        NSTimeInterval cancaleDuration = context.transitionDuration * context.percentComplete;
        [UIView animateWithDuration:cancaleDuration animations:^{
            animations(UITransitionContextFromViewControllerKey);
        }];
    } else {
        
        //正常拖動(dòng),使用fromVC的屬性相關(guān)
        NSTimeInterval finishDuration = context.transitionDuration * (1 - context.percentComplete);
        [UIView animateWithDuration:finishDuration animations:^{
            animations(UITransitionContextToViewControllerKey);
        }];
    }
}

因?yàn)閭?cè)滑返回也會(huì)執(zhí)行此協(xié)議方法,而側(cè)滑返回不同于手動(dòng)返回的一點(diǎn)就是,側(cè)滑返回中途有可能cancel。所以上面的方法根據(jù)轉(zhuǎn)場(chǎng)上下文協(xié)議是否cancel。來(lái)確定最后使用fromVC的alpha還是toVC的alpha

3.側(cè)滑Pop,動(dòng)態(tài)改變導(dǎo)航欄透明度

根據(jù)上面兩點(diǎn)的實(shí)現(xiàn),效果如下:


目前的效果

可以看到,手動(dòng)側(cè)滑的過(guò)程中,缺少了漸變的效果。在之前的自定義轉(zhuǎn)場(chǎng)動(dòng)畫(huà)中,我們知道轉(zhuǎn)場(chǎng)過(guò)程中,有一個(gè)方法會(huì)持續(xù)執(zhí)行。

- (void)updateInteractiveTransition:(CGFloat)percentComplete;

使用runtime對(duì)此方法進(jìn)行方法交換,在我們交換的方法中,根據(jù)進(jìn)度percentComplete對(duì)導(dǎo)航欄alpha做動(dòng)態(tài)改變處理:

- (void)xll_updateInteractiveTransition:(CGFloat)percentComplete
{
    UIViewController *topVC = self.topViewController;
    if (!topVC) return;
    //1.獲取轉(zhuǎn)場(chǎng)上下文協(xié)議
    id <UIViewControllerTransitionCoordinator>transitionCtx = topVC.transitionCoordinator;
    //2.根據(jù)轉(zhuǎn)場(chǎng)上下文協(xié)議獲取轉(zhuǎn)場(chǎng)始末控制器
    UIViewController *fromVC = [transitionCtx viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toVC = [transitionCtx viewControllerForKey:UITransitionContextToViewControllerKey];
    //3.獲取始末控制器導(dǎo)航欄透明度
    CGFloat fromAlpha = fromVC.navAlpha;
    CGFloat toAlpha = toVC.navAlpha;
    //4.計(jì)算出轉(zhuǎn)場(chǎng)過(guò)程中變化的透明度值
    CGFloat newAlpha = fromAlpha+(toAlpha-fromAlpha)*percentComplete;
    //5.重新設(shè)定透明度
    [self setNavigationBackgroundAlpha:newAlpha];  
    [self xll_updateInteractiveTransition:percentComplete];
}

效果如下:


最終效果圖

六、后期思考

導(dǎo)航欄上不僅有導(dǎo)航欄透明度,還有導(dǎo)航欄背景顏色,導(dǎo)航標(biāo)題大小與顏色,導(dǎo)航左右item內(nèi)容顏色,狀態(tài)欄樣式等。這些都有可能在相鄰的兩個(gè)頁(yè)面不同。
文章也是匆忙寫(xiě)的,講述的也比較籠統(tǒng),小伙伴們結(jié)合Demo會(huì)更有效地明白我要表達(dá)的意思這是Demo,后期也會(huì)慢慢將以上所考慮到的實(shí)現(xiàn)代碼加進(jìn)去。希望發(fā)現(xiàn)問(wèn)題及時(shí)指正,共同進(jìn)步??

七、更新

之前文章最后提出了自己的設(shè)想,已經(jīng)對(duì)其進(jìn)行了實(shí)現(xiàn)。并且整理相關(guān)的代碼,使其能夠方便地移植到現(xiàn)有項(xiàng)目中。更新點(diǎn)如下:

  • 代碼部分重構(gòu),更方便地移植到項(xiàng)目中
  • 對(duì)segue線跳轉(zhuǎn),或者代碼跳轉(zhuǎn)。進(jìn)行了兼容
  • 對(duì)系統(tǒng)側(cè)滑,或者自定義側(cè)滑。進(jìn)行了兼容。
  • 默認(rèn)的navigationItem,或者自定義的navigationItem。都可在兩個(gè)頁(yè)面中變化
    默認(rèn)的navigationItem可以設(shè)置控制器的navTintColor進(jìn)行漸變。自定義的navigationItem可以分別設(shè)置其背景圖片。
    注意?。?!

因?yàn)閷?dǎo)航欄透明度不為1的時(shí)候,根控制器最好在導(dǎo)航欄底下(注意不是下方,是頂部與導(dǎo)航欄一齊)。否則肯定是不符合需求的,我不相信有哪個(gè)項(xiàng)目會(huì)讓某個(gè)頁(yè)面導(dǎo)航欄部分是一片透明,沒(méi)有任何元素。
要控制導(dǎo)航欄頂部與根控制器頂部一齊。要設(shè)置navigationBar.translucent = YES,并且vc.edgesForExtendedLayout = UIRectEdgeTop,并且導(dǎo)航欄不能被隱藏。所以代碼中進(jìn)行了以下設(shè)置:

- (void)setNavAlpha:(CGFloat)navAlpha
{
    objc_setAssociatedObject(self, @selector(navAlpha), @(navAlpha), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    if (navAlpha < 1)
    {
        //必須滿(mǎn)足這兩個(gè)條件,導(dǎo)航根控制器頂部才能在導(dǎo)航欄底下
        //否則就沒(méi)有意義了
        self.navigationController.navigationBar.translucent = YES;
        self.edgesForExtendedLayout = UIRectEdgeTop;
    }
    [self.navigationController setNavigationBackgroundAlpha:navAlpha];
}

這兩坨東西都可在IB設(shè)置。更有甚者,導(dǎo)航欄半透明度還可以在plist文件中設(shè)置,所以需注意。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容