苹果公司于9月份如期发布了新的iPhone-iPhone8,iPhone8 Plus,iPhoneX,前两个不用多说,正常形态的iPhone和前代外观上没有太大区别。iPhoneX则带来了不同的样式,不同的体验,18:9的全面屏屏幕,小刘海,去掉Home键后超前的交互方式。当然对于开发者也带来了对于这块屏幕的适配问题。我想苹果爸爸决定在11月初开售iPhoneX也有一部分让开发者对自己的App做iPhoneX适配的一部分原因,毕竟现在Xcode已经有iPhoneX的模拟器了。
过去,我们拿到的手机是方方正正的矩形,所以整个屏幕都可以看做是安全区域Safe Area,而如今由于iPhone X屏幕上的“刘海”以及屏幕四周采用圆角的设计,需要设计师对绘图区域做出调整。苹果给出的安全区域如下
页面内容不能超出安全区域(Safe Area)
下面我们以通讯录和News应用为例看下iPhoneX模拟器中原生应用对于这块全面屏如何适配的
通过例子我们可以发现主要的三点原则:
- 带有空间按钮的顶部导航栏(NavigationBar)要处在“刘海”下面
- 底部导航栏(Tabbar)不能在虚拟横条Home键(不知道咋叫,暂且这么叫吧)下面,也就是说要和屏幕底部保持距离
- 可滚动的列表整块屏幕都是可展示的,但是滚动条要和顶部和底部保持距离不能超出
基本以上三点原则可以概括为一句话,所有不可滚动的控件推荐在安全区域内展示,可滚动的控件整个屏幕都可以用来展示
这么做也算是充分利用了这块屏幕,并且不影响用户正常使用iPhoneX了
Toon的适配
初期Toon对于iPhoneX的适配基本为0,所以出现不少问题,主要集中以下几点:
- 顶部导航栏和底部导航栏超出安全区域
- 没有导航栏的列表没有全屏展现
- 吸底按钮超出安全区域
- 顶部导航栏UI错乱
- 列表加载控件在安全区域外部展示
下面通过一个Gif图来看下未经过适配的Toon的部分界面在iPhoneX上表现
可见未经适配的Toon将会以16:9的样子展现在用户手中,这对于产品在iPhoneX中的体验来说将会是极大的灾难,没有充分利用iPhoneX的全面屏,用户体验将是缺失的
经过一段时间的适配,现在开发版的Toon在iPhoneX上已经可以有良好的展示了,虽然还有很多地方没有经过重新设计和优化,不过已经利用了iPhoneX的屏幕展示了
经过一段时间的适配,解决了16:9展示,导航栏错位等问题,在的问题主要集中在列表底部加载控件的问题等问题上,接下来本文将通过Demo和Toon的部分界面来具体讲一下iPhoneX UI适配上的问题
启动页的适配
如果对于启动页不做任何适配,那么App启动后你会发现应用是16:9的样式展示的
解决方案有两种:
- Xib或者Stroyboard来作为应用的启动图
- 添加iPhoneX下启动页的图片
Toon工程中采取的方案是第二种
顶部的适配
以前通过加减20来覆盖或者避免状态的代码都会出问题在iPhoneX上
状态栏高度不是20了,iOS11安全区的提出,在iPhoneX上状态栏的高变为44
代码中需要通过[UIApplication sharedApplication].statusBarFrame.size.height
获取状态栏高度
[self.tableView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.view.mas_top).offset([UIApplication sharedApplication].statusBarFrame.size.height);
make.bottom.equalTo(self.view.mas_bottom);
make.left.equalTo(self.view.mas_left);
make.right.equalTo(self.view.mas_right);
}];
iOS11automaticallyAdjustsScrollViewInsets属性废弃了
会出现ScorllView下沉20的现象
可以调用scrollview新的apicontentInsetAdjustmentBehavior
self.automaticallyAdjustsScrollViewInsets = NO;
if (@available(iOS 11.0, *)) {
self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}
但是这么写会导致在iPhoneX下出现,由于在X下安全区域的出现,顶部异形区域不建议覆盖,会造成视觉的差异
在代码中我们需要来根据设备高度来判断iPhoneX,从而来避免这种情况
[self.tableView mas_remakeConstraints:^(MASConstraintMaker *make) {
if (CGRectGetHeight([UIScreen mainScreen].bounds) == 812.0) {
if (@available(iOS 11.0, *)) {
make.top.equalTo(self.view.mas_safeAreaLayoutGuideTop);
}
} else {
make.top.equalTo(self.view.mas_top);
}
make.bottom.equalTo(self.view.mas_bottom);
make.left.equalTo(self.view.mas_left);
make.right.equalTo(self.view.mas_right);
}];
如果用了MJRefresh在iPhoneX下列表顶部会出现这样的情况,顶部刷新控件会有露出,UI不美观
如果设置contentInsetAdjustmentBehavior
为UIScrollViewContentInsetAdjustmentNever
,并且设置顶部距离为导航栏距离,又会造成全面屏展示不充分也不是很好
- (void)viewDidLoad {
[super viewDidLoad];
<!--省略部分代码-->
if (@available(iOS 11.0, *)) {
self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}
<!--省略部分代码-->
}
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
[self.tableView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.view.mas_top).offset(self.view.layoutMargins.top);
<!--省略部分代码-->
}];
}
我建议的适配方式,根据具体情况来设置contentInset
的值
- (void)viewDidLoad {
[super viewDidLoad];
<!--省略部分代码-->
if (@available(iOS 11.0, *)) {
self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}
<!--省略部分代码-->
}
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
if (CGRectGetHeight([UIScreen mainScreen].bounds) == 812.0) {
// 只在iPhoneX下适配
if (@available(iOS 11.0, *)) {
self.tableView.contentInset = UIEdgeInsetsMake(self.view.safeAreaInsets.top, 0, 0, 0);
}
}
[self.tableView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.view.mas_top).offset(0);
<!--省略部分代码-->
}];
}
使用以上代码,或者UI设计顶部刷新控件都样式都可以解决该问题,但是我觉得最终极的解决方案还是UI设计根据iPhoneX的异性全面屏给以良好的适配方案,如果有更好的设计方案,比如当列表为初始滚动状态时不显示顶部刷新控件,可以跟我交流
顶部的适配问题主要集中体现在以前通过写死状态高度20造成的,对于这个问题,只要调用系统提供获取状态栏高度的方法,就可以避免,至于顶部刷新控件的问题,这个本文建议采取和本文建议的处理底部加载控件的方案来实施,具体可以继续看下文
底部的适配
底部导航栏
如果是采用系统默认的底部导航栏,没有采用自定义的方式,底部导航栏iOS系统级就做了处理,会保证在Tabbar是在安全区域之内
如果是采取自定义的方式那么则要对做出响应的处理
+ (CGFloat)computeTabbarHeight {
NSInteger style = [[TNAppStackManager shareInstance] rootStyle];
if (style == RootControllerStyle_TabCircleDrawer || style == RootControllerStyle_TabCircleNoDrawer) {
return 70.;
} else if (style == RootControllerStyle_TabNormal) {
return [[UIDevice currentDevice] systemVersion].doubleValue >= 11.0 ? (fabs(CGRectGetHeight([UIScreen mainScreen].bounds) - 812.) >= 1.0 ? 49. : 83.) : 53.;
}
return 0;
}
以上是Toon工程在处理底部导航栏高度的示例代码,通过系统版本和设备来判断具体导航栏的高度
列表底部加载控件的的处理
列表的底部加载控件和在全屏下的顶部刷新控件的问题是我认为不不好给出解决方案的问题
iOS11废弃了原有的automaticallyAdjustsScrollViewInsets
属性,为scrollview添加了新的属性
contentInsetAdjustmentBehavior
现在对于我列表的适配,我看大都是这个样子的
self.automaticallyAdjustsScrollViewInsets = NO;
if (@available(iOS 11.0, *)) {
self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
这两个属性都是为了让列表对于全屏和异形屏幕下有良好的展示设计的,对于非全屏状态下的列表,这两个属性不处理没有关系,因为只有在全屏或者半全屏(只有顶部导航或者)
对于Toon来说,刚开我们对于所有的列表都将上面的属性置为了UIScrollViewContentInsetAdjustmentNever
,这样在iPhoneX部分界面就变成这样了
会发现在iPhoneX下,tableView展示区域是到底的,这样会影响用户使用home虚拟横条,所以这个值是需要根据具体情况分析的,例如如果tableView是全屏展示的就需要设置为UIScrollViewContentInsetAdjustmentNever
在此基础上需要适配就是tableView的刷新控件和加载控件了,假设大家使用的都是MjRefresh,那么对于刷新控件出现的问题上文已经讲过了,不在赘述。我们来讨论下加载控件会出现的问题。
刷新控件还好,大部分刷新控件都是在有顶部导航栏的情况下,可是底部加载控件不同,又很多处理方式,本文只做一个抛砖引玉的示例,具体处理方式还是要结合产品、UI、技术来以前讨论针对具体情况具体分析,接下来我将会以Toon中小组模块我的评论界面为例,给出我的解决方案
如果contentInsetAdjustmentBehavior
设置为UIScrollViewContentInsetAdjustmentNever
,那么出现的问题是,底部加载控件会在安全区域意外露出。
为了明显我讲底部加载控件的背景色置成了橙色,可以看到正常情况,加载控件是暴露在安全区域外部,上面的文字也能看到,这样一来既不没关也显得不够专业,并且文字也被home虚拟横条挡住了
那么怎么处理这种情况才会更好些呢,本文给的解决方案是给底部加载控件加一个遮罩,而这个遮罩是根据,tableView的偏移量来展示的,最后的效果如下。
核心代码如下:
- (void)setLoadFooter {
self.tableView.mj_footer = [MJRefreshBackStateFooter footerWithRefreshingTarget:self refreshingAction:@selector(loadCommentData)];
self.tableView.mj_footer.backgroundColor = [UIColor orangeColor];
self.tableView.mj_footer.maskView = [[UIView alloc] init];
self.tableView.mj_footer.maskView.backgroundColor = [UIColor whiteColor];
[self.tableView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if (object == self.tableView && [keyPath isEqualToString:@"contentOffset"]) {
if (@available(iOS 11.0, *)) {
/*
判断设备为iPhoneX时,
并且contentInsetAdjustmentBehavior不为UIApplicationBackgroundFetchIntervalNever
*/
if (CGRectGetHeight([UIScreen mainScreen].bounds) == 812.0 && self.tableView.contentInsetAdjustmentBehavior == UIScrollViewContentInsetAdjustmentAutomatic) {
CGFloat distanceToSafeBottom = (self.tableView.contentOffset.y + CGRectGetHeight(self.tableView.frame) - self.view.safeAreaInsets.bottom) - self.tableView.contentSize.height;
if (distanceToSafeBottom < 0) {
self.tableView.mj_footer.maskView.frame = CGRectZero;
} else {
CGFloat showFooterHeight = distanceToSafeBottom;
if (showFooterHeight > CGRectGetHeight(self.tableView.mj_footer.bounds)) {
showFooterHeight = CGRectGetHeight(self.tableView.mj_footer.bounds);
}
if (self.tableView.mj_footer.state != MJRefreshStateRefreshing) {
self.tableView.mj_footer.maskView.frame = CGRectMake(0, 0, CGRectGetWidth(self.tableView.mj_footer.bounds), showFooterHeight);
}
}
}
}
}
}