前言:
Android 技能树系列:
Android 基础知识
Android 技能树 - 动画小结
Android 技能树 - View 小结
Android 技能树 - Activity 小结
Android 技能树 - View 事件体系小结
Android 技能树 - Android 存储路径及 IO 操作小结
Android 技能树 - 多进程相关小结
Android 技能树 - Drawable 小结
数据结构基础知识
Android 技能树 - 数组, 链表, 散列表基础小结
Android 技能树 - 树基础知识小结 (一)
算法基础知识
Android 技能树 - 排序算法基础小结
Rx 系列相关
Android 技能树 - RxPermission 分析
Android 技能树 - Rxjava 取消订阅小结 (1): 自带方式
Android 技能树 - Rxjava 取消订阅小结 (2):RxLifeCycle
关于屏幕适配, 几乎每隔一段时间就会看见有人发出来说 XXX 方案, 实现超级简单的适配方式等等. 所以我把我目前了解过的常用的适配方案做个总结, 并简单说说原理, 从而让大家也初步了解各个方案的实现.(其实很多人都是看见别人写的适配方案, 虽然可能实际在使用了, 但是却从来没有去了解过这个方案的原理, 而且遇到一些简单的坑的时候, 因为不知道原理, 也无法自己解决.)
常见适配方案:
生成分辨率 values 文件夹
生成 values -sw 文件夹
谷歌百分比布局库
AutoLayout
动态更改 density
1. 基础知识
其实本来不想写这块, 因为基本大家都懂什么 dp, dpi ,px , inch ,density 等, 但是后面的一些适配都会涉及到这些原理, 外加有时候面试别人, 都是感觉知道这个知识点, 但并不是真正的了解, 所以我这边还是重新提一下,
我会用通俗易懂的例子来让大家更好的理解
- . (PS: 当然想不看的可以直接跳过.)
- 1.1 px
我们可以看到现在市面上的手机分辨率截止到 2018-05 月, 统计为:
这里额外提一下, 类似 1080 x 1812,720 x 1184 等看着很奇怪的结尾不是 0 的分辨率, 大部分是因为有虚拟键的原因, 虚拟键占去了一部分高度.
以 1080 X 1920 为例, 它代表的是手机上的像素点,
类似这种, 表示横着有 1080 个像素点, 竖着有 1920 个像素点, 所以 1080 X 1920 代表了手机在横向, 纵向上的像素点数总和
所以如果我们写了一个 Button, 假设高度和宽度都为 10px , 则说明在这个屏幕点上高宽都占了 10 个点.
1.2 inch(屏幕尺寸)
手机屏幕的物理尺寸, 我们经常听到有人说我买的是 iPhone 8 plus, 尺寸是 5.5 的屏幕, iPhone 8 尺寸是 4.7 的. 其实它们所带的单位都是 inch(英寸), 1(inch)2.54(cm)
所以屏幕尺寸就是按屏幕对角测量的实际物理尺寸. 为简便起见, Android 将所有实际屏幕尺寸分组为四种通用尺寸: 小, 正常, 大和超大.
1.3 dpi
屏幕物理区域中的像素量; 通常称为 dpi(Dots Per Inch 每英寸 点数). 所以看标题就知道, 他更像是在求一个密度. 那我们既然知道了手机屏幕对角线的尺寸, 我们只要知道了手机对角线上的 px 数量, 除一下就知道了每英寸上的像素点数了.
所以我们只需要通过勾股定理获取对角线上的像素值, 再除以屏幕尺寸值就可以了.
为简便起见, Android 将所有屏幕密度分组为六种通用密度: 低, 中, 高, 超高, 超超高和超超超高.
六种通用的密度:
- ldpi(低)~120dpi
- mdpi(中)~160dpi
- hdpi(高)~240dpi
- xhdpi(超高)~320dpi
- xxhdpi(超超高)~480dpi
- xxxhdpi(超超超高)~640dpi
1.4 dp 和 density
其实 dp 本来是叫 dip (Density Independent Pixels), 所有有时候面试的别人, 面试者会弄错, 把 dip 当做了 dpi, 所以你问他请说下 dp 和 dip , 他会把 dip 说成 dpi 的内容.
我们举例说下这块知识点: 要画一个 高和宽各为屏幕的一般的按钮, 我们假设有二块屏幕, 一块是 100 X 100 , 一块是 200 X 200 , 那这时候第一块的屏幕上我们写 Button 应该为:
- <Button
- layout_height = "50px"
- layout_width = "50px"/>
复制代码
第二个屏幕的 Button 应该为:
- <Button
- layout_height = "100px"
- layout_width = "100px"/>
复制代码
这样是不是都各自占了屏幕的高宽的一半, 但是假如有第三个屏幕 300 X 300 呢, 难不成再写一个 Button 的高宽值? 所以我们可以用一种单位来代替, 但是这种单位可以在不同的屏幕环境下, 值是不同的. 比如我们就把这个单位当做 "haha".
比如我们现在都这么写:
- <Button
- layout_height = "50haha"
- layout_width = "50haha"/>
复制代码
这时候在 100 x 100 的时候, 50haha = 50px , 在 200 X 200 屏幕的时候 , 50 haha = 100px , 在 300 X 300 屏幕的时候, 50haha 等 150px.
这个感觉就很像你跟别人说我欠你 50 money, 如果在中国, 代表你欠别人 50 元人民币, 但是如果在美国, 你这么说, 指你欠 50 美元, 也就是欠了三百多元人民币.(这个例子不要跟我较真, 我就意思意思而已)
所以 dp 就是类似我们上面自己定义的 haha 这个单位.
比如 50dp = 50px , 这时候 1dp = 1px , 50dp = 100px 的时候 是 1dp = 2px , 所以我们可以看到倍数分别为 1 和 2 , 我们用 density 来代表这个倍数. 也就是说: dp * density = px, 这时候就是 50 dp * 1 = 50px , 50dp * 2 = 100px
(就像是我说我欠你 50 money, 在中国, 这个 density 就是 1 , 也就是欠你 50 元人民币, 在美国可能就是指 300 多人民币, 这个 density 也就是 美元换算成人民币的倍数)
那么这个 density 具体是怎么来的呢? 其实很简单, 记不记得我们前面说过 dpi , 也就是屏幕的密度, 我们就用这个密度来做比较, 比如我们 把 160dpi 作为标准, 那另外一个手机是 320dpi , 那么这个 density 就是 (320/160 = 2). 所以我们再次把公式 : dp * density = px 转变为: dp * (dpi / 160) = px
那么为什么用 160dpi 作为标准呢, 以前看到文章提过: mdpi 基于第一款 Android 设备 T-Mobile G1 的屏幕配置 (缩放系数 scale=1).
1.5 基础知识小结
所以假如我们现在的手机分辨率知道了, 手机屏幕尺寸也知道了. 我们通过公式求出 dpi , 然后 dpi / 160 就是当前手机的 density, 然后我们就知道我写了 1dp 在这台手机上具体是多少 px 了.
具体的安卓手机尺寸四个分类及 6 中 dpi 分类:
我们的某台手机的 dpi,density, 分辨率等如何获取呢,:
- DisplayMetrics mDisplayMetrics = getResources().getDisplayMetrics();
- // 横向分辨率
- int width = mDisplayMetrics.widthPixels;
- // 竖向分辨率
- int height = mDisplayMetrics.heightPixels;
- //density 值
- float density = mDisplayMetrics.density;
- //dpi 的值就等于 density * 160
- float dpi = density * 160;
复制代码
也许有人说, 那我们使用 dp 不是已经完美的实现了各种兼容性吗, 就像我们上面提到过的, 100 X 100 ,200X200 , 300 X 300 的屏幕, 我们都只要写 50haha, 就分别代表了 50,100,150, 不是就占了各自屏幕的一半了么. 理论上的确是这样, 但是我们刚提过我们的 density 是等于 (dpi / 160), 而 dpi 又由分辨率和屏幕尺寸同时决定, 安卓手机的碎片化太过严重, 所以很多手机虽然分辨率不同及屏幕尺寸不同, 造成最后的 dpi 一样, 所以最后的 density 也一样, 就造成了适配实现不全. 假设我们多了一个 400X400 的设备, 因为它的屏幕尺寸也同时变大了很多, 所以最终的 density 和 300X300 一样, 那这时候我们写了 50haha, 也就代表了 150px, 这时候明显在 400X400 上面并没有显示为一半, 甚至当这个 400X400 的设置的屏幕尺寸超级大, 反而可能算下来的 density 与 100X100 的一样, 那这时候 50haha 可能就只有 50px, 则显示差距就更大了. (其实主要原因就是 dpi 不是单独由分辨率来决定, 同时还有屏幕尺寸影响, 所以二个变量同时作用, 造成不同分辨率的手机最后的 density 也可能相同. 这样 dp 转换成的 px 也就相同了, 但是手机的分辨率本身有不同, 这时候就会出现适配不对.)
2 各类适配方案
2.1 生成分辨率 values 文件夹
因为我们上面提过 , px = (dpi / 160) * dp, 但是 dpi 又是同时由分辨率和屏幕尺寸同时决定, 造成了不同的分辨率, dpi 可能一样, 这样最终得到的 px 一样, 比如都是占屏幕的一半, 300X300 得到的可能是 150, 但是 400X 400 得到的也是 150, 这时候就不对了.
那我们就想到了. 我们能不能不是同时受到分辨率和屏幕尺寸决定, 而是只受一个因素来影响, 这样就是真正的按比例来了. 比如 300X300 是 150,400X400 是 200,500X500 是 250, 是只受分辨率的影响, 所以分辨率大的, 最终得到的结果一定就大. 所以我们就不能使用 dp 了. 而是一个新的单位, 而这个单位是根据不同的分辨率, 得到不同的值, 那怎么计算呢, 就是穷举法, 比如刚才的 300X300, 我们规定 1 haha 等于 1 px, 然后再 600 X 600 里面, 1 haha 等 2 px , 1200X1200 里面是 1 haha 等于 3 px . 所以我们在不同分辨率下的 values 文件夹下写上不同的值:
300X300 下
<dimens name = "1haha"> 1px </dimens>
600X600 下
<dimens name = "1haha"> 2px </dimens>
1200X1200 下
<dimens name = "1haha"> 3px </dimens>
复制代码
所以这个就是方案 1 , 附上文章链接.
Android 屏幕适配方案 https://blog.csdn.net/lmj623565791/article/details/45460089 我们可以看下面的图:
我们可以看到列举了所有可能的屏幕分辨率的 values, 然后手动按照倍数, 进行相应的赋值. 当然这些文件不可能手写, 通过 Java 自动生成相应的文件:
这样最终影响结果的就只是分辨率的了, 分辨率越大的, x1 的值越大.
但是这个方案有一个致命的缺陷, 那就是需要精准命中才能适配, 比如 1920x1080 的手机就一定要找到 1920x1080 的限定符, 否则就只能用统一的默认的 dimens 文件了. 而使用默认的尺寸的话, UI 就很可能变形, 简单说, 就是容错机制很差.
2.2 生成 values -sw 文件夹
可以参考: Android 目前最稳定和高效的 UI 适配方案
其实这个方式跟上面的 2.1 方法原理可以说一模一样. 唯一的区别就是使用了 sw 来保证一定的容错性.
我们看到其实就是把上面具体的分辨率 values 改成了 values - sw 而已.
2.3 百分比布局库
Android 百分比布局库 (percent-support-lib) 解析与扩展 https://blog.csdn.net/lmj623565791/article/details/46695347 Android 增强版百分比布局库 为了适配而扩展 https://blog.csdn.net/lmj623565791/article/details/46767825
其实这个也是很简单的, 字面意思, 我写了这个 Button 宽度为父布局的百分之 50, 则在不同手机上, 都是占据了百分之 50. 使用过过百分比布局的人都应该知道, 我们写的时候是这么写的:
- <android.support.percent. PercentRelativeLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
- <Button
- android:layout_width="0dp"
- android:layout_height="0dp"
- app:layout_heightPercent="20%"
- app:layout_widthPercent="50%"
- android:gravity="center"
- />
- </PercentRelativeLayout>
复制代码
其实原理很简单, 就是动态计算实际的百分之 50 在不同机器的时候到底占了多少 px,2.1,2.2 则是等于提前帮我们计算好了具体的 px, 然后写在了文件里面, 然后我们去读数据.
那它的实现原理是什么呢? 简单来说就是二步:
获取用户到底填了多少的百分比数值
获取父布局的空间, 然后乘以用户填的百分比数值, 或者一个新数值, 然后赋值给该控件.
我们一步步来看源码:
2.3.1 获取用户到底填了多少的百分比数值:
我们知道我们的百分比布局中的核心属性是子控件填写:
- app:layout_heightPercent="20%"
- app:layout_widthPercent="30%"
复制代码
所以我们需要在 PercentRelativeLayout 中遍历它下面的子控件, 然后分别获取每个子控件的百分比数值. 其实很简单, 写过自定义 View 的人应该都知道, 因为这个其实就是自定义属性而已.
- TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.PercentLayout_Layout);
- float value = array.getFraction(R.styleable.PercentLayout_Layout_layout_widthPercent, 1, 1,-1f);
复制代码
2.3.2 获取计算后的值并且赋值:
因为要动态获取父控件的控件, 同时把新的值赋值给子控件, 所以该行为在 onMeasure 方法中执行.
- // 传入的 ViewGroup.LayoutParams params 是遍历的每个子 View 的 LayoutParams
- public void fillLayoutParams(ViewGroup.LayoutParams params, int widthHint,
- int heightHint) {
- // Preserve the original layout params, so we can restore them after the measure step.
- mPreservedParams.width = params.width;
- mPreservedParams.height = params.height;
- if (widthPercent>= 0) {
- params.width = (int) (widthHint * widthPercent);
- }
- if (heightPercent>= 0) {
- params.height = (int) (heightHint * heightPercent);
- }
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "after fillLayoutParams: (" + params.width + "," + params.height + ")");
- }
- }
复制代码
当然具体源码会更多, 我不会大篇幅完整讲流程, 更多的是讲解思路.
2.4 AutoLayout
Android AutoLayout 全新的适配方式 堪称适配终结者 https://blog.csdn.net/lmj623565791/article/details/49990941
使用方式很简单:
注册设计图尺寸
将 https://blog.csdn.net/lmj623565791/article/details/autolayout 引入
- dependencies {
- compile project(':autolayout')
- }
复制代码
在你的项目的 AndroidManifest 中注明你的设计稿的尺寸.
- <meta-data android:name="design_width" android:value="768"></meta-data>
- <meta-data android:name="design_height" android:value="1280"></meta-data>
复制代码
Activity 中开启设配 让你的 Activity 去继承 AutoLayoutActivity
我们想到的原理, 肯定也是把填在 AndroidManifest.xml 里面的数值读取出来, 然后作为参考值. 然后在不同手机上动态的计算出来数值, 是不是感觉和百分比布局有点相似.
我们来看下 AutoLayoutActivity 源码:
- public class AutoLayoutActivity extends AppCompatActivity
- {
- private static final String LAYOUT_LINEARLAYOUT = "LinearLayout";
- private static final String LAYOUT_FRAMELAYOUT = "FrameLayout";
- private static final String LAYOUT_RELATIVELAYOUT = "RelativeLayout";
- @Override
- public View onCreateView(String name, Context context, AttributeSet attrs)
- {
- View view = null;
- if (name.equals(LAYOUT_FRAMELAYOUT))
- {
- view = new AutoFrameLayout(context, attrs);
- }
- if (name.equals(LAYOUT_LINEARLAYOUT))
- {
- view = new AutoLinearLayout(context, attrs);
- }
- if (name.equals(LAYOUT_RELATIVELAYOUT))
- {
- view = new AutoRelativeLayout(context, attrs);
- }
- if (view != null) return view;
- return super.onCreateView(name, context, attrs);
- }
- }
复制代码
我们发现把我们写在 Layout.xml 里面的布局控件替换成 AutoXXXX 等自定义控件. 那我们以 AutoLinearLayout 来分析: 其实看过百分比布局的源码, 就会发现基本架构都一样, 所以百分比布局的代码看得懂, 再去看 AutoLayout 相关代码会很快.
2.5 动态更改 density
一种极低成本的 Android 屏幕适配方式 https://mp.weixin.qq.com/s/d9QCoBP6kV9VSWvVldVVwA
Android 屏幕适配很麻烦吗? 不! 太简单了.
假如设计图是按 1920px * 1080px 来设计, 以 density 为 3 来标注, 也就是屏幕其实是 640dp * 360dp. 这时候如果我们的 Button 想要占据一半, 是不是宽度需要设置成 180dp.
那假如我们的手机屏幕是 1280X 720,density 是 2 , 则宽度是 360dp, 的确当设置成 180dp 的时候也正好占据一半.
但是万一 1280X 720 的手机的 density 是 3 呢, 则宽度为 240dp, 这时候设置成 180dp, 实际的 px 值为: 180 * 3 = 540px , 但是我们想要的是 360px , 也就是 180 * density = 360px , 既然我们设置成的 180dp 不能改变 (也就是设置一个值, 适配各种手机), 那么我们只能改变这个 density 值.
换成公式就是: 180 * density = 360, 那么 density 是多少. 哈哈. 没错是 2 , 我们动态把 density 从 3 变成 2, 是不是就符合了.
比如 960X540 的手机, density 是 2 , 因为我们的 Button 宽度设置成了 180dp, 宽度为 180 X 2 = 360px, 超过了一半, 我们只需要动态更改 density 满足 180X density = 270px 即可, 所以我们的 density 算出来是 1.5.
那么 density 具体怎么得出来呢, 很简单, 我们刚才假设的是有一个按钮, 占了屏幕的一半, 那我们假设占了整个手机屏幕不就可以了. 设计图的宽度是 360dp, 而 960X540 的手机, 只要 540/360 = 1.5 就可以得到, 所以 density = 设备真实宽 (单位 px) / 360
- if (orientation.equals("height")) {
- targetDensity = (appDisplayMetrics.heightPixels - barHeight) / 667f;
- } else {
- targetDensity = appDisplayMetrics.widthPixels / 360f;
- }
复制代码
所以本方案就是动态更改 density 以满足设计图方案.
结语:
emm....... 大家轻喷即可....
来源: https://juejin.im/post/5b5315c8e51d45198565b172