前言
屏幕适配一直是Android开发人员躲避不开的话题,更多的同学使用dp单位结合权重去做屏幕适配,但是当设备的物理尺寸存在差异的时候,dp就显得无能为力了。为4.3寸屏幕准备的UI,运行在5.0寸的屏幕上,很可能在右侧和下侧存在大量的空白。而5.0寸的UI运行到4.3寸的设备上,很可能显示不下。也有同学使用GooGle的百分比布局,但是实践过程中需要增加代码量,也没有那么简单高效,有没有一种无脑按照UI设计图设置宽高就能完美适配不同机型的方案?
思路
我们可以按照百分比布局的思想创造出一个全新的单位变量,来衡量屏幕到底是多少份,无论是480 x 800 还是720 x 1280 甚至1080 x 1920分辨率的手机,我们都可以把他的宽分成100份,高分成100份。
一份宽就是480/100 =
一份高就是800/100 =
一份宽就是720/100 =
一份高就是1280/100 =
在布局的时候,比如设计稿上需要一个View宽占屏幕宽度一半,高也占屏幕一半 如下图
image.png
那我们就已经知道宽设置成50份,高也设置成50份,就可以完美适配所有分辨率的屏幕。
android:layout_width="50份"
android:layout_height="50份"
宽实际上就是50份x
= 240像素
高实际上就是50份x
= 400像素
宽实际上就是50份x
= 360像素
高实际上就是50份x
= 640像素
伪实践
按照UI设计师给我们的蓝湖设计稿为例
蓝湖设计稿.png
UI设计师给我们了360*640设计原稿 那么我们可以把宽分成360份,高分成640份。
一份宽就是480/360 =
一份高就是800/640 =
一份宽就是720/360 =
一份高就是1280/640 =
蓝湖设计稿2.png
那么我们在写这个View宽高布局的时候如下
android:layout_width="48份"
android:layout_height="24份"
工程实践
工程方案1(已经过多个项目实践)
针对你所需要适配的手机屏幕的分辨率各自建立一个value文件
image.png
比如以蓝湖设计稿320*480的分辨率为基准
宽度为320,将任何分辨率的宽度分为320份,取值为x1-x320
高度为480,将任何分辨率的高度分为480份,取值为y1-y480
x1相当于1份宽
y1相当于1份高
例如对于800*480的宽度480:
image.png
可以看到x1 = 480 / 基准 = 480 / 320 = 1.5 以此类推
假设我现在需要在屏幕中心有个按钮,宽度和高度为我们屏幕宽度的1/2,我可以怎么编写布局文件呢?
android:layout_gravity="center"
android:gravity="center"
android:text="@string/hello_world"
android:layout_width="@dimen/x160"
android:layout_height="@dimen/x160"/>
可以看到我们的宽度和高度定义为x160,其实就是宽度的50%
不同机型的效果图
image.png
好了,有个最主要的问题,就是分辨率这么多,难道我们要自己计算,然后手写?
jar包内置了常用的分辨率,默认基准为480*320,当然对于特殊需求,通过命令行指定即可
例如基准 1280 * 800 ,额外支持尺寸:1152 * 735;4500 * 3200
java -jar xx.jar width height width,height_width,height
20150503173911632.gif
这样拷贝到自己的项目中就可以使用了
缺点
新建了很多value文件,给apk新增了3-4M的体积
默认基准分辨率只能适配99%的机型,如果遇到value里面不存在的分辨率则不能适配,需要动态增加该分辨率下的value文件
工程方案2(头条的适配方案)
大致思路:通过重写Activity的getResources(),重写冷门单位pt作为基准单位,1pt代表前文提到的一份
AdaptScreenUtils
public final class AdaptScreenUtils {
private static List sMetricsFields;
private AdaptScreenUtils() {
throw new UnsupportedOperationException("u can't instantiate me...");
}
/**
* Adapt for the horizontal screen, and call it in {@link android.app.Activity#getResources()}.
*/
public static Resources adaptWidth(final Resources resources, final int designWidth) {
float newXdpi = (resources.getDisplayMetrics().widthPixels * 72f) / designWidth;
applyDisplayMetrics(resources, newXdpi);
return resources;
}
/**
* Adapt for the vertical screen, and call it in {@link android.app.Activity#getResources()}.
*/
public static Resources adaptHeight(final Resources resources, final int designHeight) {
return adaptHeight(resources, designHeight, false);
}
/**
* Adapt for the vertical screen, and call it in {@link android.app.Activity#getResources()}.
*/
public static Resources adaptHeight(final Resources resources, final int designHeight, final boolean includeNavBar) {
float screenHeight = (resources.getDisplayMetrics().heightPixels
+ (includeNavBar ? getNavBarHeight(resources) : 0)) * 72f;
float newXdpi = screenHeight / designHeight;
applyDisplayMetrics(resources, newXdpi);
return resources;
}
private static int getNavBarHeight(final Resources resources) {
int resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android");
if (resourceId != 0) {
return resources.getDimensionPixelSize(resourceId);
} else {
return 0;
}
}
/**
* @param resources The resources.
* @return the resource
*/
public static Resources closeAdapt(final Resources resources) {
float newXdpi = Resources.getSystem().getDisplayMetrics().density * 72f;
applyDisplayMetrics(resources, newXdpi);
return resources;
}
/**
* Value of pt to value of px.
*
* @param ptValue The value of pt.
* @return value of px
*/
public static int pt2Px(final float ptValue) {
DisplayMetrics metrics = FWAdSDK.sContext.getResources().getDisplayMetrics();
return (int) (ptValue * metrics.xdpi / 72f + 0.5);
}
/**
* Value of px to value of pt.
*
* @param pxValue The value of px.
* @return value of pt
*/
public static int px2Pt(final float pxValue) {
DisplayMetrics metrics = FWAdSDK.sContext.getResources().getDisplayMetrics();
return (int) (pxValue * 72 / metrics.xdpi + 0.5);
}
private static void applyDisplayMetrics(final Resources resources, final float newXdpi) {
resources.getDisplayMetrics().xdpi = newXdpi;
FWAdSDK.sContext.getResources().getDisplayMetrics().xdpi = newXdpi;
applyOtherDisplayMetrics(resources, newXdpi);
}
static void preLoad() {
applyDisplayMetrics(Resources.getSystem(), Resources.getSystem().getDisplayMetrics().xdpi);
}
private static void applyOtherDisplayMetrics(final Resources resources, final float newXdpi) {
if (sMetricsFields == null) {
sMetricsFields = new ArrayList<>();
Class resCls = resources.getClass();
Field[] declaredFields = resCls.getDeclaredFields();
while (declaredFields != null && declaredFields.length > 0) {
for (Field field : declaredFields) {
if (field.getType().isAssignableFrom(DisplayMetrics.class)) {
field.setAccessible(true);
DisplayMetrics tmpDm = getMetricsFromField(resources, field);
if (tmpDm != null) {
sMetricsFields.add(field);
tmpDm.xdpi = newXdpi;
}
}
}
resCls = resCls.getSuperclass();
if (resCls != null) {
declaredFields = resCls.getDeclaredFields();
} else {
break;
}
}
} else {
applyMetricsFields(resources, newXdpi);
}
}
private static void applyMetricsFields(final Resources resources, final float newXdpi) {
for (Field metricsField : sMetricsFields) {
try {
DisplayMetrics dm = (DisplayMetrics) metricsField.get(resources);
if (dm != null) dm.xdpi = newXdpi;
} catch (Exception e) {
Log.e("AdaptScreenUtils", "applyMetricsFields: " + e);
}
}
}
private static DisplayMetrics getMetricsFromField(final Resources resources, final Field field) {
try {
return (DisplayMetrics) field.get(resources);
} catch (Exception e) {
Log.e("AdaptScreenUtils", "getMetricsFromField: " + e);
return null;
}
}
}
使用方法
以宽度320为基准
@Override
public Resources getResources() {
return AdaptScreenUtils.adaptWidth(super.getResources(),320);
}
假设我现在需要在屏幕中心有个按钮,宽度和高度为我们屏幕宽度的1/2,我可以怎么编写布局文件呢?
android:layout_gravity="center"
android:gravity="center"
android:text="@string/hello_world"
android:layout_width="160pt"
android:layout_height="160pt"/>
优点
1. 无侵入性
用了这个之后你依然可以使用dp包括其他任何单位,对你从前使用的布局不会造成任何影响,在老项目中开发新功能你可以胆大地加入该适配方案,新项目的话更可以毫不犹豫地采用该适配,并且在关闭该关闭后,pt 效果等同于 dp 哦。
2. 灵活性高
如果你想要对某个 View 做到不同分辨率的设备下,使其尺寸在适配维度上所占比例一致的话,那么对它使用 pt 单位即可,如果你不想要这样的效果,而是想要更大尺寸的设备显示更多的内容,那么你可以像从前那样写 dp、sp 什么的即可,结合这两点,在界面布局上你就可以游刃有余地做到你想要的效果。
3. 不会影响系统 View 和三方 View 的大小
这点其实在无侵入性中已经表现出来了,由于头条的方案是直接修改 DisplayMetrics#density 的 dp 适配,这样会导致系统 View 尺寸和原先不一致,比如 Dialog、Toast、 尺寸,同样,三方 View 的大小也会和原先效果不一致,这也就是我选择 pt 适配的原因之一。
4. 不会失效
因为不论头条的适配还是 AndroidAutoSize,都会存在 DisplayMetrics#density 被还原的情况,需要自己重新设置回去,最显著的就是界面中存在 WebView 的话,由于其初始化的时候会还原 DisplayMetrics#density 的值导致适配失效,当然这点已经有解决方案了,但还会有很多其他情况会还原 DisplayMetrics#density 的值导致适配失效。而我这方案就是为了解决这个痛点,不让 DisplayMetrics 中的值被还原导致适配失效。
效果
480 x 800 - mdpi(160dpi)
image
720 x 1280 - xhdpi(320dpi)
image
1080 x 1920 - xxhdpi(480dpi)
image
1440x2560 - 560dpi
image
可以看到效果图中 WebView 对之后的 View 并没有产生适配失效的问题,这是之前适配所不能解决的问题。
如何创建预览?
在 AS 中 Tools -> AVD Manager -> Create Virtual Device...,我们以适配 1080 x 1920px 为例,具体操作如下:
image
创建完设备我们在预览界面选中这个设备即可看到 pt 单位效果。
注:有的设备需要重启电脑才可以选中设备效果
设计师给你的设计图尺寸是多少,那你就建多少尺寸的设备即可,比如是 720 x 1280px 的,那你把上图的尺寸换成 720 和 1280,再计算下屏幕尺寸即可,如果是 360 x 640dp 的话,那就把上图的尺寸换成 360 和 640,再计算下屏幕尺寸即可,不用去 care 单位到底是什么,设计图标注多少那你就写多少即可,无需换算。适配的时候传入这个维度的尺寸值即可,比如 720 x 1280 的宽度适配,那就传入 720 即可。
参考链接