Android适配:DP简述

3,363 阅读11分钟

上一篇文章发到掘金后,发现省略了内容后,竟然很多人说看不懂;这就尴尬了。我原本觉得DP这个东西说起来实在布一样长,而且是入门的东西,不用在讨论。但发现很多人对DP适配存在误区,尤其还有人任务不同设备屏幕像素不一样需要适配的问题。所以这里说一下DP究竟怎么来的。

像素(pixel)

先直接看Wiki 像素。简单说,像素就是表示一个点的RGB颜色;这个点,是数学上的概念,是没有大小的(回顾一下z初中数学,我没记错吧)。就是说,我们要描述一幅图像(比如清明上河图),可以转换成X*Y像素的位图(Bitmap),但转换后我们是不知道这个Bitmap原来的物理尺寸,就这样丢失了。为什么会这样?因为现实世界是连续的,不可能记录下来(有限的存储空间放不下无限量的数据)。Bitmap也是没有物理大小的。

屏幕分辨率

把Bitmap重新表示出来,我们接触到最多的就是屏幕和打印了。要展示像素,最简单的就是在屏幕上取一个点对应一个像素的点就行了。假如把屏幕分为1080*1920个点,这样同样1080*1920像素的Bitmap刚好展示。屏幕上的点虽然对应像素的点,但它是有物理尺寸的(通过屏幕宽高除以份数得到)。知道了展示规则,虽然像素本身是没有大小的,但我们也可以用像素来表示一个图形展示时究竟有多大。比如一个字体是16px,这不是说它的物理尺寸有多大,而是它显示时会占据多少个点。

体验问题

屏幕点和像素对应,很长时间内都是这样过来的,直到后来手机从奢侈品变成日用品再变成消耗品。手机的普及,每个人整天都抱着手机屏幕盯着看,然后发现了问题:屏幕颗粒看起来太大。屏幕上每英寸上点的数量,我们叫做DPI(dots per inch,因为屏幕上面每个点对应一个像素,所以也有叫PPI;两者大部分情况混用,有时候又不一样,可以参考,不展开讨论)。当DPI小的时候,每个点的物理尺寸就变大(点的大小理解为 DPI 分之一 英寸;比如DPI是160,每个点就是 1/160 英寸);所以要解决屏幕颗粒大的问题,提高DPI值就可以了。但DPI提高后,又出现了另外一个问题:同样像素的Bitmap在新的屏幕上看起来小了。假设把DPI从160提高到320,原来160像素在新屏幕上就只覆盖了1/4英寸。

DP方案

无规矩不成方圆。要完整的解决问题,需要订立新的标准,不能再让屏幕点和像素一一对应了。这里是屏幕展示大小,为什么我们不直接以尺寸为标准呢?简单粗暴直接规定一英寸就是160dp,如果是160dpi的设备,1dp对应一个点(像素);如果是320dpi的设备,1dp对应两个点(像素)。这样我们要描述一个控件究竟多大,原来用像素的地方,就改成DP。比如一个用户头像可以是48dp,表示它的大小是48/160英寸。注意,这里DP只是取代了像素作为描述控件展示大小的作用,实际展示时屏幕上还是点,系统内部Bitmap用的还是Px。使用DP的好处是把控件大小转换为物理上的尺寸,让不同dpi上的控件可以看起来大小一样。
这个DP当然也可以从其他方面理解,原来用px表示大小(一个px对应一个点),现在屏幕dpi提高了一倍,之前一个点的大小现在就要对应4个点。这样用px表示大小就不合适了,得重新取个名字(dp),当然也有叫pt的(iOS大小单位)。不管这个单位叫什么,它表示的都是一个物理尺寸的单位,把物理尺寸大小和和最终展示点的数量进行了剥离。
用DP作为描述大小的单位后,Bitmap展示的问题还没有解决。在160dpi上铺满一平方英寸需要160*160像素,但320dpi上需要320*320像素(依然使用160*160只能铺满1/4,前面提到的问题)。这个问题现在解决起来也简单,我们把160作为标准(mdpi),把320作为两倍图(xhdpi),为每个标准创建一个文件夹,同样的图片在mdpi里面放标准的,在xhdpi放mdpi 2倍大的。再扩展一下,还可以支持0.75倍的,1.5倍的,3倍的,4倍的。 到这里,我们用dp解决了大小单位的表示问题,还兼容了原来的Bitmap展示。DP是Android方案,实际上iOS用的pt也是同样的思路,mmdpi、xhdpi也对应iOS的1倍图、2倍图。需要注意的是,上面说1英寸为160dp,不是强制的,而是灵活的;硬件厂商可以是一定范围为调整,但总的1dp的视觉大小并不会差距太大;这也是DP的出发点,让同样单位(dp)的控件再不同设备上看起来一样大。
就这样,新的单位订好了,也解决了和像素直接的转换问题。

原型设计

引入了DP,这是对开发而已的。对于设计师来说,他面对的仍然是px。对于手机App的设计而已,设计师会取720p、1080p或者750*1334作为原型大小开发。对应到市面上的手机,我们可以直接任务720p、750p是xhdpi的,1080是xxxhdpi的。一个控件的dp单位的大小用它px的大小除以2或者3就可以得到;现在有原型工具也支持这种换算。这可以理解成是一个约定好的尺寸,或者一个实践得出的结果。实际上,设计师也是遵循设计指导的,比如Material Design(虽然官方建议margin padding用8的倍数,但很多设计师用各种奇怪的大小)。

屏幕适配

屏幕适配是任何UI设计需要面对的问题,从移动设备出来之前,PC软件和Web就积累了屏幕适配的方案了。我们先说屏幕适配的来源,再说之前的经验,最后说点实践。
引入了DP概念后,对开发而已,屏幕的大小就是以dp来看的。Android是开放系统,设备众多,比较通用的可以分为两类:手机和平板。这些设备的都是ldpi-xxxxhdpi的(实际上Google自己还弄出了420dpi和560dpi的设备);因为现在UI设计上一般认为垂直方向是可以无限延长的(上下滚动),高是一定满足展示的,更多的我们要注意不同宽度的屏幕适配。以手机为例,一般宽度介于320到411dp之间。这就是说开发是适配手机(不包括平板),布局必须能适应320到411dp之间的任何宽度,这就是开发要面对的适配问题。
在PC软件和Web上,它们不仅要适配不同屏幕的像素,还要处理同个屏幕上父窗口的不同大小;这个在核心上是和移动适配是一致的,但是要不同尺寸的外部约束下,很好的处理内部的控件位置。这个适配处理,实际上是个设计问题。主要的思路是,设计时以屏幕上下左右作为锚点摆放控件,规定好控件的margin和padding,宽高会变化的控件自适应(TextView,各种父布局),宽高固定的控件写死大小(头像控件等)。开发者还原出设计师的设计思路,就能做出满足屏幕适配的布局。这里我强调还原设计思路,而不是还原设计。设计图不能展示在不同尺寸上的效果,但设计思路已经设计到了。还是上一篇文章的例子,顶部的tab设计图上是308dp,设计思路是要表示它距离左右两侧22dp(这样左侧刚好和返回按钮右侧对齐)。

图片来自网络http://www.shui-mai.com/2017/07/11/androidduanuishejiqietubiaozhubanfa/
图片来自网络

在具体实践上,还原一个设计最好的父布局是RelativeLayout,它本身就是表面不同控件之间的位置关系的,和设计思路一致。这里直接实现设计图中列表的item

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:padding="16dp"
    android:background="@color/white"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <ImageView
        android:id="@+id/iv_avatar"
        android:layout_marginRight="15dp"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:scaleType="fitXY"
        android:src="@drawable/img_avatar" />

    <TextView
        android:id="@+id/tv_name"
        android:layout_toRightOf="@+id/iv_avatar"
        android:layout_toLeftOf="@+id/tv_price"
        android:layout_marginRight="15dp"
        android:singleLine="true"
        android:maxEms="8"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tools:text="朱霸天"
        android:textColor="#FF222222"
        android:textSize="15sp"
        />

    <TextView
        android:id="@+id/tv_price"
        android:gravity="center_vertical"
        android:layout_alignParentRight="true"
        android:singleLine="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tools:text="+50"
        android:textColor="@color/red"
        android:textSize="15sp"
        />

    <TextView
        android:layout_alignBottom="@+id/iv_avatar"
        android:layout_toRightOf="@+id/iv_avatar"
        android:singleLine="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="#FF9C9FA2"
        android:textSize="13sp"
        tools:text="2018-08-28"
        />

</RelativeLayout>

上面的代码,在320dp到411dp的手机上都是适配了的;代码也很简单。这里我的确是做了适配的,还原了控件之间的关系(价格在屏幕右侧,标题和时间在头像右侧);设计图上名字“朱霸天”是三个字,实践上TextView都要考虑内容过多换行或挤到右侧的问题(名字挤到右侧红色价格,会重叠),所以需要限制为SingleLine和maxEms,名字控件必须是在价格控件左侧(android:layout_toLeftOf="@+id/tv_price")。

更多

上面说到手机屏幕适配就是适配320到411dp的设备,这个数据可能不准确。手机屏幕众多,这只是我看到的大部分的屏幕。但这个并不影响适配的效果,就算是扩展到480dp甚至平板的600dp,按dp适配也是能兼容的。当然有些效果可能不是太好,这更应该是设计问题,而不是适配问题,毕竟有些UI的确不适合在大屏上显示,内容会显得太空。虽然不是很准确,但知道这个很重要。比如你不能看到一行可以显示50个文字写死了一个TextView的宽度,毕竟你看的效果可能是360dp的,在320dp上一行50个字会放不下。
DP适配后完全不用管屏幕像素也不太准确,毕竟该做的还是要做的。该做的也说过了,给不同xxxxdpi的文件夹放对应的资源文件。
在高度的处理上,前面也说到现在UI一般认为高度是无限延长的。如果是一个特别长的列表,适配时会使用ListView或ScrollView实现无限延长效果。要小心的是一个看起来是一个屏幕大小的布局,不能假设页面一定有640dp高或者其他。本来Android屏幕的高度就不一样;有些设备的系统按键可能是虚拟的,会占据一部分屏幕;Android 添加了分屏效果(multi-window)后,屏幕高度可能不是App窗口的高度了。
DP适配当然不是什么问题都没有。比如实现一个启动页面上放一张满屏的图片,要兼容像素和宽高比例的屏幕的话很难保证不拉伸。这些问题又变成了一个设计问题,我近来看到像网易云音乐或QQ音乐启动广告页底部都留了一截显示App名称,让图片自带宽高比例,这种设计就很好。对于Android而已,整个系统是基于DP机制的,甚至还有sp机制,整个适配也并不复杂。

最后

没有了;看情况待续。