透明与半透明 NavigationBar 切换的三种方案

App 中会经常需要在透明与不透明 NavigationBar 的页面相互切换。有些时候在透明 NavigationBar 页面甚至还需要根据 scrollView 的 contentOffset 来动态调整 NavigationBar 的透明度。有很多 App 都解决不好这里的问题(其实更多的是不在意)。这里 BAT 的反面教材各举一例:QQ、百度云、菜鸟裹裹。下边简单分析下他们的问题。

常用 App 分析

由于导航栏 push 和 pop 的动画都很短,这些问题都是通过侧滑手势来复现的。而且在侧滑返回的时候页面不一定会被 pop。由于实现导航栏隐藏的代码大部分都放在 viewWillAppear、viewDidAppear、viewWillDisappear、viewDidDisappear 四个方法中。通过侧滑手势滑动一段距离再退回去会让这四个方法都被调用,这样才会让其中的 BUG 变得明显。如不加说明文中都是通过侧滑手势进行的测试。

QQ 的问题出现在“动态” Tab 点击进入“好友动态”页面的过程。也许你会觉得这并没有什么问题。的确,这种方案在所有的解决方案中最少算是及格了。但是他的问题在于放弃了系统导航栏中 navigationItem 的动画,导致上一个页面和下一个页面的 title 和 navigationItem 重合。而系统 NavigationBar 的动画做了平移、渐变、裁剪来避免这些问题。QQ 算是一个偷懒的做法。

百度云的问题就要严重的多了。他的问题出现“更多” Tab 跳转到任意一个下级页面的过程中。当点击跳转之后,导航栏会突然出现。在 pop 结束后导航栏又突然消失,显得十分突兀。好在下级页面和本级页面的 Header 都是相近的颜色,如果不通过手势返回很难注意到这个问题。

如果说 QQ 的方案是 60 分,百度云的方法是 40 分的话。那菜鸟裹裹只能得 0 分了。不管是在 push 还是 pop,还是 viewWillAppear、viewDidAppear、viewWillDisappear、viewDidDisappear 的时候,导航栏统统突然出现突然消失,而且前后页面的颜色差异巨大。更夸张的是,他的每一个 Tab 都是这种透明导航栏的设计,所以这个 BUG 贯穿了整个 App 的一级页面跳转。(现在已经修复,但仍有很多问题)

说了这么多反面例子。说说一些正面的例子。天猫、淘宝、支付宝、今日头条、百度贴吧、去哪儿这些 App 都有处理过这些问题。他们都使用系统提供的方案,仅仅几行代码就可以。系统的方案放弃了系统导航栏的动画,但已经十分完美。

最后一个最优秀的例子就是微信。微信在导航栏上所有细节的处理都是最优秀的方案,这在所有 App 中是很罕见的。他保留了导航栏的毛玻璃效果和动画效果并解决了系统导航栏的 BUG。具体可以参考微信的聊天页面与红包页面之间的跳转。

系统 NavigationBar 的 BUG

分析了其他的 App 后就该讲讲本文的三种方案了。但在这之前先要指出一个系统导航栏的 BUG。就是当 pushViewController 时设置了 hideBottomBarWhenPushed 并且此时的 NavigationBar 是半透明效果,那么 NavigationBar 在隐藏与不隐藏 tabBar 的两个页面 push 和 pop 的过程中会出现透到下一层的现象。具体的现象就如 这里这里 的描述。

这个 BUG 不仅在 hideBottomBarWhenPushed 时出现,文中添加假的 NavigationBar 等许多场景都会出现。但这都是在导航栏具有毛玻璃效果的情况下。它在百度贴吧的个人中心被完美复现。同样在微信中被完美解决。

三种解决方案

本文提供的三种方案如下图所示。你可以在这里下载代码。

说了这么多,你一定感觉很神秘了。不要被上边的瞎扯迷惑了,实际的实现都十分简单。下边就逐一说下思路。

Demo 的文件名与文中方案的对应关系
Hidden : 对应方案一
Opaque : 对应方案二
Translucent : 对应方案三
Native : 对应系统不隐藏 NavigationBar 效果

1.系统方案

系统本身就为这种场景设计了专门的 API。你只需要在需要隐藏导航栏的界面的 viewWillAppear 和 viewWillDisappear 调用如下 API 就可以了。

- (void)viewWillAppear:(BOOL)animated {
    [self.navigationController setNavigationBarHidden:YES animated:YES];
}

- (void)viewWillDisappear:(BOOL)animated {
    [self.navigationController setNavigationBarHidden:NO animated:YES];
}

那些导航栏突然出现和消失的都是因为他们分别在这两个方法调用的不是

[self.navigationController setNavigationBarHidden:YES animated:YES];

而是

self.navigationController.navigationBar.hidden = YES;

这样就可以实现像天猫、淘宝、支付宝、今日头条等上述列出 App 的效果。那些连这种效果都没有实现的,像 QQ 可能是为了追求导航栏渐变的效果,但其他的那些只能是因为产品本身(或程序员本身)不够追求细节导致的。

这种效果有一个坏处就是导航栏被完全隐藏了。这�导致导航栏的动画丢失,侧滑手势失效,而且如果想添加 navigationItem 只能手动在对应位置放上 Button。

Demo 中的这种方案还实现了导航栏的渐变,其实是在隐藏 NavigationBar 的界面添加了一个假的 NavigationBar。但这会导致系统 NavigationBar 的 BUG 再次出现。想要解决这个问题你只能放弃毛玻璃效果用假的 view 来替代假的 NavigationBar,或者放弃 iOS7 在 view 上实现系统毛玻璃的效果。

另外,要想使侧滑返回手势生效。可以自己添加 Gesture。或者可以在 viewController 中
使用如下代码:

self.interactivePopGestureRecognizer.delegate = self;

使用这行代码同样会带来一个不容易复现的问题,就是当界面多次 push 和 pop 后手势就会错乱,导致界面无法 push,这时侧滑变成了 push。Demo 中在 LPHiddenNavigationController 中解决了这个问题,大概就是通过在适当的时候调整手势的 enabled 来实现,这里不再详细说明。

self.interactivePopGestureRecognizer.enabled = NO;

2.全局替换

这种方案的原理同样十分简单。首先是通过如下代码来全局隐藏原 NavigationBar 的背景和分割线。这段代码是写在 baseNavigationController 里的,你也可以通过 appearance 来全局设置。

[self.navigationBar setBackgroundImage:[UIImage new] forBarMetrics:UIBarMetricsDefault];
[self.navigationBar setShadowImage:[UIImage new]];

之后通过 baseViewController 给每个 controller 添加假的 NavigationBar 充当背景。这同样会导致系统 NavigationBar 的 BUG 出现。你仍然可以通过放弃毛玻璃效果用假的 View 来替代假的 NavigationBar,或者放弃 iOS7 在 view 上实现系统毛玻璃的效果来避免这个问题。而且使用 NavigationBar 充当背景比较笨重,view 就显得轻便很多了。

这种方案保留了系统原有的 NavigationBar 动画,添加的假的 NavigationBar 只是充当背景,NavigationItem 还由原来的 NavigationBar 实现,已经是十分完美的方案了。但是缺点是需要将所有 viewController 继承自 baseViewController,如果项目本身没有 baseViewController 就会比较麻烦。

在这个 Demo 中同样做了导航栏的渐变效果。由于是每个页面都有一个假的 NavigationBar 充当背景,实现这个效果就十分简单了,只需要调整当前页面 NavigationBar 的透明度即可。这种方案去实现 NavigationBar 的颜色变化也是非常方便的。

3.微信方案

这种方案是在知乎上看到的。“会会”的工程师通过反编译微信找到了这种实现方案。这其实是对方案二的“改进”。方案二需要全局隐藏 NavigationBar 的背景才行,因为涉及到 push 和 pop 的两个 viewController 的 NavigationBar 是不同的透明度或者不同的颜色,而在 push 和 pop 的过程中两种不同的 NavigationBar 要同时显示出来,这就导致我们不可能去统一设置 NavigationBar 的样式。所以不管什么方案我们都只能通过添加假的 NavigationBar 来实现。

但这种方案添加的方式比较巧妙。在 push 或 pop 动画将要执行的时候隐藏系统 NavigationBar 的背景,给涉及到的需要不透明效果的 viewController 添加假的 NavigationBar。当动画执行完毕后,再将假的 NavigationBar 移除并根据当前页面是否需要显示 NavigationBar 来选择是否将系统 NavigationBar 的背景重新设置回来。这些功能均在 viewWillAppear、viewDidAppear、viewWillDisappear、viewDidDisappear 四个方法中实现,具体的代码逻辑稍微复杂一点,直接看代码来理解:

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    self.navigationController.navigationBar.userInteractionEnabled = NO;
    [self removeFakeNavBar];
    if (((LPTranslucentNavigationController *)self.navigationController).shouldAddFakeNavigationBar) {
        [self.navigationController.navigationBar setBackgroundImage:[UIImage new] forBarMetrics:UIBarMetricsDefault];
        [self addFakeNavBar];
    }
}

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    self.navigationController.navigationBar.userInteractionEnabled = YES;
    if (![self useTransparentNavigationBar]) {
        self.navigationController.navigationBar.barStyle = UINavigationBar.appearance.barStyle;
        self.navigationController.navigationBar.translucent = YES;
        [self.navigationController.navigationBar setBackgroundImage:[UINavigationBar.appearance backgroundImageForBarMetrics:UIBarMetricsDefault] forBarMetrics:UIBarMetricsDefault];
    }
    [self removeFakeNavBar];
}

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    [self removeFakeNavBar];
    if (((LPTranslucentNavigationController *)self.navigationController).shouldAddFakeNavigationBar) {
        [self addFakeNavBar];
    }
}

- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
    [self removeFakeNavBar];
}

其中 shouldAddFakeNavigationBar 是在 LPTranslucentNavigationController 中实现的方法。其值根据 navigationController 中最后两个 controller 是否需要透明效果来改变。如果有至少一个需要透明效果,就 return YES,否则 return NO。

另外我们还需要隐藏 NavigationBar 的分割线(Demo 中并没有做)。你也许会觉得像方案二一样添加一行代码不就可以了。

[self.navigationController.navigationBar setShadowImage:[UIImage new]];

但事实这行代码还是会触发系统 NavigationBar 的 BUG。所以只有通过遍历来去掉这根顽固的线了,以下代码在 Demo 中 UIViewController+HideBottomLine 里实现。

- (void)hideBottomLineInView:(UIView *)view {
    UIImageView *navBarLineImageView = [self findLineImageViewUnder:view];
    navBarLineImageView.hidden = YES;
}

- (void)showBottomLineInView:(UIView *)view {
    UIImageView *navBarLineImageView = [self findLineImageViewUnder:view];
    navBarLineImageView.hidden = NO;
}

- (UIImageView *)findLineImageViewUnder:(UIView *)view {
    if ([view isKindOfClass:UIImageView.class] && view.bounds.size.height <= 1.0) {
        return (UIImageView *)view;
    }
    for (UIView *subview in view.subviews) {
        UIImageView *imageView = [self findLineImageViewUnder:subview];
        if (imageView) {
            return imageView;
        }
    }
    return nil;
}

微信的红包页面其实并不是隐藏了 NavigationBar,而是将 NavigationBar 变为了红色。但思路是相同的,只是将调整透明度变成了调整颜色。

如果整个 App 中有很多这样 NavigationBar 不同的页面,可以参照 Demo 中将其写在 baseViewController 和 baseNavigationController。如果只是单个页面使用,只需要在单个页面按照上述思路去实现就行。

但这种方案也并不是没有缺点。他有一个小小的问题就是。由于频繁的切换真假 NavigationBar,如果通过侧滑手势快速频繁的“返回-取消-返回-取消” NavigationBar 可能偶尔会出现闪烁现象。当然这只是极限测试,正常使用中很难遇到。而且这只出现在从不透明页面向透明页面返回的时候。微信的红包页面调整的是颜色而不是透明度,所以不会出现这个问题。

另外一个问题就是,如果想在此基础上实现 NavigationBar 的渐变,就需要在透明 NavigationBar 的页面出现后再次添加一个假的 NavigationBar。这使得整个过程显得十分的繁琐,而且同样带来了系统 NavigationBar 的 BUG,还不如直接使用方案二。

总结

上述的方案中,如果不需要毛玻璃效果,都是比较完美的方案。如果需要毛玻璃且不想自己实现,只有第三种方案是比较合理的,但是这种方案也只是能满足透明到不透明的效果,想要实现渐变效果还是会引发 BUG。

如果想要最简单的实现方式,还是应该选择方案二。这种方案也是在自己的很多项目中实际使用的方案,因为事实上很多项目都不需要 NavigationBar 的毛玻璃效果。

如果不嫌手动添加返回手势麻烦,或者干脆不需要滑动返回手势,比如作为 tabBar 一级页面的个人中心等,使用方案一才是最简单的选择。

文中使用 baseViewController 和 baseNavigationController 实现的思路都可以通过 Category + Method Swizzling 实现。这种侵入式的方案可以让你免去继承的麻烦,都是可以考虑的。比如这位韩国开发者就是这样实现的。

博客:xuyafei.cn
简书:jianshu.com/users/2555924d8c6e
微博:weibo.com/xuyafei86
Github:github.com/xiaofei86

参考资料

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,796评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,066评论 4 62
  • 这是第二次去日本,在京都呆了五天,虽然时间也不算很长,但好歹,圆了一个梦想,又开启了更多的期待。 这是七月,说起京...
    柚子琥珀阅读 524评论 7 2
  • 请用自己的实力证明自己。战胜自我,既然选择了语言, 那么请把外语学习这件“费力”的事情担当起来。 我没有语言天赋。...
    月儿好看_阅读 3,203评论 5 66
  • 大家下午好!下面是五个工作小组的分组情况: 接下来还会有更多新的组员的加入,希望大家不要太担心工作量,我们还会再继...
    LisaHong_52cd阅读 210评论 0 0