总所周知,苹果从iOS7开始采用扁平化的界面风格,颠覆了果粉们“迷恋”的拟物化风格。对于开发者而言,全新的风格带来新的接口,这些新的接口改动中,有些更加合理了,有些更加方便了,而有些可能让开发者容易迷糊,下面本人就来谈谈iOS7这些新添加“鬼魅”的接口中的经常接触到的一个----UITabBar/UINavigationBar的translucent属性。
新的属性translucent简介
顾名思义,translucent属性能决定UITabBar/UINavigationBar是否为半透明的效果,苹果官方对此的解释如下:
- UINavigationBar的translucent属性解释
/*
New behavior on iOS 7.
Default is YES.
You may force an opaque background by setting the property to NO.
If the navigation bar has a custom background image, the default is inferred
from the alpha values of the image—YES if it has any pixel with alpha < 1.0
If you send setTranslucent:YES to a bar with an opaque custom background image
it will apply a system opacity less than 1.0 to the image.
If you send setTranslucent:NO to a bar with a translucent custom background image
it will provide an opaque background for the image using the bar's barTintColor if defined, or black
for UIBarStyleBlack or white for UIBarStyleDefault if barTintColor is nil.
*/
@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
- UITabBar的translucent属性解释
/*
Default is YES.
You may force an opaque background by setting the property to NO.
If the tab bar has a custom background image, the default is inferred from the alpha
values of the image—YES if it has any pixel with alpha < 1.0
If you send setTranslucent:YES to a tab bar with an opaque custom background image
the tab bar will apply a system opacity less than 1.0 to the image.
If you send setTranslucent:NO to a tab bar with a translucent custom background image
the tab bar will provide an opaque background for the image using the bar's barTintColor if defined, or black
for UIBarStyleBlack or white for UIBarStyleDefault if barTintColor is nil.
*/
@property(nonatomic,getter=isTranslucent) BOOL translucent NS_AVAILABLE_IOS(7_0);
简而言之,这个BOOL属性能控制UITabBar/UINavigationBar的半透明效果,默认为YES,即默认情况下为半透明效果(后面会详细解释这两段官方文字描述的含义)。
translucent缘何“鬼魅”
新属性的出现往往会导致一些“注意事项”,主要体现为被半透明效果处理后产生的色差和添加的类view控件的坐标变动。
默认情况下,如果使用UITabBarController和UINavigationBarController(translucent属性默认为YES),设置一个蓝色的view添加其中并设置距离屏幕边距为(0,0,0,0),展示效果如下:
可以看到,UITabBarController和UINavigationBarController被蓝色的view“穿透”了,此时view的边距也正如设置的一样,零点坐标在(0,0)处。
这时候,如果将view替换成tableView,展示效果如下:
可以看到,tableView的cell并没有因为“穿透”效果而出现被遮挡的情况,这是由于苹果对滚动视图的特殊性进行处理:对于类ScrollView,系统默认控制器属性automaticallyAdjustsScrollViewInsets默认为YES。
请注意:iOS11开始,苹果摒弃了automaticallyAdjustsScrollViewInsets属性,改由contentInsetAdjustmentBehavior(枚举值)控制,下面会有详细的解释。
-
automaticallyAdjustsScrollViewInsets = YES时系统底层所干的事
scrollView的内容原本没有内边距,但是考虑到导航栏(高度44px)、状态栏(高度20px)、TabBar(高度49px)会挡住后面scrollView所展示的内容,系统自动为scrollView增加上下的内边距,这时候我们打印一下tableView的描述(友情提示:tableView继承自scrollView):
可以看出,contentOffset(内容坐标偏移量)和contentSize(内容尺寸大小)都发生了变化,结合tableView自身的frame可以看出,系统自动为scrollView增加了顶部64px的内边距以及底部49px的内边距,正好是导航栏高度+状态栏高度以及TabBar高度。一旦手动在系统布局页面之前设置automaticallyAdjustsScrollViewInsets = NO,将会取消上述操作,届时scrollView内容将会被部分挡住。
请注意:上述的情况仅仅对UIScrollView或者子类(如UITableView)有效。 contentInsetAdjustmentBehavior定义及使用(适用于iOS11+,替代automaticallyAdjustsScrollViewInsets)
如果只想单纯地设置导航条不偏移导航条+状态栏和Tabbar高度,不想看解释,可以直接使用该宏定义解决方法适配的问题(宏定义来源://www.greatytc.com/p/352f101d6df1):
// 宏定义解析:看UIScrollView实例是否响应setContentInsetAdjustmentBehavior方法,响应则赋值2(2表示枚举值UIScrollViewContentInsetAdjustmentNever),否则设置视图控制器的automaticallyAdjustsScrollViewInsets = NO,_Pragma包括的代码表示消除-Warc-performSelector-leaks警告。
#define adjustsScrollViewInsets_NO(scrollView,vc)\
do { \
_Pragma("clang diagnostic push") \
_Pragma("clang diagnostic ignored \"-Warc-performSelector-leaks\"") \
if ([UIScrollView instancesRespondToSelector:NSSelectorFromString(@"setContentInsetAdjustmentBehavior:")]) {\
[scrollView performSelector:NSSelectorFromString(@"setContentInsetAdjustmentBehavior:") withObject:@(2)];\
} else {\
vc.automaticallyAdjustsScrollViewInsets = NO;\
}\
_Pragma("clang diagnostic pop") \
} while (0)
首先来看下在Xcode 9的SDK中关于原有属性automaticallyAdjustsScrollViewInsets的描述:
@property(nonatomic,assign) BOOL automaticallyAdjustsScrollViewInsets API_DEPRECATED_WITH_REPLACEMENT("Use UIScrollView's contentInsetAdjustmentBehavior instead", ios(7.0,11.0),tvos(7.0,11.0)); // Defaults to YES
// 中文解析:此API已经过期且被UIScrollView下的contentInsetAdjustmentBehavior属性替代
这里描述得很清楚了,iOS11之后该属性过期了!那么接下来我们看看替代属性到底是啥:
/* Configure the behavior of adjustedContentInset.
Default is UIScrollViewContentInsetAdjustmentAutomatic.
中文解析:该属性用来配置UIScrollView调整内边距的行为,其值为枚举值,默认值是UIScrollViewContentInsetAdjustmentAutomatic,就是自动调整。
*/
@property(nonatomic) UIScrollViewContentInsetAdjustmentBehavior contentInsetAdjustmentBehavior API_AVAILABLE(ios(11.0),tvos(11.0));
// 以下是该枚举的具体值(选项)
typedef NS_ENUM(NSInteger, UIScrollViewContentInsetAdjustmentBehavior) {
// 中文解析:与UIScrollViewContentInsetAdjustmentScrollableAxes相似,但为了向后兼容(向低版本兼容),当scroll view被view controller管理,且该view controller的automaticallyAdjustsScrollViewInsets = YES并在导航条控制器栈内,该枚举也会调整顶部(导航条)和底部(Tabbar)的内边距,无论该scroll view是否可滚动。
UIScrollViewContentInsetAdjustmentAutomatic, // Similar to .scrollableAxes, but for backward compatibility will also adjust the top & bottom contentInset when the scroll view is owned by a view controller with automaticallyAdjustsScrollViewInsets = YES inside a navigation controller, regardless of whether the scroll view is scrollable
// 中文解析:滚动轴的边缘会被调整(例如contentSize.width/height > frame.size.width/height 或 alwaysBounceHorizontal/Vertical = YES)
UIScrollViewContentInsetAdjustmentScrollableAxes, // Edges for scrollable axes are adjusted (i.e., contentSize.width/height > frame.size.width/height or alwaysBounceHorizontal/Vertical = YES)
// 中文解析:内边距不会被调整
UIScrollViewContentInsetAdjustmentNever, // contentInset is not adjusted
// 中文解析:内边距总是被scroll view的safeAreaInsets所调整,safeAreaInsets顾名思义就是safeArea的内边距,safeArea下面会有一个概括性的解释。
UIScrollViewContentInsetAdjustmentAlways, // contentInset is always adjusted by the scroll view's safeAreaInsets
} API_AVAILABLE(ios(11.0),tvos(11.0));
safeArea概括性解释:表示一个区域,该区域避开了导航条、Tabbar等可能挡住view controller的view的控件,如果添加一个view控件是相对于它父控件的safeAreaLayoutGuide做布局,则不用担心被导航条、Tabbar等控件“挡住”
-
非滚动视图中导致“鬼魅”的原因
从上述内容中,我们可以了解到滚动视图默认情况下系统会给滚动内容增加上下内边距,以防内容被导航条和TabBar遮挡,但是实际上在常用的项目界面结构(TabBarController--NavigationController--ViewController)以及系统默认情况下,viewController中的view是没有内边距的(也就是说view穿透上下两个Bar)。但是我们有时候会看到应用中会有这样的情形:非滚动视图依然有类似增加上下内边距的效果,自己并没有手动更改过视图的frame。这种情况,就要结合其他几个系统的属性来解释了。
针对非滚动视图的需求
由上述内容我们知道,默认情况下,导航条和TabBar都是半透明,添加在上面的控制器的视图会有“穿透”效果。如果现在有这样的需求:对于非滚动视图,从原点(0,0)布局,但是内容不被遮挡,能够正常显示。
这时候我们需要区分两种情况:是否需要导航条/TabBar带有半透明效果。
-
保留半透明效果(导航条/TabBar的translucent == YES)
- 手动修改frame布局,从(0,64)开始布局,底部同理,需要计算坐标尺寸,比较繁琐。
-
修改viewController的edgesForExtendedLayout属性,edgesForExtendedLayout = UIRectEdgeNone
(备注:设置后,控制器的view的frame的坐标Y增加64px紧挨着navigationBar下方,底部同理,该属性支持iOS7及以后的版本。)
注意:这里虽然看着导航条和TabBar还有半透明效果,但是实际上下面的内容已经无法再”穿透“了。
运行效果如下图所示:
-
不保留半透明效果(导航条/TabBar的translucent == NO)
将TabBar和导航条的translucent属性分别设置为NO,不做任何其他修改,此时bar的背景颜色为默认的白色,且不再有色差,运行效果如下图所示:
关于iOS7之后与view全屏相关的知识点
在iOS7之后,默认情况下,控制器的view是”全屏“的,也就是说,即便有TabBar或者NavigationBar,控制器的view也是可以”穿透至全屏“的,针对这一特性,苹果对UIViewController提供了以下几个属性供开发者使用:
// 在iOS7之前,控制器的view默认非全屏,如果想要全屏效果,需要设置为YES,该属性已从iOS7开始过期
@property(nonatomic,assign) BOOL wantsFullScreenLayout NS_DEPRECATED_IOS(3_0, 7_0) __TVOS_PROHIBITED;
// Deprecated in 7_0, Replaced by the following: 该属性被以下三个属性代替
// Defaults to UIRectEdgeAll
@property(nonatomic,assign) UIRectEdge edgesForExtendedLayout NS_AVAILABLE_IOS(7_0);
// Defaults to NO, but bars are translucent by default on 7_0.
@property(nonatomic,assign) BOOL extendedLayoutIncludesOpaqueBars NS_AVAILABLE_IOS(7_0);
// Defaults to YES
@property(nonatomic,assign) BOOL automaticallyAdjustsScrollViewInsets NS_AVAILABLE_IOS(7_0);
下面解释一下这三个属性:
edgesForExtendedLayout:意思是view的边缘允许额外布局的情况,默认为UIRectEdgeAll,意味着全屏布局(带穿透效果)。
-
extendedLayoutIncludesOpaqueBars:意思是额外布局是否包括不透明的Bar,默认为NO,意味着如果导航条或者TabBar非透明,view内容不会被他们遮挡,如果该属性设置为YES,那么在导航条或者TabBar非透明的情况下,view的内容将会被他们遮挡(原点为0,0),该属性仅仅对非透明的Bar控件有效。
示例展示如下图所示(代码设置 navigationBar.translucent = NO 并且 extendedLayoutIncludesOpaqueBars = YES)
automaticallyAdjustsScrollViewInsets:意思是是否由系统自动调整滚动视图的内边距,默认为YES,意味着系统将会根据导航条和TabBar的情况自动增加上下内边距以防止滚动视图的内容被Bar遮挡。
设置导航条或者TabBar背景图片的注意事项
这里会详细解释官方文档对translucent属性的注释,为什么放到这里才说?因为官方文档对该属性的解释全部跟设置导航条或者TabBar的背景图片有关!说明苹果也知道,这里坑很多,下面就来梳理一下吧。。。
translucent属性的官方解释
- UINavigationBar/UITabBar的translucent属性解释:默认为YES,可以通过设置NO来强制使用非透明背景,如果导航条使用自定义背景图片,那么默认情况该属性的值由图片的alpha(透明度)决定,如果alpha的透明度小于1.0值为YES。如果手动设置translucent为YES并且使用自定义不透明图片,那么会自动设置系统透明度(小于1.0)在这个图片上。如果手动设置translucent为NO并且使用自定义带透明度(透明度小于0)的图片,那么系统会展示这张背景图片,只不过这张图片会使用事先确定的barTintColor进行不透明处理,若barTintColor为空,则会使用UIBarStyleBlack(黑色)或者UIBarStyleDefault(白色)。
设置导航栏背景图片透明度问题
- 如果背景图片没有透明度,系统会自动把导航控制器的栈顶控制器的view的Y值增加64,如果没有透明度,则不会增加。
- 这一情况的图片必须是imageSet格式的,并且图片的透明度为1(不透明),系统才会去调整。