Android
的屏幕适配一直以来都在折磨着我们这些开发者,本篇文章以Google
的官方文档为基础,全面而深入的讲解了Android
屏幕适配的原因、重要概念、解决方案及最佳实践。
本文转载自《Android屏幕适配全攻略(最权威的官方适配指导)》,内容有删改,笔者在此感谢该博主的无私分享。
第一节 为什么要屏幕适配
在我们学习如何进行屏幕适配之前,我们需要先了解下为什么Android
需要进行屏幕适配。
由于Android
系统的开放性,任何用户、开发者、OEM
厂商、运营商都可以对Android
进行定制,修改成他们想要的样子。但是这种“碎片化”到底到达什么程度呢?
OpenSignalMaps
(以下简称OSM
)发布的Android
碎片化报告,统计数据表明:
- 2012年,支持Android的设备共有3997种。
- 2013年,支持Android的设备共有11868种。
- 2014年,支持Android的设备共有18796种。
下面这张图片所显示的内容足以充分说明当今Android
系统碎片化问题的严重性,因为该图片中的每一个矩形都代表着一种Android
设备。
而随着支持Android
系统的设备(手机、平板、电视、手表)的增多,设备碎片化
、品牌碎片化
、系统碎片化
、传感器碎片化
和屏幕碎片化
的程度也在不断地加深。而我们今天要探讨的,则是对我们开发影响比较大的——屏幕的碎片化。
详细的统计数据请到 这里 查看。
现在大家应该很清楚为什么要对Android
的屏幕进行适配了吧?屏幕尺寸这么多,为了让我们开发的程序能够比较美观的显示在不同尺寸、分辨率、像素密度(这些概念会在下面详细讲解)的设备上,那就要在开发的过程中进行处理,至于如何去进行处理,这就是我们今天的主题了。
第二节 适配哪些设备?
但是在开始进入主题之前,我们再来探讨一件事情,那就是Android
设备的屏幕尺寸,从几寸的智能手机,到10
寸的平板电脑,再到几十寸的数字电视,我们应该适配哪些设备呢?
其实这个问题不应该这么考虑,因为对于具有相同屏幕密度的设备来说,像素越高,尺寸就越大,所以我们可以换个思路,将问题从单纯的尺寸大小转换到像素大小和像素密度的角度来。
下图是2014
年初,友盟统计的占比5%
以上的6
个主流分辨率,可以看出,占比最高的是480*800
(宽*高
),320*480
的设备竟然也占据了很大比例,但是和2015
年的数据相比较,中低分辨率(320*480
、480*800
)的比例在减少,而中高分辨率的比例则在不断地增加。虽然每个分辨率所占的比例在变化,但是总的趋势没变,还是这六种,只是分辨率在不断地提高。
截止至2016年2月14日,我们最低只要需要适配480*800分辨率,就可以在大部分的手机上正常运行了。当然了,这只是手机的适配,对于平板设备(电视也可以看做是平板),我们还需要一些其他的处理。
第三节 重要概念
到目前为止,我们已经弄清楚了Android
开发为什么要进行适配,以及我们应该适配哪些对象,接下来,先要学习几个重要的概念。
屏幕尺寸
屏幕尺寸指屏幕(左上角到右下角)的对角线的长度,单位是英寸(1
英寸=2.54
厘米)。比如常见的屏幕尺寸有2.4
、3.5
、5.0
等。
屏幕分辨率
屏幕分辨率是指屏幕上的像素总数,即屏幕水平方向的像素数*屏幕垂直方向的像素数
。单位是px
,1px
就是指1
个像素点。如480*800
。
屏幕像素密度
屏幕像素密度是指每英寸上的像素点数,单位是dpi
,即“Dots Per Inch”
的缩写。屏幕像素密度与屏幕尺寸和屏幕分辨率有关,在单一变化条件下,屏幕尺寸越小、分辨率越高,它的像素密度越大,反之越小。
px、dp、dip、sp
px
(像素)我们应该是比较熟悉的,前面的分辨率就是用的像素为单位,大多数情况下,比如UI
设计、Android
原生API
都会以px
作为统一的计量单位,像是获取屏幕宽高等。
dip
和dp
是一个意思,都是Density Independent Pixels
的缩写,即密度无关像素,上面我们说过,dpi
是屏幕像素密度,假如一英寸里面有160
个像素,这个屏幕的像素密度就是160dpi
。那么dp
和px
如何换算呢?
在Android
中,规定以160dpi
(mdpi
)为基准,1dip=1px
,如果密度是320dpi
,则1dip=2px
,以此类推。
我们可以通过context.getResources().getDisplayMetrics().density
来在获取当前设备的dppx
参数,即1dp=?px
,然后可以写出如下的代码让实现dp
和px
之间的转换:1
2
3
4
5
6
7
8
9
10
11
12
13// 从dp转成为px
public static int dip2px(Context context, float dpValue) {
float dppx = context.getResources().getDisplayMetrics().density;
// 如果 dpValue * dppx 的结果的小数为是0.5以上,那么此处的+0.5就可以让结果进位,即实现四舍五入的功能。
// dp转px的公式为:px = dp * (dpi / 160),其中“(dpi / 160)”已经由系统帮我们计算好了,它就是dppx。
return (int) (dpValue * dppx + 0.5f);
}
// 从px转成为dp
public static int px2dip(Context context, float pxValue) {
float dppx = context.getResources().getDisplayMetrics().density;
return (int) (pxValue / dppx + 0.5f);
}
sp
,即scale-independent pixels
,与dp
类似,但是可以根据文字大小首选项进行放缩,是设置字体大小的御用单位。
用 dp 而不用 px
假如我们使用px
为单位来画一条长度为320
的横线,程序分别运行在480*800
和320*480
的手机上时,虽然线的长度都是320px
,但是线的视觉效果却不一样:线在480*800
上占据屏幕2/3
的宽度,在320*480
上占满了全屏。
而使用dp作为单位时:
- 线最终绘制到屏幕上的长度是变化的,即随着设备的屏幕密度而变化。
- 即100dp在160dpi的设备上为100px,在320dpi的设备上为200px,若两个设备的屏幕尺寸相同,那么不论二者的分辨率是否相同,100dp长的线在两个设备中看起来是一样长的,虽然它们实际占有的px是不同的。
正因为dp在相同尺寸的设备中显示的效果是一样的,所以在开发中都使用dp而不是px。
dpi 的自动归一
值得注意的是,px = dp * (dpi / 160)
中的dpi
是归一化后的dpi
,而不是手机的实际dpi
值。目前常见的dpi
有:
- ldpi (low) ~ 120dpi
- mdpi (medium) ~ 160dpi
- hdpi (high) ~ 240dpi
- xhdpi (extra-high) ~ 320dpi
- xxhdpi (extra-extra-high) ~ 480dpi
- xxxhdpi (extra-extra-extra-high) ~ 640dpi
比如三星Galaxy S5
的参数为5.1寸、1920×1080、432dpi、3dppx
,假设我现在创建一个宽度为360dp
的背景是红色的TextView
,那么它换算成px
后的宽度应该是360 * (432 / 160) = 972
像素,按道理说TextView
应该填充不满整个屏幕的宽度,但程序运行时却发现它填充满了。这是因为Android
系统并没有拿432
来计算,而是将S5
视作xxhdpi
设备,并使用480dpi
来计算,所以最终是:360 * (480 / 160) = 1080
像素,刚好满屏。
drawable 的 mdpi、hdpi、xdpi、xxdpi
Android
中的drawable
文件夹后面可以跟随一些修饰符,如:hdpi
、mdpi
、ldpi
,这三个文件夹分别放置高清分辨率、中分辨率、低分辨率的资源文件。
当前你的应用在hdpi
的手机上运行,假设代码中加载一张“icon.png”
的图,那么系统首先会去drawable-hdpi
文件夹下去找这张图,一旦找不到,系统会再到其他drawable
下寻找,再假设你其实把这张“icon.png”
放在了drawable-mdpi
中,那么系统会默认把这张图片放大;反之一样,如果你在ldpi
中运行加载一张图片的话,一旦你将图片放入高清的drawable-dpi
中,那么系统默认缩小这张图。
系统为什么要这么做呢?这是因为:
假设我们现在使用ImageView
来显示一张180*180
尺寸的图片,ImageView
的宽高为wrap_content
。
当程序运行在160dpi
的手机上时,图片可以按照正常尺寸(180*180
)显示,但当运行在320dpi
的手机上时,虽然图片仍然是180*180
像素,但是从显示效果上看就比前者缩小了一半。
因此,Android
为了让图片能适应当前设备,默认提供了一个自动缩放图片的机制。
举个例子来说,我们已经知道了三星Galaxy S5
被视为480dpi
以及xxhdpi
,那么此时找一张180*180
的图,把它复制三份并改名,然后放在下面三个目录:
- res\drawable\icon1.jpg
- res\drawable-xhdpi\icon2.jpg
- res\drawable-xxhdpi\icon3.jpg
范例1:布局文件activity_main.xml
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/rootView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:id="@+id/m1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/icon1" />
<ImageView
android:id="@+id/m2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/icon2" />
<ImageView
android:id="@+id/m3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/icon3" />
</LinearLayout>
语句解释:
- 创建垂直排列的三个ImageView控件,它们的尺寸都是wrap_content。
范例2:MainActivity
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public class MainActivity extends Activity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
printSize((ImageView) findViewById(R.id.m1)); // 输出:width = 540, height = 540
printSize((ImageView) findViewById(R.id.m2)); // 输出:width = 270, height = 270
printSize((ImageView) findViewById(R.id.m3)); // 输出:width = 180, height = 180
}
void printSize(ImageView img){
BitmapDrawable d = (BitmapDrawable) img.getDrawable();
System.out.println("width = "+d.getIntrinsicWidth()+", height = "+d.getIntrinsicHeight());
}
}
语句解释:
- 首先,m1所显示的图片icon1被放到了res\drawable目录。在Androi中,“drawable”专门用来存放默认的资源,系统认为该目录下的资源都是基于normal屏幕大小和mdpi密度设计的。由于当前的设备是xxhdpi密度的,但是图却是从drawable目录下找到的,为了显示统一系统会将图片放大3倍,导致图片的宽度和高度都是540。
- 同样的道理,m2所显示的图片icon2被放到了res\drawable-xhdpi目录,系统会从xhdpi目录下找到的图片,宽高放大1.5倍。
- 你可能会疑问系统是怎么决定应该放大或缩小多少倍的? 后面就会详细说明。
事实上,Android
自动缩放图片的机制有一个前提:系统只有在drawable-当前屏幕密度
目录中没找到想要的图片,但是在其它目录下却找到了时,才会执行缩放。即如果系统在drawable-当前屏幕密度
目录中找到了它想要的图片,那么就会把图片原样的显示出来,绝不会执行任何缩放。
因此如果不想让系统自动帮我们缩放图片,我们可以自己来提供多套图片:
- 第一,在res下创建若干目录,比如ldpi、mdpi、hdpi、xhdpi、xxhdpi。
- 第二,在这些目录下放置不同尺寸的图片。
上图表明,如果你在mdpi
密度下有一张48*48
尺寸的图,那么你在hdpi
下的图的尺寸应该是mdpi
的1.5
倍(72*72
),在xhdpi
下图片的尺寸应是mdpi
的2
倍(96x96
),依此类推。
本节参考阅读:
第四节 开始适配
我们已经知道屏幕适配要考虑很多问题,如屏幕尺寸(手机/平板/电视)
、屏幕密度
等,因而也就不可能只通过一种方法来实现所有的适配任务,也就是说我们需要针对不同的问题来使用不同的适配方法。
本节将介绍一些常见的问题以及对应的解决方案。
布局适配
第一,尽量使用wrap_content
和match_parent
来设置尺寸,而不是不写死尺寸,这样可让布局正确适应各种屏幕尺寸
和屏幕方向
。
第二,LinearLayout
的子控件可以使用android:layout_weight
属性来按照比例对界面进行分配。
第三,使用相对布局,禁用绝对布局。
第四,使用.9
图片。
第五,提供备用位图。
就像前面说的那样,如果不想让系统自动帮我们缩放图片,我们可以自己来提供多套图片:
- 第一,在res下创建若干目录,比如ldpi、mdpi、hdpi、xhdpi、xxhdpi。
- 第二,在这些目录下放置不同尺寸的图片。
但是还有个问题需要注意下,.9
图放在drawable
文件夹即可,对应分辨率的图片要正确的放在合适的文件夹,否则会造成图片拉伸等问题。
第六,更多内容请移步《Android屏幕适配全攻略(最权威的官方适配指导)》。