适配篇 第一章 屏幕适配

  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*480480*800)的比例在减少,而中高分辨率的比例则在不断地增加。虽然每个分辨率所占的比例在变化,但是总的趋势没变,还是这六种,只是分辨率在不断地提高。

  截止至2016年2月14日,我们最低只要需要适配480*800分辨率,就可以在大部分的手机上正常运行了。当然了,这只是手机的适配,对于平板设备(电视也可以看做是平板),我们还需要一些其他的处理。

第三节 重要概念

  到目前为止,我们已经弄清楚了Android开发为什么要进行适配,以及我们应该适配哪些对象,接下来,先要学习几个重要的概念。


屏幕尺寸
  屏幕尺寸指屏幕(左上角到右下角)的对角线的长度,单位是英寸(1英寸=2.54厘米)。比如常见的屏幕尺寸有2.43.55.0等。


屏幕分辨率
  屏幕分辨率是指屏幕上的像素总数,即屏幕水平方向的像素数*屏幕垂直方向的像素数。单位是px1px就是指1个像素点。如480*800


屏幕像素密度
  屏幕像素密度是指每英寸上的像素点数,单位是dpi,即“Dots Per Inch”的缩写。屏幕像素密度与屏幕尺寸和屏幕分辨率有关,在单一变化条件下,屏幕尺寸越小、分辨率越高,它的像素密度越大,反之越小。


px、dp、dip、sp
  px(像素)我们应该是比较熟悉的,前面的分辨率就是用的像素为单位,大多数情况下,比如UI设计、Android原生API都会以px作为统一的计量单位,像是获取屏幕宽高等。

  dipdp是一个意思,都是Density Independent Pixels的缩写,即密度无关像素,上面我们说过,dpi是屏幕像素密度,假如一英寸里面有160个像素,这个屏幕的像素密度就是160dpi。那么dppx如何换算呢?
  在Android中,规定以160dpimdpi)为基准,1dip=1px,如果密度是320dpi,则1dip=2px,以此类推。


dp与px对比图

  我们可以通过context.getResources().getDisplayMetrics().density来在获取当前设备的dppx参数,即1dp=?px,然后可以写出如下的代码让实现dppx之间的转换:

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*800320*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文件夹后面可以跟随一些修饰符,如:hdpimdpildpi,这三个文件夹分别放置高清分辨率、中分辨率、低分辨率的资源文件。
  当前你的应用在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
18
public class MainActivity extends Activity {

@Override
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下的图的尺寸应该是mdpi1.5倍(72*72),在xhdpi下图片的尺寸应是mdpi2倍(96x96),依此类推。


本节参考阅读:

第四节 开始适配

  我们已经知道屏幕适配要考虑很多问题,如屏幕尺寸(手机/平板/电视)屏幕密度等,因而也就不可能只通过一种方法来实现所有的适配任务,也就是说我们需要针对不同的问题来使用不同的适配方法。
  本节将介绍一些常见的问题以及对应的解决方案。

布局适配


  第一,尽量使用wrap_contentmatch_parent来设置尺寸,而不是不写死尺寸,这样可让布局正确适应各种屏幕尺寸屏幕方向


  第二,LinearLayout的子控件可以使用android:layout_weight属性来按照比例对界面进行分配。


  第三,使用相对布局,禁用绝对布局。


  第四,使用.9图片。


  第五,提供备用位图。
  就像前面说的那样,如果不想让系统自动帮我们缩放图片,我们可以自己来提供多套图片:

-  第一,在res下创建若干目录,比如ldpi、mdpi、hdpi、xhdpi、xxhdpi。
-  第二,在这些目录下放置不同尺寸的图片。

  但是还有个问题需要注意下,.9图放在drawable文件夹即可,对应分辨率的图片要正确的放在合适的文件夹,否则会造成图片拉伸等问题。


  第六,更多内容请移步《Android屏幕适配全攻略(最权威的官方适配指导)》