目录
1、AndroidAutoSize实战
1.1 AndroidAutoSize简介
- 基于今日头条屏幕适配方案的一个开源库(Star: 4329);
- 通过修改Application/Activity等的DisplayMetrics中核心数据,使得在不同分辨率手机上对应的dp相等而达到每个显示的View占用屏幕的比例相同。
https://github.com/JessYanCoding/AndroidAutoSize
1.2 代码实现
1.2.1 依赖
compile 'me.jessyan:autosize:1.0.1'
1.2.2 manifest配置
<manifest>
<application>
<meta-data
android:name="design_width_in_dp"
android:value="360"/>
<meta-data
android:name="design_height_in_dp"
android:value="640"/>
</application>
</manifest>
1.2.3 Activity支持
-
默认情况下已经支持适配(宽度)
-
取消适配
取消适配只需要我们的Activity实现CancelAdapt即可,具体如下:
public class TestActivity extends Activity implements CancelAdapt{
}
效果图如下:
取消适配后可以看到宽度360dp 已经不能占满整个宽屏。
-
自定义宽/高(dp)
如果我们的Activity不能使用Manifest中配置的宽/高,需要个性化配置。Activity只需要实现CustomAdapt 接口并实现isBaseOnWidth和getSizeInDp两个函数即可,具体代码如下:
public class TestActivity extends Activity implements CustomAdapt {
@Override
public boolean isBaseOnWidth() {
// true: 以宽度等比例缩放 false: 以高度等比例缩放
return true;
}
@Override
public float getSizeInDp() {
// 对应的宽/高
return 540;
}
}
效果图如下:
通过自定义总宽度值,可以看到宽度180dp只占有整个屏幕宽度的1/3。
1.2.4 Fragment支持
(略)
1.3 不同分辨率屏幕效果对比
-
设备介绍
屏幕分辨率 | dpi | 屏幕宽度dp | |
---|---|---|---|
手机 A | 720×1280 | 160 | 720(px=dp*(dpi/160)) |
手机 B | 720×1280 | 320 | 360(px=dp*(dpi/160)) |
-
适配前效果对比(左:手机A, 右:手机B)
可以发现,仅仅通过dp来适配,不同的设备显示的差异非常大。
-
适配后效果对比(左:手机A, 右:手机B)
为了与上图有一个比较,此处将Manifest中meta-data的design_width_in_dp设置为400。
适配过后,不同分辨率的设备显示非常相似。
2、AndroidAutoSize原理分析
2.1 基本概念
2.1.1 一些重要的单位
名称 | 简介 |
---|---|
px | pixels(像素),屏幕上的实际像素点,无论控件或文字最终都会转化为px单位来显示其大小。 |
dp | 与dip雷同,指的是设备独立像素,在不同分辨率和尺寸的手机上代表了不同的真实像素,计算公式:px = dp(dpi/160) |
dpi | 像素密度,指的是在系统软件上指定的单位尺寸的像素数量,它往往是写在系统出厂配置文件的一个固定值。 |
sp | 全称scaled pixels,放大像素的缩写,专门用于处理字体的大小。它不仅与屏幕dpi有关,还与系统的默认字体大小有关。 |
2.1.2 单位转换中涉及到的两个重要类
-
DisplayMetrics.java
public class DisplayMetrics {
public static final int DENSITY_MEDIUM = 160;
public static final int DENSITY_DEFAULT = DENSITY_MEDIUM;
public static final float DENSITY_DEFAULT_SCALE = 1.0f / DENSITY_DEFAULT;
public static int DENSITY_DEVICE = getDeviceDensity();
public int widthPixels; // 屏幕像素(宽)
public int heightPixels; // 屏幕像素(高)
public float density; // 屏幕密度, density = dpi/160, dp与px之间的转化就是用此参数
public int densityDpi; // dpi
public float scaledDensity; // 字体大小转换会用到此参数 px = sp*scaledDensity
public float xdpi;
public float ydpi;
......
public void setToDefaults() {
widthPixels = 0;
heightPixels = 0;
density = DENSITY_DEVICE / (float) DENSITY_DEFAULT;
densityDpi = DENSITY_DEVICE;
scaledDensity = density;
xdpi = DENSITY_DEVICE;
ydpi = DENSITY_DEVICE;
......
}
}
这里面有几个重要的参数: density、densityDpi、scaledDensity,AndroidAutoSize的原理主要就是修改这几个参数值来实现屏幕的适配。下面还有一个系统提供的单位转化API,系统内部基本上都是调用此API来实现单位转化。
-
TypedValue.java
public static float applyDimension(int unit, float value,
DisplayMetrics metrics){
switch (unit) {
case COMPLEX_UNIT_PX:
return value;
case COMPLEX_UNIT_DIP:
return value * metrics.density;
case COMPLEX_UNIT_SP:
return value * metrics.scaledDensity;
case COMPLEX_UNIT_PT:
return value * metrics.xdpi * (1.0f/72);
case COMPLEX_UNIT_IN:
return value * metrics.xdpi;
case COMPLEX_UNIT_MM:
return value * metrics.xdpi * (1.0f/25.4f);
}
return 0;
}
根据unit,将value转化为px值。
2.2 实现原理
通过上面的单位介绍及之间的转换,我们可以得到如下结论:
- p x= dp(dpi/160), density = dpi/160 => px = dp*density => dp = px/density;
明白上面这个结论,下面我们来讨论为什么我们日常对控件设置的宽/高为某一dp时,无法做到各个手机屏幕的适配。
- 设备 A , 屏幕宽度为 720px, dpi为160,则屏幕总dp为 720/(160/160) = 720 dp
- 设备 B , 屏幕宽度为 720px, dpi为320,则屏幕总dp为 720/(320/160) = 360 dp
可以看到屏幕的总 dp 宽度在不同的设备上是会变化的,但是我们在布局中填写的 dp 值却是固定不变的,这就导致我们设置的固定宽度在不同的设备上显示的比例不一样。 例如我们布局中有一个View设置固定宽度为180dp,在设备A中会占屏幕宽度的1/4,但是在设备B中只会占屏幕宽度的1/2,这种差别是十分巨大的。
这时我们要想完美适配,那就必须保证这个 View 在任何分辨率的屏幕上,与屏幕的比例都是相同的。
要做到在任何分辨率的屏幕上显示比例相同,我们该怎么做呢?
-
方案一: 动态改变每个View的dp值
由于每种设备的宽度dp值是不同的,为使得View能够在不同设备上显示的比例一致,可以通过代码计算动态的设置每个View的dp值,这种方式显然是不合适的,工作量太大,太复杂。
-
方案二: 每个View设置固定的dp值,通过修改density 值而达到每种分辨率手机的宽度dp值相同
由公式:dp = px/density 可知,由于px是屏幕分辨率,这个值有硬件确定,我们是无法改变的,那么我们可以通过修改density 的值使得不同分辨率的手机宽度dp值是相同的,这样当我们对View设置为某一特定的dp宽度时,占总宽度的dp比例是相同的,这样也就达到占屏幕的比例相同。
那么问题来了,我们如何确定density 的值呢?
由dp = px/density => 屏幕的总 px 宽度 / density = 屏幕的总 dp 宽度
屏幕的总 px 宽度 / density = 屏幕的总 dp 宽度 => 当前设备屏幕总宽度(单位为像素)/ 设计图总宽度(单位为 dp) = density
如果我们将一套设计图的总宽度(dp)作为最终手机屏幕的中宽度(dp), 从而达到修改density的目的,同时又可以保证最终不同分辨率手机的屏幕总宽度是相同的,这也就完成了适配。
AndroidAutoSize/今日头条 就是基于方案二的原理来实现屏幕适配的。
2.3 方案可行性
假设设计图中宽度为300dp,一个View在设计图上的尺寸为 100dp * 100dp,那么这个View的宽度占整个设计图宽度的33.3%,那么接下来我们来验证下通过方案二的适配方案,这个View能否做到不同分辨率的设备还能保持与设计图中一致的比例。
-
验证设备 1
屏幕总宽度为 1080 px,根据上面公式求出density, 1080/300 = 3.6(density),那么这个尺寸为 100dp * 100dp的View,系统最后会将都换算成px,也就是 px=100*3.6=360(px), 然后 360/1080=0.333=33.3%,与设计图上的比例一致。
问题一
某些设备总宽度为1080px,但设备的dpi不同,是否对该方案适配产生影响?
答案当然是不会的,其实这个方案根本没有根据 设备提供的dpi 求出 density,是根据自己的公式求出的 density,所以这对该方案是没有影响的。
-
验证设备 2-
刚才我们验证的是宽度为1080px的设备,现在我们用另外一种分辨率的设备720px来验证。
屏幕总宽度为 720 px,根据上面公式求出density, 720 /300 = 2.4(density),那么这个尺寸为 100dp * 100dp的View,系统最后会将都换算成px,也就是 px=100*2.4=24.(px), 然后 240/720=0.333=33.3%,与设计图上的比例一致。
通过计算,该方案是可行的。
2.4 优缺点
-
优点
-
使用成本非常低,操作非常简单,使用该方案后在页面布局时不需要额外的代码和操作,这点可以说完虐其他屏幕适配方案
-
侵入性非常低,该方案和项目完全解耦,在项目布局时不会依赖哪怕一行该方案的代码,而且使用的还是 Android 官方的 API,意味着当你遇到什么问题无法解决,想切换为其他屏幕适配方案时,基本不需要更改之前的代码,整个切换过程几乎在瞬间完成,会少很多麻烦,节约很多时间,试错成本接近于 0
-
可适配三方库的控件和系统的控件(不止是是 Activity 和 Fragment,Dialog、Toast 等所有系统控件都可以适配),由于修改的 density 在整个项目中是全局的,所以只要一次修改,项目中的所有地方都会受益
-
不会有任何性能的损耗。
-
缺点
-
只需要修改一次 density,项目中的所有地方都会自动适配,这个看似解放了双手,减少了很多操作,但是实际上反应了一个缺点,那就是只能一刀切的将整个项目进行适配,但适配范围是不可控的。
-
这个方案依赖于设计图尺寸,但是项目中的系统控件、三方库控件、等非我们项目自身设计的控件,它们的设计图尺寸并不会和我们项目自身的设计图尺寸一样。
其实 AndroidAutoSize开源库已经很大程度上解决了如上两个缺点,如前面已经给出Activity的用法,适配粒度可以达到Activity。
3、一些思考
3.1 其他方案对比
-
传统的dp、layout_weight等做简单的适配
设备的dpi值并不是任意指定的,它是通过
sqrt(screenWpx2 + screenHpx2) / 屏幕尺寸
计算出的结果(上面模拟器参数是我特意设置,为了很明显的演示所需) , 因此在大多数设备上对View的宽/高设以dp为单位进行设置值,差别并不是十分大,当然这只是大多数设备,因此要适配每种设备还是很难做到的。
-
smallestWidth 限定符屏幕适配方案
开发者先在项目中根据主流屏幕的 最小宽度 (smallestWidth) 生成一系列 values-swdp 文件夹 (含有 dimens.xml 文件),当把项目运行到设备上时,系统会根据当前设备屏幕的 最小宽度 (smallestWidth) 去匹配对应的 values-swdp 文件夹,而对应的 values-swdp 文件夹中的 dimens.xml 文字中的值,又是根据当前设备屏幕的 最小宽度 (smallestWidth) 而定制的,所以一定能适配当前设备。
├── src/main
│ ├── res
│ ├── ├──values
│ ├── ├──values-sw320dp
│ ├── ├──values-sw360dp
│ ├── ├──values-sw400dp
│ ├── ├──values-sw411dp
│ ├── ├──values-sw480dp
│ ├── ├──...
│ ├── ├──values-sw600dp
│ ├── ├──values-sw640dp
详情可参考:
https://juejin.im/post/5ba197e46fb9a05d0b142c62
-
优点
- 非常稳定,极低概率出现意外
- 不会有任何性能的损耗
- 适配范围可自由控制,不会影响其他三方库
-
缺点
- 在布局中引用 dimens 的方式,日常维护修改时较麻烦
- 侵入性高,如果项目想切换为其他屏幕适配方案,因为每个 Layout 文件中都存在有大量 dimens 的引用,这时修改起来工作量非常巨大,切换成本非常高昂
- 无法覆盖全部机型,想覆盖更多机型的做法就是生成更多的资源文件。
- 如果想使用 sp,也需要生成一系列的 dimens,导致再次增加 App 的体积
3.2 应用到项目中可能存在的问题
- 如果使用AndroidAutoSize开源库,那么默认就已经使用AndroidAutoSize对应的适配方案来适配了,没有提供相关API关闭默认适配,这样会对已有的组件造成不可预估的风险;
- 由于AndroidAutoSize 需要设置一个全局的design_width_in_dp和design_height_in_dp(也就是设计图的dp值),那么对于我们目前的各个组件是否适合;
3.3 建议
- 针对3.2中的问题1,我们可以对AndroidAutoSize进行一定的修改,例如增加相关api,在应用的application执行过程中,关闭AndroidAutoSize的默认开启全局适配的特性;
- 针对3.2中的问题2,目前AndroidAutoSize已经能够做到Activity的粒度,让每个activity的总宽度/高度 dp值个性化(AndroidAutoSize实战已有讲解)。
参考文献
https://juejin.im/post/5b7a29736fb9a019d53e7ee2