背景
华为对新发布的机器进行适配测试,发现手淘存在全面屏适配问题,随后还附了个3页的文档,文档比较粗泛的描述了一下不适配将会存在的问题,适配可以采取的措施,以及Google开发者文档。简单来说,因为全面屏长宽比大于16:9的标准屏,如果不做全面屏适配,会出现上下方黑边,对于全屏设置背景的图片,可能出现上下拉伸效果,体现到手淘上是这样的:
和手机淘宝的拉伸:
随后贴出了完美适配过的王者荣耀:
打脸啪啪啪啊,再一看适配文档日期,居然这个问题存在了大半年,不禁为用户捏了把汗。。。
再来看下某东的闪屏页,中规中矩,没这个毛病:
再来看下某东的闪屏页,中规中矩,没这个毛病:
淘宝好歹也是大厂,首屏就输给了某东,这个我服~~~~
开始适配
既然问题存在,那就开干吧,按照华为文档的说法是:
也就是说,只要在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,清新而脱俗,视觉效果也不错( 详细文章链接 ),见下图:
(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图片标注好合适的安全区域跟可填充区域。下面来探索下上面提到的三个“合适”。
第三种方案基本思想是使用一张图适配所有的机型。最后的效果实现,其实是利用了安卓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区间的资源文件放置到对应的目录,但图片只有px属性,需要换算。这里我们假设目标机器也是1280 * 720,以HTC One X 为例,斜对角尺寸是4.7 inch,那么PPI就是:
这里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的屏幕。 好了,到这里我们文件夹目录是放对了,如下定义一个主题:
将定义好的Theme在Application或者Welcome页面应用,我们会发现图片被放大,且上下拉伸:
<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>
好了,我们分析下图片的拉伸过程。图片本身是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里面定义属性
查了一下 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款机型的适配测试任务,为了用户体验的极致,我们也算是尽心尽力了。
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