手淘Web页面Bar和纵向适配的设计

发布于 大漠

记得在去年双11的互动页面中,我们折腾了一波iPhone8、iPhone8 plus和iPhone X的适配,特别是iPhone X刘海区域的适配。针对这方面的页面适配,沉淀出相应的适配方案。而今年的双11期间,苹果又推出了iPhone XS、iPhone XR和iPhone XR Max以及众多的安卓刘海设备。言外之意,前端在这方面的适配变得越来越复杂。面对众多场景,我们应该怎么去面对呢?接下来聊聊我在今年双11的主互动玩法中是怎么处理的。

面对的场景

在双11主互动玩法(最近手淘热门活动)双11合伙人 组队PK人气 集能量瓜分10亿红包。这是一个纯Web页面,在整个活动页面中有两个场景的适配是较为蛋疼的。首先就是页头和页脚的位置适配。

由于刘海机的增加,顶部和底部的安全区域成为不定因素,所以这一部分面对的难度和不定因素也变多。稍后我们会深度的剖析这部分从设计到开发应该怎么来处理。

另外一个场景是纵向(垂直)方向的适配。

其实纵向的适配一直是全屏模式的一种痛点。在不同高度的终端上如果要达到一样的效果,实现成本是非常高的。至少目前的各种方案都存在这样的问题。

线上最终效果

先来看看养眼的高端机iPhone X 和 iPhone XS Max:

再来看看普通的iPhone设备(iPhone6, iPhone6+, iPhone7+,iPhone8),我身边的设备都跑了一圈:

最后来看看众生相的安卓设备(我只是截取了部分):

是不是顿时感觉前端苦逼。

理解设计

前面的都是一些前奏,似乎有点长,但不要紧,接下来我们进入正题。

在开始编码之前,理解设计是非常的重要,特别是在这样的复杂场景(主要说的是复杂的适配场景)。整个项目的设计无法一一来进行剖析,这里就拿两个特别之处。因为我们要解的是Web页面的Bar,顶部Bar和底部的Bar。先上一张图:

优秀的设计师会给我提供多种状态,或者告诉我不同状态的尺寸。比如分享的页面,其底部就具备两种尺寸:

短屏下是尺寸是750px * 315px,理解成iPhoneX设备之下,都采用的是这个尺寸,当然背景图片也是如此;另外一个尺寸是750px * 448px,对应iPhone X及新出的iPhone XS/XR/XS Max。事实上这样的理解并不完全正确,因为有一些安卓的设备也具备类似于iPhone X的尺寸或更大的尺寸。暂时先忽略这个现象吧。

另外就是页面的顶部Bar和底部Bar的尺寸了。在iPhone X的设计中,顶部Bar的高是128px(设计师告诉我顶部Bar有两种尺寸,iPhone X以下的是128px,iPhone X 及其他带刘海的设备尺寸是176px),底部Bar尺寸是200px

做过iPhone X 适配,都知道有安全区域一说,或者说顶部刘海和底部Home键的位置(大小),但对于新出来的设备(不管是iOS系列还是Android系列)有好多的不确定。就算是抛开这些新设备,也面临着不少的问题。比如说Bar的尺寸,应该怎么确定,是不是跟着设计稿走就行;纵向布局应该怎么处理短屏和高屏。这一切的一切对于前端来说,在不同的项目中都会有未知的问题出现。那么问题来了,我们可以通过设计的标准或者说技术的手段来规避吗?

在我的理解中应该是可以的。为什么这么说呢?我们先来理解设计的标准(不同的设备出现,他都有一套完成的设计标准)。在接下来的内容我们以iOS系列为依据来做判断,因为安卓我真心的惧怕,也没有抽出那么多的时间一款款的测试(这对于做技术的而言是不好的一点)。

理解标准

理解标准需要具备一些基础,比如说一些术语的描述,如果你感兴趣的话,建议你移步阅读一下《移动端上的设计和适配》一文。如果没有这些基础也不会太防碍你继续往下阅读。

在苹果发布新款设备的时候,不管是设计师还是前端开发,都急切的想知道这些设备的各种参数,比如说分辨率、PPI之类的。

这里给大家特别推荐一个网站Vizdevices,可以让大家轻易的获取有关于主流移动设备的各项参数。比如下面的截图:

估计大家更为关心的是如何在项目中适配:切几倍的图用几倍的尺寸和**刘海怎么适配(新机是不是和iPhoneX一样)**等等。到目前为止,最起码在这个项目中,前端和视觉设计师约定的尺寸还是以750px宽度:

而且我们构建工具中有关于单位之间的转换依旧约定的还是以750px为基础。也就是说目前主流还是以@2x为基准做设计稿,然后提供@2x图给前端,当然也有的设计师会额外提供@3x的切图给到开发人员。

手机适配采用几倍图与PPI有关系,也就是像素密度,所以我们可以理解为什么iPhone4、5、6之间分辨率和屏幕尺寸不一样,但是同样采用@2x图的原因,是因为它们有同样的PPI(326ppi。但新款的设备就不一样了:

从上图中我们可以获知,iPhone XS与iPhone X的数据是一致的。因此我们可以得出iPhone XS可以像iPhone X一样,提供@3x的尺寸:

而iPhone XS Max的PPI和iPhone X/XS是一样的,都是458PPI,只是其分辨率和Viewport要更大。由此可以推论出iPhoneXs Max使用的同样是三倍图@3x

从屏幕(Viewport)宽高比例来看:

  • iPhoneXS Max宽度1242/3=414pt,iPhone8 Plus宽度1242/3=414pt,两者的宽度一致
  • iPhoneXS Max高度2688/3=896pt,iPhone8 Plus高度2208/3=736pt

iPhoneXS Max比iPhone8 Plus长一截,多了160pt

因此,我们这次的设计,iPhoneXS Max也跟着iPhone X走的:

接着我们最后来看iPhone XR这款设备。其PPI是326,分辨率为828px * 1792px,屏幕宽度比为:414px * 896px。而iPhone其他款设备,比如iPhone 7 Plus、iPhone 7,甚至iPhone 5,它们的PPI都是326。也就是说iPhoneXR与苹果二倍图的PPI是一致。如此就可以推论出iPhoneXR使用的是二倍图@2x

同样从页面宽高比例来看:

  • iPhoneXR宽度828/2=414pt,iPhoneXS Max宽度1242/3=414pt
  • iPhoneXR高度1792/2=896pt,iPhoneXS Max高度2688/3=896pt

如果你够仔细的话,不难发现,从设备的宽度(Viewport)比来看,iPhoneXR 和 iPhoneXS Max是一样的,如下图所示:

对于设计和开发而言,他们不同之处就是iPhoneXR使用的是@2x图或尺寸,而iPhoneXS Max是@3x图或尺寸:

上面就是有关于苹果新机和老设备之间的差异。但这仅仅是iOS系列,我们还需要面对的是复杂的Android系列,但这一块在这篇文章不做过多阐述,因为我目前还不具备方面能力,能把这里面的故事说清楚。

有了这些基础和概念,我们就可以继续下一步的探讨了。

这些新尺寸对我们Web页面的设计和布局有何影响?如何去适配?

其实这才是我们一线开发人员最为关心的东西。

苹果自从iOS11开始,抛出一个安全区域的概念(其实现在安卓刘海机也有安全区域存在)。对于Web页面设计和开发都有必要对这一概念有所了解。那先花一点时间来了解一下安全区域,如果你对这方面很熟悉,你可以跳过这一节,继续往下阅读。

安全区域

安全区域(Safe Area),一个熟悉又陌生的词语。

熟悉是因为在平面设计中,由于印刷裁切过程中的误差,设计师需要给设计稿预留出「出血」 位置,确保设计内容在安全区域中;陌生又是因为在互联网设计中已极少被提及。

这里指的安全区域不仅仅针对于iOS的设备,只不过以iOS设备为例。所以这里所指的设备安全区域指的是屏幕内适合放置控件的安全区域。

在没有状态栏和其他东西的 iPhone 8 里,Safe Area 是指整个屏幕。

当加入状态栏后,Safe Area 便向下减少了 20pt。当我们加入 Navigation 的时候,Safe Area 又减少了 44pt。同理,我们再加入 Tabbar 的时候,Safe Area 又减少了 44pt(PS:此处更正, Tabbar 高度应该是 49pt)。

在 iPhone X 里,当我们没有使用状态栏时,Safe Area 依然和上下边有一定的距离。按照我的测量,此时距离底部应该是 43pt,距离顶部应该是 44pt

同理,加入不同 Bar 之后,iPhone X 的 Safe Area 都会有相应的变化。

后续设计中,iPhone XS/XR/XS Max可以按这个距离做为设计依据。

遵守 Safe Area (安全区域) 的界定

拿苹果官网针对于iPhone X的安全区域来举例。

状态栏。遵守安全区域的界定,在状态栏下面留出适当的空间。避免为状态栏高度预设值,这可能会导致您的内容被状态栏遮挡或形成错位。

圆弧展示角和传感器槽。您的 App 的内容元素和控制按键应避开屏幕角落和传感器槽,让其在填满屏幕的同时不被角落切割。

主屏幕指示器。为使 App 的内容和控件始终保持清晰可见且便于点按,请确保您的 App 不会干扰主屏幕指示器。

屏幕边缘手势。iPhone X 显示屏利用屏幕边缘手势来访问主屏幕,App 切换器,通知和控制中心。请避免对这些手势造成干扰。您可将控件移到安全区域并调整用户界面。极少数情况下,您可以考虑使用边缘保护:用户的首个滑动手势将被视为 App 内的特定手势,第二次滑动才会被视为系统手势。

确保您的代码能适应不同的屏幕宽高比。许多 App 会根据特定的宽度,高度或宽高比来定位其内容。请检查您的内容是否已正确缩放并定位。

调整视频的缩放度。iPhone X 上的视频内容应填满屏幕。但是,如果这导致顶部或底部被切割,或侧面裁剪太多,则应将视频拉伸或缩小以配合屏幕。当 AVPlayerViewController 自动管理时,基于AVPlayerLayer 的自定义视频播放器需要选择适当的初始视频重力设置,并允许用户根据自己的喜好在 aspect (固定宽高比) 和 aspectFill (固定宽高比且全屏) 观看模式之间进行切换。

安全区域布局

新发布的iPhoneXS、XS Max、XR都采用了全面屏设计,因此我们必须保证布局填满屏幕,并且考虑到交互操作,要留出安全区域,才不会被圆角、刘海影响使用,布局的左右边距可根据产品自定义,这些点与iPhoneX是相同的。

上面提到过,iPhoneXS与iPhoneX尺寸大小完全一致,所以页面布局也是一样的。我们只需要懂得怎样适配到iPhoneXS Max以及iPhoneXR的布局就可以了(两者的的逻辑像素是一致的,均为414pt * 896pt,区别在于一个是@3x,一个是@2x)。

这样设计也更能符合苹果官方所提的设计理念。设计布局要填充整个屏幕,这里有两块区域需要额外考虑:

屏幕顶部,即StatusBar部分

这条状态栏本来并没有可发挥的空间,但是iPhone的StatusBar与NavigationBar(以下简称NavBar)背景是可以通栏的,以达到一种完全沉浸式体验的设计。

大部分的APP应该也是没有影响的(主流NavBar都采用纯色背景,StatusBar背景沿用NavBar的背景),但是对于那些做了NavBar视觉效果的设计师就要考虑了,你的渐变色背景、或者带底纹的背景、还包括电商平台商品图是通栏展示的商品图,多少会对实际效果产生一些影响。

比如,NavigationBar是渐变色背景的,由于iPhoneX/XS/XR/XS Max的Status+Nav高度增加,我们1242 x 192(@3X)的背景图会被等比例拉伸至这两块区域并且剪辑多余部分。

屏幕底部

针对于iPhoneX/XS/XR/XS Max设备,屏幕底部的虚拟区,替代了Home键,高度为34pt

指示灯区域是一个带着系统功能的内容显示区域,这就意味着它可以展示内容;同时如果你的底部是TabBar,那么指示灯区域背景会来自于TabBar背景的延伸;如果我们是一个feed流的页面,底部则会展示次屏feed流的局部。

鉴于圆角、传感器、指示灯区域的影响,iPhoneX/XS/XR/XS Max给出了设计布局的安全区意见:

再考虑必要的NavBar、TabBar,主题内容显示的安全区需要根据设计需求进行考虑。根据实际需要,我们添加的所有控件都应当在安全区内,如各类型的Button、Edit Menu、Pickers、Sliders等等。

从设计的角度做适配

这个时候设计也是痛苦的,其实仔细思考一下,我们还是有一定的应对方案。比如下面的两种方案,都值得我们在平时的设计中做考量。

如果我们在设计的时候以iPhone8(375pt * 667pt)为基准做设计稿,先得到iPhoneXR:由于都是@2x,首先需要将画板宽度拉宽为414pt,高度拉高为896pt(与我们做iPhone5到iPhone6的宽高变化处理是一样的道理),状态栏由20pt变高为44pt,在底部加上主页指示器(Home Indicator)高度为34pt,导航栏以及标签栏高度不变。我们发现iPhoneXR内容呈现的比iPhone8要多一些。

有了iPhoneXR后,直接等比例放大1.5倍就可以得到iPhoneXS Max。

另外一个方案是,我们在设计的时候以iPhoneX(375pt * 812pt)为基准做设计稿,先得到iPhoneXS Max:由于都是@3x,首先需要将画板宽度拉宽为414pt,高度拉高为896pt(与方法一同理),状态栏、导航栏、标签栏、主页指示器的高度均不用更改。有了iPhoneXS Max后,直接等比例缩小2/3就可以得到iPhoneXR,很简单~。

当然,这仅仅是其中两种设计的适配方案,事实上应该还有其他的方案。如果你在这方面有更多的经验或成功的案例,欢迎与在下面的评论中与我们一起共享。

从开发的角度做适配

自从去年过年项目开始,互动前端开发都开始基于vw来进行布局和适配的开发。在构建工具中,我们采用了postcss-px-to-viewport这款PostCSS插件。

// .postcssrc.js

module.exports = {
    "postcss-px-to-viewport": {
        viewportWidth: 750,      // (Number) The width of the viewport.
        viewportHeight: 1334,    // (Number) The height of the viewport.
        unitPrecision: 3,        // (Number) The decimal numbers to allow the REM units to grow to.
        viewportUnit: 'vw',      // (String) Expected units.
        selectorBlackList: ['.ignore', '.hairlines'],  // (Array) The selectors to ignore and leave as px.
        minPixelValue: 1,        // (Number) Set the minimum pixel value to replace.
        mediaQuery: false        // (Boolean) Allow px to be converted in media queries.
    },
}

其中有两个非常关键字段:viewportWidthviewportHeight , 其对应的就是设计稿的长和宽,按照我们以前是以iPhone8的分辨率:750px * 1334px。如果设计稿是按iPhone X进行设计的话,对应的viewportWidthviewportHeight就需要做出相应的调整,将调整为:1125px * 2436px。对应的配置文件更换成:

// .postcssrc.js

module.exports = {
    "postcss-px-to-viewport": {
        viewportWidth: 1125,      // (Number) The width of the viewport.
        viewportHeight: 2436,     // (Number) The height of the viewport.
        unitPrecision: 3,         // (Number) The decimal numbers to allow the REM units to grow to.
        viewportUnit: 'vw',       // (String) Expected units.
        selectorBlackList: ['.ignore', '.hairlines'],  // (Array) The selectors to ignore and leave as px.
        minPixelValue: 1,        // (Number) Set the minimum pixel value to replace.
        mediaQuery: false        // (Boolean) Allow px to be converted in media queries.
    },
}

Web页面的Bar适配设计

前面花了很大的篇幅在探讨有关于设备的标准以及设计适配的处理方案和需要注意的细节。从这里开始,我们就来看看Web页面Bar适配设计和实现细节。

在我们的Web页面,对于Bar而言,常见的情形有两种,Bar是固定的,另外一种是不固定的(随着页面一起滚动的)。而我们这次的项目就是固定的(其实一开始是不固定的)。其实是不是固定并不重要,重要的是我们把设计和实现方案容入到标准中去。只有这样,我们的技术方案才会更适合于各种场景。

回过头来看我们的标准和设计。

简单的理解,不带刘海的设备(iPhone 8Plus 及以下),其StatusBar高度是20pt + 44pt(Status为20pt,NavBar是44pt)。而带刘海的设备(iPhoneX/XS/XR/XS Max),其StatusBar高度变成了44pt + 44pt。这只是针对iOS系列的设备,对于Android系列,这个尺寸目前未知。所以后续聊的尺寸都是以iOS为基准!!!

根据上面的尺寸,就可以理解为,Web页面顶部Bar的尺寸是44pt88px),而前面提到过,设计师告诉我们,iPhone X之下的设备,顶部Bar是64pt128px),iPhoneX之上的设备,顶部Bar是88pt176px)。看到这两组数据,是不是有点晕呼。事实上没有那么复杂,因为设计师提供的尺寸是StatusBar,记住,这个尺寸是StatusBar(状态栏 + Bar)。但是对于我们前端开发而言,所说的Bar就是Bar,并没有包括状态栏。那么问题来了,状态栏有没有高度有没有必要包含进来。如果包含进来,Bar的高度就是128px176px。这就变成两难的竞地。不管选择哪一个值,都不是理想的值,因为这样一来会造成在部分设备中顶部的Bar不是标准的88px,这个项目在部分安卓机就有这方面的现象,比如下图所示:

因此,我把顶部Bar的高度(height)设置为**88px** 。这样做的主要原因是,我不需要纠结是128px还是176px。因为Bar就是Bar,Status就是Status。可能大家会问,全屏的项目,如果高度只设置88px,企不是会被Status遮住。这样的想法是对的,但我们这个时候应该通过别的方式把Status的高度填进来。比如说padding-topmargin-top之类的(填充容器高度的方法有很多种,这仅仅是最简易的方式而且)。当然,也有同学会问,就算是使用padding-top来撑高,以至Bar不会被Status遮住,难道就不会面临padding-top的值是40px还是88px。此时想哭!

其实,这个时候我们应该想到CSS的另一个特性:即**env()** 。该属性以前是iOS的私有属性,现在也被纳入到W3C规范草案中了。我们可以给其传递safe-area-inset-*参数。

有关于CSS的env()函数和safe-area-inset-*相关的介绍这里就不再花篇幅来阐述了,感兴趣的同学,可以阅读《iPhone X的缺口和CSS》一文。

也就是说,我们在设置padding-top的值,不需要显式的设置,可以通过env()函数帮我们自动获取。这个时候,自然能做到不同的设备取得值不同。

.nav-bar {
    height: 88px;
    padding-top: env(safe-area-inset-top);
}

这个方式对于Android目前还不凑效。所以有可能会造成部分Android显示有缺陷,需要设备来进行验证。主要看你的Web页面是不是能穿透到状态栏(Status)。

同样的,对于Web页面底部的Bar也可以使用类似的原理(相对顶部Bar要简单一些,只需要考虑iPhoneX/XS/XR/XS Max)。从上面的图我们可以获得底部Bar高度也是44pt88px),如果底部是Tabbar的话(一般是Tabbar),其高度是49pt96px)。假设你的Web页面是一个Tabbar:

.tab-bar {
    height: 96px;
    padding-bottom: env(safe-area-inset-bottom)
}

现在回到我们的项目中来,先来看设计稿:

通用的底部Bar,其高度是200px(带Home键的设备)。从上面可以获知,底部Home键的高度是34pt68px)。这样一来,对于不带Home键的设备,底部Bar的高度应该是:200px - 68px = 132px。但设计师又告诉我,不带Home键的Bar高度是150px。那么问题来了,这个时候我们就要自己做出选择了。这里说一下我的做法,类似于顶部bar设计,我把height设置为普通设备的高度,也就是150px。对于带Home键的设备,同样借用env()来出处理,所以代码就像下面这样:

.footer-bar {
    height: 150px;
    padding-bottom: env(safe-area-inset-bottom)
}

对于其他页面,其高度也按类似的原理进行处理。说了这么多,我们通过示例来演示一波。

先上二维码,请使用手机淘宝扫下面的二维码:

结构很简单:

<!-- App.vue -->
<template>
  	<div id="app">
        <NavBar />
        <FooterBar />
        <div class="content">
      		<main>
                <div>Main content</div>
                <ul>
          			<li v-for="(item, index) in num" :key="index">{{ index++ }}</li>
                </ul>
      		</main>
        </div>
  	</div>
</template>

大家可以根据自己的需要做出相应的调整,其中有一个细节需要注意的是,固定元素nav-barfooter-bar在主内容.content前面,这主要是为了配合CSS选择器做一些元素控制。

CSS很简单:

// Navbar.vue style
.nav-bar {
    background: orange; 
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 2;
}
.nav {
    height: 88px;
    background: #f36;
    color: #fff;
    font-size: 30px;
    display: flex;
    justify-content: center;
    align-items: center;
}

.nav h1 {
    font-size: 30px;
    margin: 0;
}
@supports (padding-top:env(safe-area-inset-top)){
    .nav-bar {
        --safe-area-inset-top: env(safe-area-inset-top);
        padding-top: var(--safe-area-inset-top);
    }
}

// FooterBar.vue Style
.footer-bar {
    background: green;
    position: fixed;
    left: 0;
    bottom: 0;
    right: 0;
    z-index: 2;
}
.bar {
    height: 150px;
    background: linear-gradient(to bottom, #ff8136 0%, #ff0f4a 100%);
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
}
@supports (padding-top:env(safe-area-inset-top)){
    .footer-bar {
        --safe-area-inset-bottom: env(safe-area-inset-bottom);
        padding-bottom: var(--safe-area-inset-bottom);
    }
}

// App.vue style
body {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
}

#app {
    width: 100vw;
    height: 100%;
}

.content {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: 1;
    overflow-y: auto;
    scroll-behavior: smooth;
    background-color: #fff;
    -webkit-overflow-scrolling: touch;
}

.content > * {
    transform: tranlateZ(0);
    will-change: transform;
}

main {
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 0 16px;
}
.nav-bar ~ .content {
  	padding-top: 88px;
}
.footer-bar ~ .content {
  	padding-bottom: 150px;
}

@supports (padding-top:env(safe-area-inset-top)){
    .nav-bar ~ .content {
        --safe-area-inset-top: env(safe-area-inset-top);
        padding-top: calc(var(--safe-area-inset-top) + 88px);
    }
    .footer-bar ~ .content {
        --safe-area-inset-bottom: env(safe-area-inset-bottom);
        padding-bottom: calc(var(--safe-area-inset-bottom) + 150px);
    }
}

其中关键点的代码:

// NavBar.vue
.nav {
    height: 88px;
    //...
}
@supports (padding-top:env(safe-area-inset-top)){
    .nav-bar {
        --safe-area-inset-top: env(safe-area-inset-top);
        padding-top: var(--safe-area-inset-top);
    }
}

// FooterBar.vue
.bar {
    height: 150px;
    //...
}
@supports (padding-top:env(safe-area-inset-top)){
    .footer-bar {
        --safe-area-inset-bottom: env(safe-area-inset-bottom);
        padding-bottom: var(--safe-area-inset-bottom);
    }
}

// App.vue
.nav-bar ~ .content {
    padding-top: 88px;
}
.footer-bar ~ .content {
    padding-bottom: 150px;
}

@supports (padding-top:env(safe-area-inset-top)){
    .nav-bar ~ .content {
        --safe-area-inset-top: env(safe-area-inset-top);
        padding-top: calc(var(--safe-area-inset-top) + 88px);
    }
    .footer-bar ~ .content {
        --safe-area-inset-bottom: env(safe-area-inset-bottom);
        padding-bottom: calc(var(--safe-area-inset-bottom) + 150px);
    }
}

上面的代码应该不需要做相应的阐述,大家应该都懂的。只不过此处借用CSS条件特性@supports对安全区域做了一些判断,只有支持env(safe-area-inset-*)的设备才能识别这段CSS。如果不这么使用,在Android设备下会出一个Bug,那就是padding-top: calc(var(--safe-area-inset-top) + 88px)会覆盖padding-top: 88px。从而影响.content在Android上的布局显示。

最终的效果如下,先来看iOS系列的效果:

再来看一下Android系列的效果:

不知道这样的效果,您是否能接受。

事实上,上面的代码还可以进行优化,但我把怎么优化的方案和后面的案例结合起来讨论。

纵向适配布局

什么是纵向适配布局?

平时的项目中,或许你碰到过Web页面全屏(关键是单屏)显示,为了让高屏(比如iPhoneX、iPhone XS Max等终端)和短屏(比如iPhone 4、iPhone 5之类的终端)都能较为美观,比如像下面这样的效果(设计师专门为不同的设备做了一些微调):

对于前端开发的同学而言,成本相对而言是要更高的。那么接下来,我们来看看,面对这样的布局,我们又应该怎么去做适配。

拿左侧的页面来举例。同样先上个二维码,使用手机淘宝扫描下面的二维码:

先来看效果:

从左右分别是iPhone 8,iPhone X和iPhoneXS Max上的效果,其他终端的效果就不上图了。感兴趣的可以扫上面的二维码。

简单的说一下,这个页面同样有一个顶部Bar和底部Bar,和前面的页面不同的是,顶部Bar只有一个返回按钮,而底部Bar变得更大了。对于布局的结构是一样的,这里不上代码了。不同的是,底部Bar的代码做了相应的调整:

/*Footerbar style*/
.footer-bar {
    position: fixed;
    left: 0;
    bottom: 0;
    right: 0;
    z-index: 2;
}
.bar {
    height: 315px;
    box-sizing: content-box;
    ...
}

@supports (padding-top:env(safe-area-inset-top)){
    .bar {
        --safe-area-inset-bottom: env(safe-area-inset-bottom);
        padding-bottom: var(--safe-area-inset-bottom);
    }
}

/* App.vue style*/
@supports (padding-top:env(safe-area-inset-top)){
    .nav-bar ~ .content {
        --safe-area-inset-top: env(safe-area-inset-top);
        padding-top: calc(var(--safe-area-inset-top) + 88px);
    }
    .footer-bar ~ .content {
        --safe-area-inset-bottom: env(safe-area-inset-bottom);
        padding-bottom: calc(var(--safe-area-inset-bottom) + 315px);
    }
}

其实顶部Bar和底部bar并没有太多的差异,只是高度的值发生了变化。这个Demo其实并不太复杂,而且差异性也并不大。在这样的环境底部,元素和元素之间的间距我们可以根据屏幕的高度来做相对计算,比如vh。因为不同屏幕的高度是不一样的。比如上例中,.title.share之间有一个间距,从设计稿中可以获知是30px。在以前,我们都是使用的是横向比来做的(根据屏幕的宽度转换成vw),在750px的设计稿,其值将是30 / 7.5 = 4vw。但对于纵向屏幕的适配,这个时候我们应该根据屏幕的高度来进行相对定位,或许更为理想一些,拿iPhone8设计稿来说,他的高度是1334px(对应的是100vh)。同样的,30 / 13.34 = 2.24887556vh

有个细节特别重要,并不是所有元素之间的间距都建议采用vh做单位,大家应该根据自己的设计稿,如果高底屏有较大差异化时,建议采用vh,否则还是更建议使用vw

当然,有的时候采用vh并不一定能完全达到所有设计的需求,那么碰到这样的场景怎么办呢?暴力一点的方案,有的同学采用window.innerWidth / window.innerHeight和一个最佳比例做比较,如果大于这个阈值,给body添加一个类名,然后再对个别元素位置进行调整。对于我而言,我个人更趋向于采用CSS的媒体查询。比如:

@media only screen and (min-width : 375px) and (min-height : 812px) and (-webkit-device-pixel-ratio : 3){
    @supports (padding-bottom:env(safe-area-inset-bottom)){
        .page-invitation-help {
              --safe-area-inset-top: env(safe-area-inset-top);
              padding-top: var(--safe-area-inset-top);
          }

        .page-invitation {
          	margin-top: 140px;
        }

        .page-title {
          	top: -80px;
        }

        .page-invitation .page-footer {
          	bottom: -90px;
        }
    }
}

这种方案令人头痛的或许就是断点的确定。比如iPhoneXS Max可以通过:

@media only screen and (min-width: 414px) and (max-width: 767px),
only screen and (min-height: 896px),
only screen and (min-device-pixel-ratio: 3) {
    /* 对应的样式 */
}

大多数媒体属性可以带有min-max-前缀,用于表达“最低...”或者“最高...”。例如,max-width:12450px表示应用其所包含样式的条件最大是宽度为12450px,大于12450px则不满足条件,不会应用此样式。

因此,对于高屏的设备,不管是iOS系列还是Android系列,我们应该找到一个最佳的临界值。这个估计需要花费一定的时间。不过在vizdevices网站提供了众多主流终端设备的媒体查询语句。感兴趣的同学,或者有心的同学,可以尝试一下。

针对不同屏幕采用不同的位置控制。我目前能想到较好处理高,短屏适配一种方案。

终端安全区域适配总方案

通过前面的两个示例,我们看到了,对于不同的页面或者需求,面对的场景是不同的。为了简化或者说让方案更趋于适用性,我们可以借助CSS自定义属性,把顶部Bar和底部Bar的height:root中定义成两个变量:

:root {
    --navBarHeight: 88px;
    --footerBarHeight: 150px;
}

除此之外,还会使用到顶部安全区域和底部安全区域的高度,同样可以把他们在:root中声明自定义属性:

:root {
    --navBarHeight: 88px;
    --footerBarHeight: 150px;
    --safe-area-inset-bottom: env(safe-area-inset-bottom);
    --safe-area-inset-top: env(safe-area-inset-top);
}

那么对应带有Bar的页面,处理安全区域,咱位就可以这样写:

.nav-bar{
    height: var(--navBarHeight, 88px);
    box-sizing: content-box;
}
.footer-bar {
    hegiht: var(--footerBarHeight, 150px);
    box-sizing: content-box;
}

.nav-bar ~ .content {
    padding-top: var(--navBarHeight, 88px)
}
.footer-bar ~ .content {
    padding-bottom: var(--footerBarHeight, 150px)
}

@supports (padding-top:env(safe-area-inset-top)){
    .nav-bar {
        padding-top: var(--safe-area-inset-top);
    }
    .footer-bar {
        padding-bottom: var(--safe-area-inset-bottom)
    }

    .nav-bar ~ .content {
        padding-top: calc(var(--safe-area-inset-top) + var(--navBarHeight));
    }
    .footer-bar ~ .content {
        padding-bottom: calc(var(--safe-area-inset-bottom) + var(--footerBarHeight))
    }
}

但我们的页面,有时候是没有固定的顶部Bar和底部Bar。换句话说,页面只有.content。即:

<!-- 带Bar的页面模板 -->
<div id="app">
  	<NavBar />
  	<FooterBar />
  	<div class="content"></div>
</div>

变成了:

<!-- 不带Bar的页面模板 -->
<div id="app">
  	<div class="content"></div>
</div>

所以我们的通用解决方案也要做相应的调整:

:root {
    --navBarHeight: 88px;
    --footerBarHeight: 150px;
    --safe-area-inset-bottom: env(safe-area-inset-bottom);
    --safe-area-inset-top: env(safe-area-inset-top);
}
.nav-bar{
    height: var(--navBarHeight, 88px);
    box-sizing: content-box;
}
.footer-bar {
    hegiht: var(--footerBarHeight, 150px);
    box-sizing: content-box;
}

.nav-bar ~ .content {
    padding-top: var(--navBarHeight, 88px)
}
.footer-bar ~ .content {
    padding-bottom: var(--footerBarHeight, 150px)
}

@supports (padding-top:env(safe-area-inset-top)){
    .nav-bar {
        padding-top: var(--safe-area-inset-top);
    }
    .footer-bar {
        padding-bottom: var(--safe-area-inset-bottom)
    }
    
    .content {
        padding-top: var(--safe-area-inset-top);
        padding-bottom: var(--safe-area-inset-bottom);
    }

    .nav-bar ~ .content {
        padding-top: calc(var(--safe-area-inset-top) + var(--navBarHeight));
    }
    .footer-bar ~ .content {
        padding-bottom: calc(var(--safe-area-inset-bottom) + var(--footerBarHeight))
    }
}

总结

文章篇幅有点长,简单的总结一下。文章先从终端设计的标准(iOS终端设备)出发,帮助我们更清楚了了解终端的顶部Bar和底部Bar如何设计更趋于标准化。这样一来,前端开发借助CSS的一些特性,比如通用兄弟选择器(E 〜 F),env()函数,分别对带有Bar和不带有Bar的安全区域控制。对于纵向屏幕的适配,建议在合适的元素之间采用vh做为度量单位,如果高、短屏有较大差异化设计,更建议采用CSS媒体查询来做相应的处理。

如果大家对这方面有更好的建议,欢迎在下面的评论中分享出您的宝贵经验!如果文章中有何不对之处,欢迎各位大婶斧正!(^_^)Adidas Ultra Boost