手淘启动页全面屏和虚拟键适配

简介: Android的多屏幕适配一直是困扰开发人员的难题,本文以淘宝启动页适配全面屏为例子,仔细剖析了多屏幕适配的基本原理,希望给大家提供参考。

背景

华为对新发布的机器进行适配测试,发现手淘存在全面屏适配问题,随后还附了个3页的文档,文档比较粗泛的描述了一下不适配将会存在的问题,适配可以采取的措施,以及Google开发者文档。简单来说,因为全面屏长宽比大于16:9的标准屏,如果不做全面屏适配,会出现上下方黑边,对于全屏设置背景的图片,可能出现上下拉伸效果,体现到手淘上是这样的:
随后google了一下全面屏适配,果然发现其他厂商也有同样的问题,比如 小米全面屏适配文档 ,就点名了今日头条的黑边:
和手机淘宝的拉伸:
随后贴出了完美适配过的王者荣耀:
打脸啪啪啪啊,再一看适配文档日期,居然这个问题存在了大半年,不禁为用户捏了把汗。。。 
再来看下某东的闪屏页,中规中矩,没这个毛病:
淘宝好歹也是大厂,首屏就输给了某东,这个我服~~~~

开始适配

既然问题存在,那就开干吧,按照华为文档的说法是:
也就是说,只要在application全局添加android.max_aspect属性即可,so eazy!但随后打开主工程的AndroidManifest.xml,看到android.max_aspect已经存在,时间是2017-06-01。确认了一下google 开发者文档,targetSdkVersion=23是Ok的,也就是说之前做过全面屏适配,只是启动页被忽视掉了。那对于启动页这种纯图片背景的怎么适配呢?在文档中看到这么一句话:
再看一下启动页的代码实现,本质上是定义了一个Theme.Welcome,然后将Theme.Welcome设置到Application,这样就实现了图片打底的特效,研究了一番Theme的适配属性,发现不存在类似于CENTER_CROP效果的(组合)属性。那既然使用ImageView承接可以实现,何不将启动页改造成ImageView来实现闪屏的效果呢?说干就干,写了一个很简单的Demo,设置好ImageView的属性,如下:
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/aab"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:contextClickable="true">

    <ImageView
        android:id="@+id/image_background"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="centerCrop"
        android:src="@drawable/taobao_launch_origin"/>
</FrameLayout>
显示效果:
整体看起来这个效果比拉伸和黑边好多了,但是仔细看下启动过程,点击icon之后会先白一下,随后才看到欢迎页,而这种启动白屏会带给用户启动的迟钝感:
研究了一下,发现启动的之所以会出现白屏和黑屏,原因是系统Launcher启动淘宝第一个Activity过程中会有个耗时,而此时设置的主题是纯色的系统主题,并没有有效可见的画面,而这个白屏和黑屏的时长,会随着应用Application的耗时变化而变化。市面上有不少的App依然采用的这种方案,比如我们熟知的某乎,启动也是白屏一段时间。
动画里的白屏时间是Demo展示的,实际上手淘由于Application较重,加上有外链回流等各种情况,用户感知的白屏时间会更长。到这里,基本上宣告ImageView填充的方式失败,也可以跟华为的适配文档说拜拜了。

重整方案

既然ImageView的方案不可行,那就重新探索一份方案吧,调研了一下适配全面屏和虚拟键的闪屏方案,发现可以有下面三种方式: 
(1)使用多套图适配不同尺寸手机。利用安卓自动资源匹配的优势,给不同屏幕提供不同比例图片,对应的三星S8应该提供一套drawable-long的启动图。 
(2)使用layer-list自定义drawable + Theme 来动态布局的形式。如果将淘宝启动页画面切割为上下两个区域,这种形式可以实现淘宝首屏素材的动态布局,一个居中,一个靠下,利用自定义drawable自动适配的特性,就能适配到大部分机型。 
(3)使用.9 drawable + Theme的形式来定义启动界面。.9 负责实现画面的填充。 
第一种方案首先毙掉,作为一个体量这么大的App,为了适配首屏加这么多图片,包大小管不住了要,架构组的KPI保不住了要,而且这样做无非是增大了UED同学的切图工作量,是一条最简单的路,但效果却不是最优。 
说说第二种方案,在网上看到这个方案的时候,作者用youtube app举例,中间一个小icon,底图是Google 的logo,灰色背景上面一个播放icon,清新而脱俗,视觉效果也不错(
详细文章链接 ),见下图:
不过我们仔细看下淘宝的启动页,最下面是“阿里云提供云计算”,中间是淘宝的Logo抽象出来的小盒子,主体部分是从小盒子里面“腾飞”出来的五彩斑斓的物品,一张图解释了什么叫万能的淘宝。回到手淘的情况,我把启动图片分成两段,上部分居中,下部分靠下,结果整体去看“万能的淘宝”,发现整个上部分是空白,而下部分在小屏手机上会交错到一块,总之两部分画面的协调不是很好。如果要继续做的话,恐怕是需要UED同学重新设计首屏画风了。 
第三种方案基本思想是使用一张图适配所有的机型。最后的效果实现,其实是利用了安卓Bitmap的预缩放 + .9的填充,具体做法是,提供一张合适大小的.9 图片,放置到合适density的目录,然后在.9图片标注好合适的安全区域跟可填充区域。下面来探索下上面提到的三个“合适”。

适配探索

Android的“碎片化”是业界皆知的,所谓的碎片化就是指因为Android开放的特性,导致各个厂商定制了不同版本的Rom,定制了不同版本的屏幕尺寸。所以相对于iOS应用开发者,Android应用开发者在适配问题上耗费的精力会更多一些。不过Android在设计之初就考虑到了这个问题,并且方案在不断完善中,我们需要做的就是了解其中原理,并选择合适的方案和充分的测试去保证适配的成功。

(1)选择合适尺寸的图片

首先我这边拿到的原图是一张720px * 1280px的png图片,我们在建立项目的时候,一般会有下面几组drawable文件夹:drawable, drawable-hdpi, drawable-xhdpi, drawable-xxhdpi, drawable-xxxhdpi,有时候我们为了适配特殊情况,可能还会加入drawable-long, drawable-nodpi,那这么多文件夹,图片应该放到哪个呢?我们来看看 Google的全屏幕适配标准
PS:这里Google开发者文档里面的dpi的概念实际上是ppi的误用,可以参考: ppi-vs-dpi-whats-the-difference  。关于 dpi, ppi, px, dp, dip, sp 的概念,建议大家参考这篇文章: http://www.jianshu.com/p/913943d25829  
这里我们看到Google会建议不同dpi/ppi区间的资源文件放置到对应的目录,但图片只有px属性,需要换算。这里我们假设目标机器也是1280 * 720,以HTC One X 为例,斜对角尺寸是4.7 inch,那么PPI就是:
312ppi再对应上面的屏幕适配标准,应该放到xhdpi里面。这里为什么要以真机举例呢,这是因为纯图片尺寸只有像素的概念,单纯给定一张图片,说他的dpi是多少,该放哪个文件夹是没办法决断的,所以给定一张图我们决定要放置到哪个目录,一般会取市面上同分辨率的有代表性的机器,计算出对应的dpi再决定。或者做的更好的是,我们可以有一份市面上主流机型的分辨率,PPI参数汇总,然后决定出什么样的图,放置到哪个目录。这样的话,我们在所有机型上面图片的整体缩放性能开销表现最佳。 为什么这么说,这是因为Android手机在使用drawable创建bitmap的时候,会有个“选择合适图片”的逻辑,首先它会获取设备本身固有的PPI参数,比如HTC One X是312 ppi,那么首先会从xhdpi的文件夹中寻找,如果找到这张图片并且发现分辨率跟设备一致,就不会对图片进行放缩,直接用这张图片覆盖屏幕,而如果没有找到,就会接着从高dpi的文件夹寻找(xxhdpi, xxxhdpi),再找不到就会从nodpi寻找,其次是hdpi -> mdpi -> ldpi。如果寻找的不是对应dpi目录的图片,会对图片进行一次放缩,放缩的scale = 设备自身density / 资源目录density,这样高分辨率的图放到低dpi的目录,会导致bitmap内存占用的增加,参考: 你的Bitmap到底占用多大内存 。而从高dpi的目录找到一张低dpi的图片,又会导致图片被压缩,在不带其他参数的情况下,会导致图片填不满目标区域。

(2)9-Patch的适配

那现在回到适配三星S8的事情上来,三星S8的分辨率是2960x1440(18.5:9),斜对角尺寸是5.8 inch,那么对应的ppi应该是567.5 ~= 568ppi,跟官方计算出来的参数一致,假设现在市面上80%都是三星S8手机,我们应该设计一张2960x1440的图片,放置到xxxhdpi目录下,这样子可以减少额外的图片放缩。但目前市面上大部分手机还都是16:9的屏,所以现在比较好的做法是采用16:9的图去适配三星18.5:9的屏幕。 好了,到这里我们文件夹目录是放对了,如下定义一个主题:
<style name="AppTheme.Splash" parent="@style/Theme.AppCompat.Light.NoActionBar">
    <item name="android:windowFullscreen">true</item>
    <item name="android:windowBackground">@drawable/taobao_launch</item>
</style>
将定义好的Theme在Application或者Welcome页面应用,我们会发现图片被放大,且上下拉伸:
好了,我们分析下图片的拉伸过程。图片本身是1280 x 720,放到xhdpi,对应资源density是320(density的定义可以查看Android系统源码:BitmapFactory),目标设备是2960 x 1440,设备是568ppi,对应的density是640,当三星S8找到这张图的时候,会进行scale = 640 / 320 = 2 的放缩,这样放缩后的bitmap是2560 x 1440,也就是说宽刚好覆盖,但高小于2960。这个时候当系统加载这张图片作为windowBackground的时候,发现纵向无法满足会进行自适应拉伸,也就是出现了纵向拉伸的效果。所以为了避免高度不够的情况下出现纵向拉伸,需要定义.9来纵向填充,而不是简单地使用match_parent。

(3)探索合适的可拉伸区域

9-patch是Android支持的一种可伸缩的图片格式,格式跟png是兼容的,基本做法是在上下左右各增加1px的边框,左边和上边定义了可伸缩区域,右边下边定义了内容可覆盖区域(更多可以查看: draw-nine-patch )。为了支持最基本的适配,我们可以在背景图空白区域的上方和左方各打几个点,允许图片拉伸适配,如下图的箭头:
写个demo拿到三星S8上面适配运行一下,内容区域已经不再拉伸了,整体效果也不错:
到这里三星S8 为代表的全面屏适配应该没问题了,为了保险起见,从组内搜刮了其他同学的几台设备一个个测试,但是到Nexus 5的时候,发现下面奇怪的一幕:
哥们,你这脚踩三个透明的虚拟按键,用户就看不出来你被遮挡了吗? 
查了一下
Nexus 5的配置参数 ,这款手机分辨率1920x1080,445ppi,那么对应的targetDensity应该是480,而图片的density是320,也就是1280x720的图片放缩480/320=1.5倍,刚好是1920x1080,此时会铺满屏幕,这样虚拟键就刚好盖住了文字部分。 那有没有其他的办法呢?继续琢磨了一下,发现Android在推出虚拟按键的时候,也提供了一个Api允许我们避开虚拟键区域,只需要在Theme里面定义属性android:windowDrawsSystemBarBackgrounds 为false。这样系统虚拟键弹出来的时候,我们可以只绘制虚拟键上方部分,而虚拟键收起来的时候,我们可以绘制全屏幕。真应了那什么什么门什么什么窗!又因为这是api level 21才引入的属性,所以我们需要建立一个values-v21的文件夹,同时在里面定义如下的Theme:
<style name="AppTheme.Splash" parent="@style/Theme.AppCompat.Light.NoActionBar">
    <item name="android:windowFullscreen">true</item>
    <item name="android:windowDrawsSystemBarBackgrounds">true</item>
    <item name="android:windowBackground">@drawable/taobao_launch</item>
</style>
效果如下:
乍一看好像没啥问题,可是我们再认真看下这张图,下面的阿里云文字貌似显示不全,图片也怀疑被压缩,我们再回忆一下刚才的Nexus参数,包含虚拟键的部分是16:9,但是如果不包含呢?嗯,所以这肯定是被纵向压缩和截断了。Ok,到这里我们解决了纵向横向拉伸,解决了虚拟键遮挡问题,那目前这个问题有没有办法?答案是有,现在是纵向区域过长,那如果我们有一个办法,在纵向区域过长压缩的时候,只压缩空白区域,问题是不是解决了?让我们在仔细的看一眼9-patch的文档: https://developer.android.com/studio/write/draw9patch.html
上面整句话的含义其实我们不用care,但是我们注意一个字眼“scale down”,也就是说Android不但支持小图适配大屏幕,还支持在图片超出之后,对指定的区域进行压缩。这样的话,我们把纵向的空白区域选多一些,那么当纵向高度超出的时候,空白区域会等比例压缩。说干就干,试一下下面这样(为了适配宽屏把横向区域空白也选上,注意看左和上的条状):
再把做出来的这张素材,放到Nexus上面运行(虚拟键弹出),效果完美:
到这里,首页的全面屏和虚拟键适配自测都已经成功了,为了能够覆盖到Android各个尺寸,制定了这次适配测试的标准: 
1)三星,小米,华为的长宽比大于16:9的全面屏手机 
2)屏幕比小于16:9的安卓手机,比如三星的pad 
3)用户机型占比Top 10 的手机 
4)自定义Rom比较深的厂商,比如魅族,vivio,yunos 
测试同学也是非常给力,创建了一个50款机型的适配测试任务,为了用户体验的极致,我们也算是尽心尽力了。

参考文档

Supporting Multiple Screens
Create Resizable Bitmaps (9-Patch files)
小米开发者文档:
splash-screens-the-right-way
你的Bitmap究竟占多大内存?
详解Android开发中常用的 DPI / DP / SP
目录
相关文章
|
2月前
|
前端开发 图形学 开发者
【独家揭秘】那些让你的游戏瞬间鲜活起来的Unity UI动画技巧:从零开始打造动态按钮,提升玩家交互体验的绝招大公开!
【9月更文挑战第1天】在游戏开发领域,Unity 是最受欢迎的游戏引擎之一,其强大的跨平台发布能力和丰富的功能集让开发者能够迅速打造出高质量的游戏。优秀的 UI 设计对于游戏至关重要,尤其是在手游市场,出色的 UI 能给玩家留下深刻的第一印象。Unity 的 UGUI 系统提供了一整套解决方案,包括 Canvas、Image 和 Button 等组件,支持添加各种动画效果。
122 3
|
3月前
|
Android开发
Flutter适配安卓刘海、水滴屏显示全屏
Flutter适配安卓刘海、水滴屏显示全屏
70 2
|
4月前
|
Android开发 Kotlin
kotlin开发安卓app,如何让布局自适应系统传统导航和全面屏导航
使用`navigationBarsPadding()`修饰符实现界面自适应,自动处理底部导航栏的内边距,再加上`.padding(bottom = 10.dp)`设定内容与屏幕底部的距离,以完成全面的布局适配。示例代码采用Kotlin。
127 15
|
4月前
|
Web App开发 编解码
软件开发常见流程之兼容性和手机屏页面设计,PC端和移动端常见浏览器,国内的UC都是根据Webkit修改过来的内核,开发重点关注尺寸,常见移动端尺寸汇总,移动端,理想视口根据你设别的样式进行修改
软件开发常见流程之兼容性和手机屏页面设计,PC端和移动端常见浏览器,国内的UC都是根据Webkit修改过来的内核,开发重点关注尺寸,常见移动端尺寸汇总,移动端,理想视口根据你设别的样式进行修改
|
5月前
|
JavaScript 前端开发
技术好文共享:移动端事件(二)——移动端滑屏切换的幻灯片
技术好文共享:移动端事件(二)——移动端滑屏切换的幻灯片
21 0
|
6月前
|
定位技术 iOS开发
自动布局xib页面的机型匹配精典问题及解决方案
自动布局xib页面的机型匹配精典问题及解决方案
41 0
|
6月前
|
数据挖掘 数据处理 API
使用TransBigData组件实现个人手机定位功能
使用TransBigData组件实现个人手机定位功能
112 0
|
iOS开发
iOS - 个人中心果冻弹性下拉动画
iOS - 个人中心果冻弹性下拉动画
252 0
iOS - 个人中心果冻弹性下拉动画
|
API
华为快应用-怎么隐藏原生导航条
华为快应用-怎么隐藏原生导航条
204 0
华为快应用-怎么隐藏原生导航条
|
前端开发
前端移动端开发中对手机机型的判断
在日常开发中,前端往往需要根据用户的手机系统类型去做相应的操作,执行对应的代码。
554 0