本文主要涉及以下几篇文章
Android 屏幕适配总结
Android ConstraintLayout 使用与适配(使用篇)
Android ConstraintLayout 使用与适配(适配篇)
Android 自动创建最小宽度限定符文件插件 AutoDimens
目录
一、与屏幕相关的概念
1. 屏幕尺寸
2.屏幕分辨率
3. 屏幕像素密度(dpi)
二、为什么需要适配
1.屏幕比例不同(屏幕尺寸不一样)
2.屏幕像素密度(dpi)不同
三、Android 屏幕适配原因
四、官方推荐方式 —— dp
具体分析
五、dp + 修改 density 值适配
一、在 Application 中修改 density 适配
二、在 Activity 中修改 density 适配
三、使用 dp + 修改 density 适配的弊端
六、官方推荐方式 —— 最小宽度限定符适配
一、最小宽度
二、限定符
三、原理解析
四、使用
七、官方推荐方式 —— ConstraintLayout
八、总结
1.建议
2.各适配方式优缺点
与 Android 适配相关的文章有很多,其中也有不少好文章,对我的受益很多,可还是有些点不是很明白,因此自己亲自验证并总结成本文。
屏幕大小单位(如平常说的 55寸电视、5寸手机),统一度量单位 (英寸),测量方式是屏幕对角线的长度为几英寸就是几寸屏。
1英寸=2.54厘米(cm)
屏幕横向、竖向显示的像素数。如分辨率为1080X1920 的屏幕,即为横向1080个像素点,竖向1920个像素点的屏幕。
屏幕每英寸显示的像素点数,在同一尺寸下,分辨率越高,屏幕越清晰,dpi越高。得到屏幕像素密度的计算方式如下
我看到的文章大部分是说 Android 屏幕碎片化严重所以需要适配。可碎片化只是一种客观存在的现象。而 Android 之所以需要屏幕适配的主要原因有两个:
屏幕比例不同,即屏幕宽高比不同,(就是宽高的不同)。屏幕比有很多,如:4:3、5:4、16:10、16:9 等。如在同密度、同尺寸的情况下,屏幕比不同会出现宽高不一至的情况,如下图示例
在同样的5.5寸屏,宽高比分别是 16:9,7:6,最终两个屏幕的大小不一样,这样画一条360px的直线,在图3就是满屏,在图4就只占屏幕的四分之三,而同样的内容可以在图3中显示下,在图4中就有可能超过底部边缘显示不下。这在一些情况下就需要适配屏幕以保证与设计图一致。
屏幕密度不同,即设备分辨率的不同,如在同尺寸的屏幕中,不同分辨率会使屏幕密度不同。如下图示例
在上面两个图中,屏幕的尺寸、比例,都是一样的,但是分辨率是不一样的,这就造成屏幕密度不一样。这样画一条 360px 的直线,在图5中是满屏,在图6中就只有三分之一,在图6中可以显示的内容,在图5中就有可能超过底部边缘,显示不下。这种情况也是造成需要适配的原因。
总结一下适配的 2 个主要原因
1.屏幕像素密度不一样
2.屏幕比例不同(屏幕尺寸不一样)
其实适配,主要是针对这两点进行适配的。理解了屏幕的概念,知道了适配屏幕的原因,这样处理问题就有了方向。下面说说几种官方推荐的主流适配方式,与民间使用比较多的适配方式。
dp 是 Android 里的一个与像素密度无关的单位,因此 Google 推荐使用 dp 解决屏幕像素密度不一样的场景,就是上面所说的第 1 个原因。
dp 为什么可以解决这个像素密度引起的适配问题呢?是因为程序运行后,Android 会根据当前屏幕密度(dpi)将布局文件中的 dp 值转换为 px 值显示在屏幕上。调整公式是:px = dp * (dpi / 160)。
总结:Android 默认会将屏幕密度为 160dpi 的屏幕定为基准(注意:与屏幕分辨、屏幕尺寸没有任何联系,此处不要与其他两个概念联系起来,他们之间没有任何的影响)。这是什么意思呢? 也就是说我们在布局文件中写的 12dp、13dp、等以 dp 为单位的值。Android 默认此布局文件是一个运行在屏幕像素密度为 160dpi 屏幕的布局文件。如果真实运行后屏幕像素密度不是 160 dpi 的屏幕,此时 Android 会以 160 dpi 为基准,将布局文件中 dp 单位的尺寸调整大小,而这个调整的公式就是:px = dp * (dpi / 160)。
这一点我们可以用事实证明一下,在同一分辨率 (360X640),不同像素密度 160 dpi 和 280dpi 的屏幕下,分别画一条 120px 和一条 120dp 的直线,效果如下:
首先看以 dp 为单位的使用结果
图1 中可以证明在像素密度为 160 dpi 的屏幕下,px 单位与 dp 单位是一致的,即 1dp = 1px (说明:这与屏幕分辨率无关,就算是在 1080*1920的屏幕下,只要像素密度是 160dpi,那 1dp 就等于 1px)。
而在像素密度为 280 dpi 的屏幕中 120dp 的直线转换为 px,利用公式:px = 120dp * (280dpi / 160),实际像素是 210px。这就是 图2 中 120dp 实际显示的像素数。
再来看以 px 为单位的使用结果
图1、图2中,在不同像素密度的屏幕下,使用 px 为单位的直线,显示结果居然是一样的(这是句费话),px 本身与像素密度是无关的,在同样大的分辨率(360X640)屏幕下,使用 px 为单位,显示结果当然是一样的。
到此就会产生一种疑惑,不是说 dp 可以解决像素密度不同引起的适配问题吗?为什么在这里还不如 px 呢?这是因为这种示例是一种特殊情况,在广大的手机厂商与海洋般的Android手机机型中,要想使所有手机是同一个分辨率,那是不可能的。一但在不同分辨率下,使用 px 为单位,可想而知,在 360X480 的屏幕 120px 的直线占屏幕宽的 1/3,而在 600X840 的屏幕下就是屏幕宽的 1/6,显示效果立马不同。
而 dp 在众多的机型面前,并不能"完美"按照设计图的样式进行适配,可为什么官方还要推荐呢?我想是因为他可以做到一套标准的适配,什么意思呢?假如你的设计图分辨率是 360x640,DPI 是160 的。则宽、高、DPI 比是 9:16:4,现在只要是以这个比值的屏幕都可以做到完美适配。示例如下
布局代码
此布局分别运行在分辨率:540X960,DPI是240 和 1080X1920,DPI是480的屏幕上适配效果是完全一致的,因为他们的宽、高、DPI比值都是 9:16:4。这一点可以用PS验证,设计图中的矩形是黑色,540X960屏幕布局中的矩形设置为粉色,1080X1920 屏幕布局中的矩形设置为绿色。用PS将两种图片重叠,看是不是百分百对应。结果如下
这种在同比例情况下的适配是完全吻合的,可一但宽高DPI比有一个不同,适配就会产生偏差,如设置一个宽高同样是1080X1920,但DPI为360的屏幕,它的宽高DPI比值就是 9:16:3。与设计图对比结果如下
橙色是运行在屏幕为 1080X1920 DPI360 上的截图,黑色是原设计图,两张图重叠比对可以看到差别很大,所以说单纯的使用 dp 并不能解决适配问题。
民间有一种使用 dp + 修改 density 值的适配方式,也可以近乎完美的适配屏幕。
上边说过只要保持当前设备屏幕的 宽、高、dpi 比值 与 设计图的 宽、高、dpi 比值 一致 就可以完美适配,设备的宽、高是无法修改的,但是 dpi 是有办法修改的。不过这种方式只能以设备的宽或高其中一种方式进行屏幕适配,因为设备的宽高是硬性条件无法修改。因此大多数情况是以设备屏幕的宽进行适配的。例如上面例子中的设备 分辨率是1080X1920 dpi为360 屏幕的 宽与dpi 比是 3:1,而 设计图的 宽与dpi 比是 9:4,要使 设备宽、dpi 比值 和 设计图宽、dpi 比值一致,就要修改设备的 dpi 值,计算公式如下:
densityDpi值 = 设备实际宽 / 设计图宽 x 设计图dpi(默认为 160,根据实际情况调整)
还有一种直接修改 density 值也可使设备与设计图比值一致,
density值 = 设备实际宽 / 设计图宽
修改这两个其中的任何一个,都可以实现不同屏幕的适配。
修改 density 的代码位于 Context 的 getResources() 中,而 Application 和 Activity 都是 Context 的子类,因此这两个类中都可以修改设备的 density 值进行屏幕适配。
注意:此方式有 Android 版本限制,在 5.0 前与 8.0 后无法使用
public class App extends Application {
private static final int designWidth = 360; //设计图宽
@Override
public void onCreate() {
super.onCreate();
//在有些设备程序运行时无法自动调用 getResources 方法,可在此主动调用一次
// getResources();
}
@Override
public Resources getResources() {
int[] screenSize = getScreenSize(this);
int screenWidth = screenSize[0]; //当前设备屏幕宽
float targetDensity = screenWidth * 1.0f / designWidth; //当前设备屏幕宽与设计图宽度比值
int targetDensityDpi = (int) (targetDensity * 160); //根据设备与设计图宽度比值算出当前屏幕适配后的DPI
Resources resources = super.getResources();
/**
* 修改屏幕密度有两种方法
* 一种使用 Configuration
* 另一种使用 DisplayMetrics
* 这两种方法任何一个都可以使用
*/
//使用 Configuration 修改设备密度
Configuration configuration = resources.getConfiguration();
configuration.densityDpi = targetDensityDpi; //设置当前程序的DPI用于屏幕适配
resources.updateConfiguration(configuration, resources.getDisplayMetrics());
//使用 DisplayMetrics 修改设备密度
// DisplayMetrics displayMetrics = resources.getDisplayMetrics();
// displayMetrics.density = targetDensity;
// displayMetrics.densityDpi = targetDensityDpi;
return resources;
}
/**
* 获取屏幕信息
*/
private int[] getScreenSize(Context context) {
int[] size = new int[2];
WindowManager w = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Display d = w.getDefaultDisplay();
DisplayMetrics metrics = new DisplayMetrics();
d.getMetrics(metrics);
size[0] = metrics.widthPixels;
size[1] = metrics.heightPixels;
return size;
}
}
在 Application 中修改 density 的方式在 Android 5.0前 和 Android 8.0 后无法使用。而另一种方式没有版本限制,即在 Activity 中的 setContentView() 前设置 Density,代码如下
1.屏幕适配工具类
public class ScreenUtil {
private static int designWidth = 360; //设计图宽
public static void setDensity(Context context) {
//计算适配后的 density 值
int[] screenSize = getScreenSize(context);
int screenWidth = screenSize[0]; //当前设备屏幕宽
float targetDensity = screenWidth * 1.0f / designWidth; //当前设备屏幕宽与设计图宽度比值
int targetDensityDpi = (int) (targetDensity * 160); //根据设备与设计图宽度比值算出当前屏幕适配后的DPI
//获取 Context 的 Resources
Resources resources = context.getResources();
//使用 Configuration 修改 density
Configuration configuration = resources.getConfiguration();
configuration.densityDpi = targetDensityDpi; //设置当前程序的DPI用于屏幕适配
configuration.fontScale = 1f; //适配字体
resources.updateConfiguration(configuration, resources.getDisplayMetrics());
//使用 DisplayMetrics 修改 density
// DisplayMetrics displayMetrics = resources.getDisplayMetrics();
// displayMetrics.density = targetDensity;
// displayMetrics.densityDpi = targetDensityDpi;
}
public static int[] getScreenSize(Context context) {
int[] size = new int[2];
WindowManager w = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Display d = w.getDefaultDisplay();
DisplayMetrics metrics = new DisplayMetrics();
d.getMetrics(metrics);
size[0] = metrics.widthPixels;
size[1] = metrics.heightPixels;
return size;
}
}
2.在 Activity 中进行适配
public class AutoJavaActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//修改 Density 进行屏幕适配,此方法必需在 setContentView 前执行
ScreenUtil.setDensity(this);
setContentView(R.layout.activity_auto_base);
}
}
以上两种适配后的截图与设计图对比结果如下
修改 Density 进行适配的方式,不需要每个 Activity 都执行适配代码,在整个程序运行期间,只要在入口 Activity 修改一次进行适配就好,其他的 Activity 无须再次修改 Density 值。
1.设计图尺寸无法修改 (注:修改设计图尺寸的情况很少见,在此分析原理与解决方式)
如使用 360X640 做设计图进行开发,一但使用这个尺寸做基准适配,以后设计图必须都要使用这个尺寸做设计,一但设计图的尺寸变更,以前布局文件的所有尺寸值都要按新设计图基准进行修改。或者将新尺寸的设计图换算成原设计图 360X640,换算方式
设计图差值 = 新设计图宽 / 原设计图宽 或 新设计图高 / 原设计图高
新设计图换算后值 = 实际尺寸 / 差值
如:新设计图尺寸是 720X1280
差值 = 720 / 360 = 2
新设计图中一条36dp的线换算后 = 36 / 2 = 18dp
以后都要按这种方式将新设计图中的每个数值除以 2 使用才能与之前的设计图适配对接。
2.不建议将设计图基准尺寸值放在 AndroidManifest.xml 清单文件中。也不建议使用 Application.ActivityLifecycleCallbacks 监听 Activity 生命周期,将 ScreenUtil.setDensity(this); 放在此监听类中进行适配
如果你能确保无论任何时候你的工程不会提供给其他应用做 libary 使用,此建议可省略。否则要重视这个建议,因为有可能其他应用无法使用,原因1清单文件中相同 key 无法合并,原因2 Application 中监听 Activity 生命周期最终只能设置一种基准尺寸的适配。
这个问题根设计图尺寸无法修改类似,如果有多个三方库都使用这样的适配方式,并且多个三方库的设计图尺寸不一样,如一个是 360X640,一个是 750X1280, 一个是 460X720,如果你真遇到这样的情况,恭喜,适配基本无法进行。除非你能要求三方库改设计图尺寸,并且要三方库反工,把以前的布局按你们自己设计图尺寸修改。随着项目越来越大,功能越来越多,集成专业领域三方库的概率也是很大的。所以在选择适配方式时应该考虑这个因素。
3.高分辨率屏幕应该显示更多内容,而不是千篇一律
这种强制将屏幕设置同一比例的方式,使所有屏幕显示效果一模一样,高分辨率屏幕与低分辨率屏幕显示内容一样,高分辨率的存在变得尴尬,用户为什么要多花钱买高分屏呢???
4.宽高无法兼备
这种方式只能以宽或高一种标准来适配,如果对宽高都有要求的场景,将不能使用这种方式适配。
例如还是使用上边的360X640 的设计图,使用此方式适配,在屏幕分辨率为 720X1200 DPI 320 的屏幕下是有问题的,结果如下
很明显在 图2 分辨率为 720X1200 DPI 320 的屏幕下,底部的View 超过了屏幕边缘,这样的场景就不适合使用这样的方式适配,如果需要宽高兼备的适配,只能使用百分比布局 Android ConstraintLayout
5.非官方推荐
官方推出了多种适配方式,就是没推荐这种适配方式,我想官方不主动推荐这种适配方式那肯定是有原因的,不然官方为什么舍简求繁呢?难道官方的开发人员都很傻很闲吗?这种修改 density 适配的方式如果像放在 Application 中修改 density 值一样,哪天有个 Android 版本不支持了,而你维护的项目正好使用这种方式,那你就。。。。。。
最小宽度限定符的适配方式其实是官方使用 dp 值直接适配的升级版本。
我一般把 最小宽度限定符 这段话拆分为二理解:1.最小宽度,2.限定符
最小宽度是指屏幕尺寸的最小一方,如:1080 X 1920 的屏幕,最小值是 1080,则以 1080 为适配尺寸,不管屏幕的方向是横向还是竖向,都以 1080 为适配尺寸的依据。
限定符就是屏幕最小宽度的 dp 值。上边说过屏幕分辨率的单位是 px,而 Android 中的 dp 值最终使用一个公式将 dp 转换成 px 显示在屏幕上,公式是
px = dp * (dpi / 160)
那么要将屏幕分辨率 px 转换成 dp 就需要变换公式 dp = px / (dpi / 160)
举例:假如 屏幕1 是一个分辨率为 1080 X 1920 DPI 为 480 的屏幕,它屏幕的最小宽度是 1080px,DPI 是 480,它的 dp 值就是 dp = 1080px / ( 480dpi / 160) = 360dp,因此 屏幕1 的限定符为 360dp。
官方为什么要使用这个最小宽度限定符做适配呢?我觉得这种方式是直接使用 dp 适配的升级版本,在分析直接使用 dp 值做适配时说过 dp 可以做到一套标准的适配,即只要屏幕的 宽、高、DPI 比值一致,无论在哪个屏幕下,显示效果都是百分百对应的。然而海洋般的 Android 手机制造商中,一套标准只能适配屏幕严重碎片化的一种。因此官方使用一个笨重的方法升级了直接使用 dp 值适配的方式。可能官方想既然一套标准不够用,那我就将一套标准升级成 N 套(即让开发人员指定多个限定符)。N 套中的每一套标准,都可看成 宽 与 DPI 的比值。有了多个同比值,屏幕就可适配的更多。
放到 Android 工程中是不可能使用比值做标准的,因此取用最小宽度限定符做标准,针对每一个最小宽度 dp 值创建一个备用布局文件夹,这个文件夹可以是 values 的,也可以是布局 layout。格式是 values-swXXXdp 或 layout-swXXXdp 如下
这些文件夹都是自己创建的(当然有插件可以一键生成 ScreenMatch),你需要适配哪种屏幕的最小宽度 dp 值,就创建哪个。Android 计算出当前屏幕的最小限定符值去寻找相应文件夹下的布局使用,如果当前屏幕的最小限定符值没有在工程目录中找到,会使用与此值最相近的限定符文件夹中的布局。
当然使用最多的还是 values 文件夹,毕竟适配多个 layout 太费劲了,不过有特殊需求的可以使用。
知道了原理,现在说说怎么使用,你首先需要确定一个最小宽度限定符的基准值,一般就是设计图的尺寸,如你的设计图尺寸是 360X640,那么 360 就是最小宽度限定符的基准值,因为设计图没有 dpi 的概念,因此设计图尺寸 px 与 dp 的关系是 1:1,即设计图尺寸是多少 px,就是多少 dp。
有了最小宽度限定符基准值 360,就以此基准调整各最小宽度限定符布局中的数值,调整公式是
当前最小宽值 / 基准值 * 实际尺寸值
举例:
假如指定 360 为最小宽度限定符基准值,那么最小宽度限定符 values-sw360dp 文件中的值 1dp、2dp、3dp
转换到限定符为 480 的文件夹中, 1dp 就换算成了 480 / 360 * 1dp = 1.3333dp,如下
这样程序运行时计算出当前屏幕最小宽度限定符值,去相应限定符文件 dimens.xml 中寻找指定的值,就可达到适配效果。
见:Android ConstraintLayout 使用与适配(适配篇)
无论使用哪种适配方式,都建议将所有布局页面中的数值 dp、sp、px 等等放在 dimens.xml 文件中,好处就是容易更改其他适配方式。如使用 dp + 修改 density 值适配,哪天这种方式不能用了,转最小宽度限定符适配会很容易,借助工具直接生成相应限定符文件就可以。如果数值写死在每个布局文件中,改起来会相当可观。
1.单独使用 dp 值的使用场景
如果说只想适配市面上的主流机,在主流机型的不同屏幕下,对页面控件的尺寸存在一点误差能够接受。则完全可以只使用 dp 值适配。因为主流机中屏幕的 宽、高、DPI 比差距不会很大。拿常见的左右边距 16dp 来说,在主流机型中不同屏幕差生的差异,普通人眼不会感觉到不适应,不能接受,但是对于专业人来说可能会有美观感的问题。
2.dp + 修改 density 值适配的使用场景
这种方式最大的好处就是方便,并且适配效果比直接使用 dp 要好很多,但是弊端还是要考虑的,如果你能接受他的弊端,这种方式还是比较方便快捷的。
3.最小宽度限定符适配
目前官方推荐的方式,基本不会有后遗症(弃用)的风险,而且很灵活,哪种尺寸的屏幕、哪种情况下的屏幕都可以自己定制布局显示样式。
但是N+1个的限定符文件夹(values-XXXdp),使终是程序员中的一个心病,总感觉不够友好,不够智能。
4.ConstraintLayout 适配
这种百分比约束布局适配,官方出的还是很得人心的,只是要弃用以前的5大布局逻辑还是需要一点时间的,并且百分比有些情况需要计算偏移量,多少不如直接写个值方便。最令人吐槽的是解决了百分比,却没有解决字体的适配。还需要搭配最小宽度限定符单独适配字体,这就有点弯弯绕了。