iOS NavigationBar 背景顏色設置方案探究
iOS中UIViewController對象如果通過push方式呈現,是由UINavigationController利用類棧結構去維護的;而UINavigationBar則是“寄生”在是UINavigationController上的屬性對象,但棧頂的UIViewController對象卻可以操作自身navigationItem屬性(UINavigationItem對象)去決定這么多controller共享的UINavigationBar的視覺和交互表現……一言以蔽之,貴圈真亂!但UINavigationBar這么較特殊的存在,落到程序猿手中,還是得老老實實為需求服務。且恕筆者才疏學淺,就不展開那么多啦,單單就聊一聊關于UINavigationBar背景色的那些事。
曲徑初探
閑話休表,直接拎出可能會影響UINavigationBar背景色表現的那些屬性瞧瞧吧
1. backgroundColor
這個不用多介紹了,從UIView基類上繼承而來,最常用的背景色設置手段,可惜UINavigationBar偏偏不那么尋常
- 先給UINaigationBar設backgroundColor為純綠色,
self.navigationController.navigationBar.backgroundColor = [UIColor greenColor];
實際效果如下:
greenBg.png
這種朦朧的味道,仿佛是三月春風吹拂過茫茫草原……呃,可是這明顯不是純正綠色啊?
碰上這種幕后的小動作,就該Xcode自帶神器Debug View Hierarchy派上用場了,讓我們瞧瞧是誰在里面搗亂?
<font color="#4590a3">(此處以iOS 9.3為例,但iOS10中導航欄結構其實發生了變化,但層級相似)</font>
居然純綠色的UINavigationBar前面還有好幾層啊,最可惡的就是淡綠色的那層,完全遮蓋住了那純正的味道。從View Hierarchy可見,這個玩意原來是名為_UIBackdropEffectView的某個私有類對象,而且還非UINavigationBar的直接子視圖,中間還隔了_UIBackdropView類對象。果然幕后好多見不得人的勾當……
- 接下再找另外一個頁面練練手,如法炮制,設置UINaigationBar其backgroundColor為純紅色
self.navigationController.navigationBar.backgroundColor = [UIColor redColor];
但是效果卻神奇的發生了變化!見下圖
呵呵,隔著屏幕放佛也能聽到某些人內心OS:這傻子連這都寫不對……但我以蘋果爸爸的聲譽起誓,代碼寫的沒錯!那問題是出在哪呢?
正在苦苦思索中的我不小心瞥到了這么一句代碼:
self.navigationBar.translucent = NO;
貌似translucent屬性默認值為YES吧,難道是這家伙在搗鬼?那么就先把這個屬性給扒個干凈吧
2. translucent
@property(nonatomic,assign,getter=isTranslucent) BOOL translucent NS_AVAILABLE_IOS(3_0) UI_APPEARANCE_SELECTOR; // Default is NO on iOS 6 and earlier. Always YES if barStyle is set to UIBarStyleBlackTranslucent
Description
A Boolean value indicating whether the navigation bar is translucent (YES) or not (NO).
The default value is YES. If the navigation bar has a custom background image, the default is YES if any pixel of the image has an alpha value of less than 1.0, and NO otherwise.
If you set this property to YES on a navigation bar with an opaque custom background image, the navigation bar will apply a system opacity less than 1.0 to the image.
If you set this property to NO on a navigation bar with a translucent custom background image, the navigation bar provides an opaque background for the image using black if the navigation bar has UIBarStyleBlack style, white if the navigation bar has UIBarStyleDefault, or the navigation bar’s barTintColor if a custom value is defined.
Availability iOS (3.0 and later), tvOS (3.0 and later)
原來這家伙會根據UINavigationBar設置的自定義的背景圖片(見setBackgroundImage:forBarMetrics:方法),去判定是否為背景圖添加透明度!
根據 1 中的探究,translucent屬性還會影響UINavigationBar backgroundColor的體現與否,不僅如此,諸位還記得 UIViewController在iOS7.0中引入的如下兩個屬性 嗎:
@property(nonatomic,assign) UIRectEdge edgesForExtendedLayout NS_AVAILABLE_IOS(7_0); // Defaults to UIRectEdgeAll
@property(nonatomic,assign) BOOL extendedLayoutIncludesOpaqueBars NS_AVAILABLE_IOS(7_0); // Defaults to NO, but bars are translucent by default on 7_0.
特別是extendedLayoutIncludesOpaqueBars,在iOS7.0后默認為NO,然而若UINavigationBar translucent屬性為YES,則UINavigationController其topViewControll.view是包含UINavigationBar和UIStatusBar下面覆蓋的那片區域的。但若translucent屬性為NO,則除非設置controller的extendedLayoutIncludesOpaqueBars屬性為YES,topViewControll.view都是不包含此區域的。
這也是經常見到與頁面大小一致的控件卻往往會有兩種不同的frame指定方式的原因:
self.tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, NAV_HEIGHT, SCREEN_WIDTH, SCREEN_HEIGHT-NAV_HEIGHT) style:UITableViewStylePlain];
或
self.tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, self.view.bounds.size.height) style:UITableViewStylePlain];
平心而論,我個人以為同一個iOS App項目內,最好統一規范controller這些屬性設置,以免這兩種思路碰撞出的不是火花,而是排版錯亂的各種bug……
既然使用backgroundColor可能會受其他屬性的干擾,那還是要嘗試一些能更直接了當的方式,比如下面這個原生方法~
3. setBackgroundImage:forBarMetrics:方法
其完全體聲明如下
- (void)setBackgroundImage:(nullable UIImage *)backgroundImage forBarMetrics:(UIBarMetrics)barMetrics NS_AVAILABLE_IOS(5_0) UI_APPEARANCE_SELECTOR;
看到這個方法,我們不禁陷入沉思:既然可以設置背景圖,那么用純色去填充生成UIImage對象,然后再利用該方法不就解決了問題嗎?思路很簡單,實現很明了:
//KPAppImage
+ (UIImage *)createImageWithColor:(UIColor *)color size:(CGSize)size {
CGRect rect = CGRectMake(0.0f, 0.0f, size.width, size.height);
UIGraphicsBeginImageContext(rect.size);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, [color CGColor]);
CGContextFillRect(context, rect);
UIImage *theImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return theImage;
}
//UINavigationBar+Color
- (void)KPSetBackgroundColor:(UIColor*)color
{
UIImage *img = [KPAppImage createImageWithColor:color size:CGSizeMake(1, 1)];
[self setBackgroundImage: forBarMetrics:UIBarMetricsDefault];
}
嗯,參照視覺大大要求,設置背景色為0xf8f8f8的效果新鮮出爐!
呃,怎么感覺有那么一絲不對勁?:sweat:
毋需動用視覺大大的像素眼,我們已經發現了端倪!
這不是列表支持下拉刷新的loading indicator嗎,居然還薄紗披身半遮面……
哎,不用多說,八成還是translucent屬性搞的鬼!
立馬把這些可惡的UINavigationBar的translucent屬性改為NO,沒想到不一會各路bug如:snowflake:般翩然而至:“消息頁面怎么導航欄底下留了一塊空白?”“為啥頁面排版都錯亂?”“iOS同學你們在搞神馬,沒有bug都改出bug了!”……
飽含著眼淚的程序猿啊,默默的把代碼回滾了……哎,剛剛還提醒大家注意呢,結果自己踩進大坑,各路UINavigationController translucent屬性設置不一致的歷史遺留問題太可惡了,任務這么緊急,可再不敢隨便改動了。那怎么才能讓效果過得了視覺大大的像素眼呢?只好再去瞅瞅UINavigationBar的視圖層級結構,看看有啥治病偏方木有
洞天石扉,訇然中開
4. 探查UINavigationBar View Hierarchy(before iOS10 vs iOS10)
借助Xcode View Hierarchy Debug工具,查看僅通過方案3設置背景色且translucent屬性為YES的UINavigationBar對象,可得iOS10之前的視圖結構如下:
而iOS10的UINavigationBar視圖結構如圖:
同時打印選中的UIImageView對象其description如下,可見是默認其alpha通道非1.0即略微透明:
<UIImageView: 0x7fa87ec42d60; frame = (0 0; 320 64); alpha = 0.909804; opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x60000042e060>> - (null)
當然,如果關閉translucent屬性,自然不會有導航欄透視的問題,但假如是存在前述的“歷史遺留問題”,這種修正影響頁面會比較龐多的情況,有沒有附帶傷害更小的解決方案呢?
自然是天無絕人之路,程序猿們總能想出一些詭計來實現期望的效果。很明顯,如若避免透視導航欄其層級之下內容,讓其渲染背景色的視圖(before iOS10:_UINavigationBarBackground, iOS10:_UIBarBackground)以及其子視圖不透明就可以了嘛,那么自然會有兩個方案:
- 1.把這些alpha值不為1.0的控件設置為不透明
- 2.利用別的視圖遮蔽掉這些透明控件
很不幸的是,我對方案1的嘗試失敗了,特別是有設置backgroundColor的情況下,UINavigationBar其subviews(或迭代包含的subviews)還包括了_UIBackDropView(before iOS10)、UIVisualEffectView(iOS10)等模糊效果控件,對其設置alpha或背景色效果可能無效甚至表現有問題,故此方案暫告一段落。
那么方案2的表現呢?
啊哦,終于達成了為導航欄完美設置0xf8f8f8背景色的需求!
再來剖析一下此時的View Hierarchy
Paste_Image.png
眼尖的同學可能發現,與之前的View Hierarchy相比,_UIBarBackground的subviews中似乎多了一個UIView對象——沒錯,這個UIView對象即擔負著填充背景色且遮擋可能出現模糊透視的任務的關鍵視圖。
Talk is cheap, show me the code~~
- (UIView *)overlay
{
return objc_getAssociatedObject(self, &overlayKey);
}
- (void)setOverlay:(UIView *)overlay
{
objc_setAssociatedObject(self, &overlayKey, overlay, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (void)KPSetBackgroundColor:(UIColor *)backgroundColor
{
if (!self.overlay) {
[self setBackgroundImage:[UIImage new] forBarMetrics:UIBarMetricsDefault];
UIView *backgroundView = [self KPGetBackgroundView];
self.overlay = [[UIView alloc] initWithFrame:backgroundView.bounds];
self.overlay.userInteractionEnabled = NO;
self.overlay.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;
[backgroundView insertSubview:self.overlay atIndex:0];
}
self.overlay.backgroundColor = backgroundColor;
}
- (UIView*)KPGetBackgroundView
{
//iOS10之前為 _UINavigationBarBackground, iOS10為 _UIBarBackground
//_UINavigationBarBackground實際為UIImageView子類,而_UIBarBackground是UIView子類
//之前setBackgroundImage直接賦值給_UINavigationBarBackground,現在則是設置后為_UIBarBackground增加一個UIImageView子控件方式去呈現圖片
if ([currentSystemVersion floatValue] >= 10.0) {
UIView *_UIBackground;
NSString *targetName = @"_UIBarBackground";
Class _UIBarBackgroundClass = NSClassFromString(targetName);
for (UIView *subview in self.subviews) {
if ([subview isKindOfClass:_UIBarBackgroundClass.class]) {
_UIBackground = subview;
break;
}
}
return _UIBackground;
}
else {
UIView *_UINavigationBarBackground;
NSString *targetName = @"_UINavigationBarBackground";
Class _UINavigationBarBackgroundClass = NSClassFromString(targetName);
for (UIView *subview in self.subviews) {
if ([subview isKindOfClass:_UINavigationBarBackgroundClass.class]) {
_UINavigationBarBackground = subview;
break;
}
}
return _UINavigationBarBackground;
}
}
#pragma mark - shadow view
- (void)KPHideShadowImageOrNot:(BOOL)bHidden
{
UIView *bgView = [self KPGetBackgroundView];
//shadowImage應該是只占一個像素,即1.0/scale
for (UIView *subview in bgView.subviews) {
if (CGRectGetHeight(subview.bounds) <= 1.0) {
subview.hidden = bHidden;
}
}
}
設置overlay的設計是比較普遍使用的一種辦法,但上述實現還針對iOS10之前與之后UINavigationBar不同的View Hierarchy去更精準地插入overlay;同時還可以很方便的定位導航欄下方的陰影條,可以隨心所欲地設置其隱藏或顯現。
歸去來兮
綜上所述,僅僅是設置UINavigationBar背景色這么簡簡單單的效果,卻可能會涉及這么多關聯因素。
不得不感嘆,縱然牛逼如蘋果,把UI相關的設計和接口做到純粹的簡約清晰也還是很有難度。
(以上語錄都可以入選裝逼遭雷劈的典型案例了吧?……:flushed:?????)
但是忽然想到,冥冥之中居然還遺漏了UINavigationBar的一個關鍵屬性,即 barTintColor
@property(nullable, nonatomic,strong) UIColor *barTintColor NS_AVAILABLE_IOS(7_0);
Description
The tint color to apply to the navigation bar background.
This color is made translucent by default unless you set the translucent property to NO.
看描述似乎我們上面那一番勞累難道白費了??拿事實說話,來瞧一瞧為translucent為YES的UINavigationBar對象設置其barTintColor為0xf8f8f8 RGB色值后的表現:
效果居然出乎意料的不錯。不過用xscope仔細探查像素發現,雖然顏色看起來差不多,但實際取到色值卻是在0xf4f4f4~0xf9f9f9之間,并不是完全純正的0xf8f8f8。這是怎么回事呢?照舊Debug View Hierarchy大法好~
<font color="#4590a3">(此處以iOS 10作為測試版本)</font>
又出現了UIVisualEffectView這家伙!前面提到過,它是負責產生模糊透明特效的,而View Hierarchy中看到它自己本身是透明的,關鍵還是_UIVisualEffectBackdropView以及兩個_UIVisualEffectFilterView共3個私有子視圖,且明顯可以看出,前者是有實時模糊生成(根據其層級之下的展現內容驗算),中者則是設置了半透明的背景色,后者則為純正的0xf8f8f8顏色但alpha值不為1,這兩者背景色合成之后的結果,才代表最終展現的導航欄顏色(不管你信不信,反正我是暈了)……利用console debug命令探查這三者alpha值、背景色屬性如下
//_UIVisualEffectBackdropView對象
(lldb) po (CGFloat)self.navigationController.navigationBar.subviews[0].subviews[1].subviews[0].alpha
1
(lldb) po self.navigationController.navigationBar.subviews[0].subviews[1].subviews[0].backgroundColor
0x0000000000000000
//第一個_UIVisualEffectFilterView對象
(lldb) po (CGFloat)self.navigationController.navigationBar.subviews[0].subviews[1].subviews[1].alpha
1
(lldb) po self.navigationController.navigationBar.subviews[0].subviews[1].subviews[1].backgroundColor
UIExtendedGrayColorSpace 0.97 0.8
//第二個_UIVisualEffectFilterView對象
(lldb) po (CGFloat)self.navigationController.navigationBar.subviews[0].subviews[1].subviews[2].alpha
0.85000002384185791
(lldb) po self.navigationController.navigationBar.subviews[0].subviews[1].subviews[2].backgroundColor
UIExtendedSRGBColorSpace 0.972549 0.972549 0.972549 1
實事也呼應了barTintColor屬性描述中那段話
This color is made translucent by default unless you set the translucent property to NO.
恍如隔世
這么一番曲折的經歷下來,除了腦袋搞暈了之外,還能得出什么結論嗎?哎,姑且以我的一家之言收尾吧:
想要為UINavigationBar設置某個色值的純色背景,則
-
translucent屬性為YES:
- 如果想要通過視覺大大一絲不茍的像素眼,請參照 4 中的方案2
- 否則若只是要求肉眼不容易發覺(請不要吐槽我的隨便),請利用 barTintColor屬性
- 如果要求放松到無所謂的程度,透不透視都不關心,請使用 setBackgroundImage:forBarMetrics: 方法或者干脆用 backgroundColor屬性
- translucent屬性為NO:
- 如果想要通過視覺大大一絲不茍的像素眼,依舊參照 4 中的方案2
- 否則若只是要求肉眼不容易發覺(請不要吐槽我的隨便),使用barTintColor 或者 setBackgroundImage:forBarMetrics: 方法(仍然存在混色問題,例如設置純紅色,view hierarchy看單個視圖取色為0xff0000,而直接從App中取色則前者效果為0xfb2930,后者效果為0xfc0d1b,相對而言后者更精準)
- backgroundColor設置則是無效的,請放棄該方式
來自:http://www.jianshu.com/p/6a5552ec5099