Android屏幕适配-必备知识
Android屏幕适配-终结者
屏幕适配问题一直在开发中存在,没有一种完美的解决方案。Android 的碎片化很严重。
下面这张图片所显示的内容足以充分说明当今Android系统碎片化问题的严重性,因为该图片中的每一个矩形都代表着一种Android设备。
而随着支持Android系统的设备(手机、平板、电视、手表)的增多,设备碎片化、品牌碎片化、系统碎片化、传感器碎片化和屏幕碎片化的程度也在不断地加深。而我们今天要探讨的,则是对我们开发影响比较大的——屏幕的碎片化。
下面这张图是Android屏幕尺寸的示意图,在这张图里面,蓝色矩形的大小代表不同尺寸,颜色深浅则代表所占百分比的大小。
而与之相对应的,则是下面这张图。这张图显示了IOS设备所需要进行适配的屏幕尺寸和占比。
smallestWidth适配,或者叫sw限定符适配。指的是Android会识别屏幕可用高度和宽度的最小尺寸的dp值(其实就是手机的宽度值),然后根据识别到的结果去资源文件中寻找对应限定符的文件夹下的资源文件。
举个例子,小米5的dpi是480,横向像素是1080px,根据px=dp(dpi/160),横向的dp值是1080/(480/160),也就是360dp,系统就会去寻找是否存在value-sw360dp的文件夹以及对应的资源文件。如果找不到,系统就会去向下寻找,下面的图就会找到 value-sw320dp的文件夹。
这套方案是最接近完美的方案。 首先,从开发效率上,它不逊色于任意一种方案。
根据固定的放缩比例,我们基本可以按照UI设计的尺寸不假思索的填写对应的dimens引用。
我们还有以375个像素宽度的设计稿为例(iOS 设计稿),在values-sw375dp文件夹下的diemns文件应该怎么编写呢?
这个文件夹下,意味着手机的最小宽度的dp值是375,直接按着 1:1的比例写就好,那么接下来的事情就很简单了,假如设计稿上出现了一个20dp*20dp的TextView,那么,我们就可以不假思索的在layout文件中写下对应的尺寸。
<TextView
android:layout_width="@dimen/x20"
android:layout_height="@dimen/x20"
android:layout_marginTop="@dimen/x20"
android:background="@color/colorAccent"
android:text="Hello World!" />
values-sw375dp 目录下的 dimens.xml
<resources>
<dimen name="x20">20dpdimen>
resources>
那么设计稿为 375 个像素宽度,那么有没有办法直接可以生成其他限定符文件夹呢?
添加 jcenter()仓库,在项目的根 build.gradle 中添加
buildscript {
dependencies {
classpath 'com.android.tools.build:gradle:3.5.0-alpha13'
// 在此处添加
classpath 'vip.ruoyun.plugin:screen-plugin:1.0.0'
}
}
在要使用插件的的子项目的 build.gradle 中添加
apply plugin: 'vip.ruoyun.screen'
screen {
smallestWidths 320, 360, 384, 392, 400, 410, 411, 432, 480 //生成的目标屏幕宽度的适配文件
designSmallestWidth 375 //苹果设计稿750 × 1334 屏幕宽度为 375
decimalFormat "#.#" //设置保留的小数 ( #.## 保留2位) ( #.# 保留1位)
log false //是否打印日志
auto false //是否每次 build 项目的时候自动生成 values-sw[]dp 文件
}
如果 auto 设置为 true ,则每次 build 项目的时候自动生成 values-sw[]dp 文件
如果 auto 设置为 false,则可以通过命令行,来生成文件.
./gradlew dimensCovert
也可以在 gradle命令的 窗口中 点击 dimensCovert 的 task.
自动生成的sw 文件
生成规则:只会生成 dp 后缀的属性值,根据 values 目录下的 dimens.xml,生成具体的文件。 values 目录下的 dimens.xml
<resources>
<dimen name="x20">20dpdimen>
<dimen name="x30">20spdimen>
resources>
生成的目标文件,values-sw320dp 目录下的 dimens.xml 文件
<resources>
<dimen name="x20">17.1dpdimen>
resources>
因为是按着一对一的方式进行生成,所以对于最后生成的 apk 来说,只是增加了几 k的大小,完全不必担心会增加包体积。
通过阅读源码,我们可以得知,density 是 DisplayMetrics 中的成员变量,而 DisplayMetrics 实例通过 Resources#getDisplayMetrics 可以获得,而Resouces通过Activity或者Application的Context获得。
先来熟悉下 DisplayMetrics 中和适配相关的几个变量:
DisplayMetrics#density 就是上述的density
DisplayMetrics#densityDpi 就是上述的dpi
DisplayMetrics#scaledDensity 字体的缩放因子,正常情况下和density相等,但是调节系统字体大小后会改变这个值
那么是不是所有的dp和px的转换都是通过 DisplayMetrics 中相关的值来计算的呢?
首先来看看布局文件中dp的转换,最终都是调用 TypedValue#applyDimension(int unit, float value, DisplayMetrics metrics) 来进行转换:
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;
}
这里用到的DisplayMetrics正是从Resources中获得的。
再看看图片的decode,BitmapFactory#decodeResourceStream方法:
public static Bitmap decodeResourceStream(Resources res, TypedValue value,
InputStream is, Rect pad, Options opts) {
validate(opts);
if (opts == null) {
opts = new Options();
}
if (opts.inDensity == 0 && value != null) {
final int density = value.density;
if (density == TypedValue.DENSITY_DEFAULT) {
opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
} else if (density != TypedValue.DENSITY_NONE) {
opts.inDensity = density;
}
}
if (opts.inTargetDensity == 0 && res != null) {
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}
return decodeStream(is, pad, opts);
}
PhoneWindow的getDimension方法
public float getDimension(@DimenRes int id) throws NotFoundException {
final TypedValue value = obtainTempTypedValue();
try {
final ResourcesImpl impl = mResourcesImpl;
impl.getValue(id, value, true);
if (value.type == TypedValue.TYPE_DIMENSION) {
return TypedValue.complexToDimension(value.data, impl.getDisplayMetrics());
}
throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id)
+ " type #0x" + Integer.toHexString(value.type) + " is not valid");
} finally {
releaseTempTypedValue(value);
}
}
当然还有些其他dp转换的场景,基本都是通过 DisplayMetrics 来计算的,这里不再详述。因此,想要满足上述需求,我们只需要修改 DisplayMetrics 中和 dp 转换相关的变量即可。
这个方案侵入性很低,而且也没有涉及私有API,是个极不错的方案,我暂时也想不到强行修改density是否会有其他影响,既然有今日头条的大厂在用,稳定性应当是有保证的。
根据我的实践,这套方案对任何项目来说都是完美的,因为修改了系统的density值之后,整个布局的实际尺寸都会发生改变,如果想要在老项目文件中使用,那么可以把DisplayMetrics#density设置成原来你项目中的设计图的宽度。因此,如果你是在维护或者改造老项目,直接使用这套方案就可以了。
在项目的根 build.gradle 中添加 jcenter 仓库
然后在子项目中的 build.gradle 文件中添加
dependencies {
implementation 'vip.ruoyun.helper:screen-helper:1.0.2'
}
使用,在每个Activity 重写getResources()方法。
public class MainActivity extends AppCompatActivity {
@Override
public Resources getResources() {
return ScreenHelper.applyAdapt(super.getResources(), 450f, ScreenHelper.WIDTH_DP);
}
}
如果是悬浮窗适配,因为 inflate 用到的 context 是 application 级别的,所以需要在自定义的 Application 中重写 getResource。
public class App extends Application {
@Override
public Resources getResources() {
return ScreenHelper.applyAdapt(super.getResources(), 450f, ScreenHelper.WIDTH_DP);
}
}
https://github.com/bugyun/ScreenHelper
如果你喜欢我的文章,可以关注我的掘金、公众号、博客、简书或者Github!
简书: https://www.jianshu.com/u/a2591ab8eed2
GitHub: https://github.com/bugyun
Blog: https://ruoyun.vip
掘金: https://juejin.im/user/56cbef3b816dfa0059e330a8/posts
CSDN: https://blog.csdn.net/zxloveooo
欢迎关注微信公众号