引言: 我相信Android碎片化问题是让所有的Android开发者都比较头疼的问题.尤其是屏幕适配这一块儿.想要自己的app在不同的设备上面都有一个比较好的显示效果.就必须做好相应的屏幕适配.本文是结合网上的相关知识总结、官方文档结合自己的一些理解来进行阐述的.如有不恰当的地方,欢迎斧正.共同学习.
转载请标明出处:http://blog.csdn.net/unreliable_narrator/article/details/51315776
我们先来了解两个概念:屏幕尺寸和屏幕的分辨率:
屏幕尺寸: 就是屏幕的对角线的长度,度量单位是英寸,1英寸等于2.54厘米.
例如小米5的屏幕尺寸就为5.15英寸.nexus 5的屏幕为4.95英寸.
屏幕分辨率: 实际上就是屏幕横纵坐标上面的像素点.如比较常见的1280×720,1920×1080,480*800等等.
那么问题来了:各家厂商生产的手机的分辨率和屏幕的尺寸都不尽相同.那么我们做做出来的app需要在成千上百的不同尺寸不同分辨率的手机上面运行肯定也是会出现显示差异问题.这里将问题归为两类:app在不同的分辨率显示出相同的效果.app在不同的屏幕尺寸上显示相同的效果.
一、解决分辨率碎片化的问题。
我们知道。如果在一块儿大小相同(尺寸)的屏幕上面显示一个宽度为100px的控件,如果在1080*1920分辨率的屏幕上面显示的效果和在分辨率为720*1280分辨率肯定是不会相同的.
那么在相同尺寸但是分辨率不同的屏幕上面如何让控件的大小看起来是一样的呢?这个就是我们先要解决的问题.
首先来了解一些相关的概念:
屏幕像素密度:就是每英寸的像素数,又叫ppi,计算方式如下图:
Px像素:一块显示屏是由很多的光点组成的,每一个光点就是一个像素。例如1280x720屏幕的像素就是921600
dpi: dot per inch,就是每英寸的点数。在电子显示范畴内它和PPI是一个意思。计算方式也是和ppi的计算方式是一样的。 只有在打印时这个缩写才有意义,在打印领域不存在 PPI的叫法,只说DPI,它表示打印机每英寸打印几个像素点。宽高同样像素下,dpi越大,打印出来的图案越小。
dip: 是Density Independent Pixels的缩写,或者叫dp,这是Android开发中特有的一种度量,称作屏幕无关像素, 它不表示任何具体的长度或者像素点。显示的时候根据具体平台屏幕密度的不同最终转换为相应的像素长度,具体转换规则是: 1dp = (目标屏幕密度/标准密度)*px,标准密度为160dpi,例如,1dp长度在密度为160dpi的平台表示一个像素的长度,而在240dpi的平台则表示1.5个像素的长度dpi是屏幕像素密度,假如一英寸里面有160个像素,这个屏幕的像素密度就是160dpi,那么在这种情况下,dp和px如何换算呢?在Android中,规定以160dpi为基准,1dip=1px,如果密度是320dpi,则1dip=2px,以此类推。与像素的换算方式为px=dp*(dpi/160)。
官方文档自备梯子查看:http://developer.android.com/guide/practices/screens_support.html#range
统一化密度
我们先来看一个例子:nexus one,分辨率是480x800屏幕的大小是3.7英寸,我们可以根据上面的公式计算出它的dpi是:约为252,如果按照像素计算公式px=dp*(dpi/160)可得:1.575dp=1px
而屏幕的宽是480px,由此我们可以计算出宽度是约为304个dp.现在我们做一个测试:新建一个View设置背景颜色为红色:宽度为304dp,按照道理来说是应该填满了整个屏幕的宽度,但是实际情况是怎么样的呢?我们来看看效果:
你可能会说wtf.那么这是怎么一回事呢?这里我们就要引入一个统一化密度。
统一化密度: 归一化的密度是有固定值的, 这个固定值是 120dpi(ldpi) , 160dpi(mdpi), 240dpi(hdpi), 320dpi(xhdpi), 480dpi(xxhdpi)。 Android中计算像素经常使用的密度是这五个值之一;
就上面的例子来说我们计算出nexus one的dpi是252,Android系统里面取的是240dpi ,我们再根据公式px=dp*(240/160)可得:1.5dp=1px,再将View的宽度设置为320dp,可以看到如下效果:
结论: dp可以自适应屏幕的密度。不管屏幕密度怎样变化,只要屏幕的物理尺寸不变,实际显示的尺寸就不会变化。在低密度的设备上它的值很小,而在高密度的屏幕上它的值会变得大些,这样一来,开发者就可以指定一个长度从而适应不同密度的屏幕,以解决控件在低密度屏幕变大,高密度屏幕变小的问题.如果屏幕物理不变的情况下设置同一个dp值在480p或者是720p或者是1080p上面的显示长度几乎是一样的.DPI 越高的屏幕,屏幕绘制1dp 需要越多的像素,反之亦然。也就是说使用dp可以让我们的app在大多数的屏幕上保持相近的呈现效果,从而也基本上解决了分辨率碎片化的问题。
(注意)实际上使用dp的话在不同的设备上面的,显示的大小是相差无几的:为什么是相差无几的呢?根据相面所说的统一化密度结合下面的例子就可以很好的理解。
例子:
手机三款:nexus s :4.0英寸 480*800
nexus one : 3.7英寸 480*800
nexus 5 : 4.95英寸1080*1920
根据上面的计算公式:我们可以很快的算出来:nexus s的dpi是 233.2380757937 px/inch
我们可以很快的算出来:nexus s的dpi是 252.1492711283 px/inch
我们可以很快的算出来:nexus 5的dpi是 445 px/inch
nexus s和nuxus one他们都会归一化到240dpi里面对应的换算单位是1.5dp=1px,而nexus 5会归一化到xxhdpi里面,对应的换算单位是3dp=1px 如果是显示50dp的长度则:
100dp在nexus s上面显示的实际像素是 100*1.5=150px
100dp在nexus one上面显示的实际像素是 100*1.5=150px
100dp在nexus 5上面显示的实际像素是 100*3=300px
100dp在nexus s显示的物理像素的长度 150px /233 px/inch= 0.644inch 1.68656厘米
100dp在nexus one显示的物理像素的长度 150px /252 px/inch= 0.595inch 1.5113 厘米
100dp在nexus 5上面显示的物理像素的长度 300px/445px/inch = 0.6741inch 1.712214厘米
通过上面的例子我们可以看出来:在Android设备屏幕显示时,虽然通过归一化密度让两款实际密度不相同的手机都处在hdpi里面,相同dp在两款设备上面的实际像素是一样的,但是因为两台设备的实际物理dpi不一样,所以相同的像素在最终的显示尺寸上是有微弱差别的。当然这种微弱的差别我们基本可以忽略不计。
再来看一个例子。
通过上面的讲解我们可以看到通过设置dp,可以做到在不同像素密度的手机屏幕上面显示的控件大小几乎是一样的大小。但是如果屏幕的物理尺寸相差很大的话又会出现怎么样的情况呢?我们看看接下里的例子:
有一台nexus 7的平板电脑.分辨率是1200*1920 屏幕的大小是7英寸,根据上面的公式我们可以算出它的dpi是:323;属于xhdpi
另外的一台设备是nexus 5的dpi是 445 ;属于xxhdpi.
我们定义一个宽度为120dp的View并设置背景颜色为红色.分别取显示在两种设备上面.
通过上面的图我们可以看到尽管使用了dp使得120dp的View在两种设备上的大小几乎是一样的.但是在nexus 5上面这个View占据了整个屏幕大小的三分之一,而在nexus 7二代上面却只占到了屏幕宽度的五分之一。通过计算我们可以得知nexus 5的宽度是360dp而nexus 7二代的宽度是600dp.所以这也就很好的解释了为什么在nexus 5上面120dp占了屏幕宽度的三分之一而在nexus 7二代上面却只占了五分之一.
通过这个例子我们也可以知道如果屏幕的物理尺寸相差很大的话使用dp就无能为力了.也就是所通过dp我们可以解决Android分辨率碎片化的问题,但是却不能够解决Android物理尺寸碎片化的问题.
dp解决了同一数值在不同分辨率中展示相同尺寸大小的问题(即屏幕像素密度匹配问题),但却没有解决设备尺寸大小匹配的问题。(即屏幕尺寸匹配问题)
二、解决在不同尺寸设备上面的适配.
方案一:使用权重 android:layout_weight ,match_parent(填充父窗体)来达到初步的适配.
权重可简单的看做是按百分比对控件进行分配.android允许layout_weight的值是小数或整数.
需要注意的一点就是权重只能在linerlayout中使用,如果想给两个控件设置为宽度相同,Android推荐的做法是将两个控件的属性这么设置:layout_width=0dp, layout_weight=1,不能设置控件包裹内容否则无法完成正确的适配。
例如:
方案二.使用限定符:
例如标准的7英寸的平板的大小是600dp,这样我们可以通过最小尺寸限定来完成不同尺寸屏幕的适配.在res文件夹下面定义一个layout-sw600dp的文件夹,在里面写上一个例如activity_main的布局文件,当我们的app运行在款或者高超过600dp的设备上面的时候就会去加载layout-sw600dp的布局文件,而在普通的小尺寸设备上面的时候还是会去加载默认的布局.
先编写res/layout/activity_main:
再编写res/layout-sw600dp/activity_main:
我们将该应用程序运行到nexus 5和nexus 7(平板设备)上面看看效果.
通过上面的例子我们可以看到,由于nexus5的最小宽高达不到600dp因此加载的布局是默认的布局.而nexus 7的最小宽高达到了600dp因此展示的是在layout-sw600dp文件夹里面定义的布局文件.
这里需要注意的是:最小宽度限定符仅适用于 Android 3.2 及更高版本 .
此方法也可以限定values资源文件的使用.例如values-sw600dp.那么在程序运行在最小宽度或者是高度为600dp的设备上面时.就会去加载values-sw600dp里面的dimens,color,string等相关资源.
另外我们如果需要对设备的横竖屏适配不同的布局只需要添加layout-sw600dp-land(横屏)和layout-sw600dp-point(竖屏)这两个文件夹里面书写相关的布局文件就可以了.
方案三: 代码动态适配:
既然在代码里面可以获取到屏幕的相关信息,那么我也就可以利用代码获取到的屏幕相关信息给控件动态设置相关的尺寸信息来达到,屏幕的适配.我们可以在第一个activity启动的时候去获取到屏幕的长宽等相关参数.然后在布局文件里面给控件的大小都设置成0dp或者是包裹内容(设置的数据其实没有任何的意义,因为我们需要在代码里面重新去定义控件的大小).
步骤:
1.创建一个类,里面包含两个静态变量即是屏幕的宽高信息.
public class Constant {
public static int displayWidth; //屏幕宽度
public static int displayHeight; //屏幕高度
}
2.然后在第一个activity启动的时候调用如下代码获取到屏幕的相关参数:
mView = findViewById(R.id.v);
DisplayMetrics displayMetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
Constant.displayWidth = displayMetrics.widthPixels;
Constant.displayHeight = displayMetrics.heightPixels;
startActivity(new Intent(MainActivity.this, ActivityTwo.class));
使用动态布局的优缺点如下:
优点:灵活性好。在应用运行过程中,可以随时去调整布局,例如:增加未加载的新控件,调整控件的位置等。
缺点:可见性差,维护非常的繁琐。
方案四:使用百分比库(官方自带的)
步骤:
1.添加gradle支持.
compile 'com.android.support:percent:23.3.0'
2.提供了两种布局供大家使用:
PercentRelativeLayout、PercentFrameLayout,通过名字就可以看出,这是继承自FrameLayout和RelativeLayout两个容器类;
支持的属性有:
layout_widthPercent、layout_heightPercent、
layout_marginPercent、layout_marginLeftPercent、
layout_marginTopPercent、layout_marginRightPercent、
layout_marginBottomPercent、layout_marginStartPercent、layout_marginEndPercent。
3.在使用的时候需要自定义命名空间.写法如下:
方案五:使用第三方框架(鸿洋大神版)
github地址:https://github.com/hongyangAndroid/AndroidAutoLayout
博客地址:http://blog.csdn.net/lmj623565791/article/details/49990941
使用步骤:
1.在gradle里面引入该库.
compile 'com.zhy:autolayout:1.4.3'
2.在清单文件中配置基础参数(也就是UI设计人员给定的尺寸例如我的例子里面是720x1280).
3.在布局文件里面写上UI设计师给定的尺寸参数.单位是px.
4.让activity去继承AutoLayoutActivity就可以了.
public class MainActivity extends AutoLayoutActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
getSupportActionBar().hide();
}
}
如果不想让自己的activity去继承AutoLayoutActivity,可以在编写布局文件时,将
目前支持下列属性:
注意:
默认使用的高度是设备的可用高度,也就是不包括状态栏和底部的操作栏的,如果你希望拿设备的物理高度进行百分比化:可以在Application的onCreate方法中进行设置:
public class MyApplication extends Application
{
@Override
public void onCreate()
{
super.onCreate();
AutoLayoutConifg.getInstance().useDeviceSize();
}
}
补充知识:
1.图片的适配:
默认的我们的app运行在哪个手机上面就会去加载默认的drawable-xxdpi文件夹下面的图片文件,例如,nexus 5如果使用到了图片就会去加载xxhdpi文件夹里面的图片文件.
但是我们不可能为每一套密度的屏幕都去制作一套图片,这样不但是增加了美工的工作量也使得我们的app的体积会过于臃肿.drawable-mdpi、drawable-hdpi、drawable-xhdpi、drawable-xxhdpi。在这些文件下提供的图片大小最好是3:4:6:8:12。
在讲解下面内容的时候我们先来讲讲图片加载规则.举个例子:现在有一个ldpi的手机屏幕,有一个应用在其上运行(假如只有ldpi、mdpi、hdpi还有drawable四个存放图片的文件夹),并需要调用一个图片a.png(在下文中用a来代替a.png)。Android系统会经历以下流程:
结论:根据实际的测量结果也是这样如果在设备对用的dpi的drawable文件夹下面没有对应的图片,那么首先是要去dpi最高的文件夹里面找看是否有对应的图片.如果有则按照相应的缩放比例进行缩放显示,如果没有则到低一级的dpi文件夹里面取查找是否有对应的图片.以此类推.如果在所有的比自己dpi都高的文件夹里面都找不到对应的文件,则去比自己低一级dpi的文件夹里面找相关的资源.如果还是找不到资源就去默认的drawable文件夹里面找,如果drawable文件夹里面还是没有相关的资源那么程序就会报错.
引用自:http://blog.csdn.net/chinalwb/article/details/18843101 (传送门)
2.mimaps文件夹的使用.
补充知识:在Android4.2以上的版本中,提供了对mipmaps的支持,说简单点就是他能对bitmap进行缩放的时候减少一些性能的耗损。如果你用Andorid Studio开发Android程序会发现Android Studio自动帮 你创建了几个mipmaps文件夹,你可将应用的启动图标放到不同的mipmaps文件夹中.所以说我们一般是可以将图标等放到mipmaps文件夹下面.
相关的参考博文:
深入理解dp,px,以及density。
android之屏幕适配之一理论知识
Android是如何在不同屏幕上适配图片的 -- 或控件大小
4种必须知道的Android屏幕自适应解决方案