前言
android设备各种各样,手机、pad、电视、车载等不一而足。即使是相同分辨率的手机也可能参数不一致,比如1080P的手机 dpi 一般认为是480,但是 Google 的Pixel2(1920*1080)的 dpi 是420。此外,android设备的宽高比更是多种多样。这就导致App适配的工作异常困难。尤其是你的app要适配各种平台,比如手机、pad、车载、电视。在这种情形下,你面临的问题让你无所适从,因为你根本猜不到设备的参数和尺寸,更别提如何适配。
相关知识
android度量计算公式
- px = density * dp
- density = dpi / 160
- px = dp * (dpi / 160)
- DisplayMetrics.density
- DisplayMetrics.densityDpi
- DisplayMetrics.scaledDensity
具体的含义自行搜索,density 的差异导致适配困难;scaledDensity 是字体的缩放因子,scaledDensity 正常情况下和 density 相等,但是调节系统字体大小后会改变这个值。
查看源码,可以得知:DisplayMetrics 实例通过 Resources#getDisplayMetrics可以获得,而Resouces通过 Activity 或者 Application 的 Context 获得。
dp 和 px 的转换是通过 DisplayMetrics 中相关的值来计算的,view、bitmap 等元素在计算中的dp转换也是如此。
布局文件中 dp 的转换,最终都是调用 TypedValue#applyDimension(int unit, float value, DisplayMetrics metrics) 来进行转换。类似的,BitmapFactory#decodeResourceStream 方法也会应用 DisplayMetrics 中的参数计算。
/**
* Converts an unpacked complex data value holding a dimension to its final floating
* point value. The two parameters <var>unit</var> and <var>value</var>
* are as in {@link #TYPE_DIMENSION}.
*
* @param unit The unit to convert from.
* @param value The value to apply the unit to.
* @param metrics Current display metrics to use in the conversion --
* supplies display density and scaling information.
*
* @return The complex floating point value multiplied by the appropriate
* metrics depending on its unit.
*/
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;
}
复制代码
今日头条方案
原理
- density = px / dp
适配方案
- 给定一个宽高大小固定的标准设计图,支持以宽或高一个维度自适应适配,保持改维度和设计图一致;
- 支持dp和sp单位。
实现
修改application和activity的density,系统修改字体时打开App也能对应修改。scaledDensity计算根据系统原来的比值来获得现在修改后的值。
final float targetScaledDensity = targetDensity * (appDisplayMetrics.scaledDensity / appDisplayMetrics.density);
复制代码
在 Activity#onCreate 方法中调用下。代码比较简单,也没有涉及到系统非公开api的调用,因此理论上不会影响app稳定性。
/**
* 头条处理多设备的方案 setCustomDensity(this, getApplication());
*
* @param activity
* @param application
*/
private void setCustomDensity(Activity activity, final Application application) {
//application
final DisplayMetrics appDisplayMetrics = application.getResources().getDisplayMetrics();
if (sRoncompatDennsity == 0) {
sRoncompatDennsity = appDisplayMetrics.density;
sRoncompatScaledDensity = appDisplayMetrics.scaledDensity;
application.registerComponentCallbacks(new ComponentCallbacks() {
@Override
public void onConfigurationChanged(Configuration newConfig) {
if (newConfig != null && newConfig.fontScale > 0) {
sRoncompatScaledDensity = application.getResources().getDisplayMetrics().scaledDensity;
}
}
@Override
public void onLowMemory() {
}
});
}
//计算宽为360dp 同理可以设置高为640dp的根据实际情况
final float targetDensity = appDisplayMetrics.widthPixels / 360;
final float targetScaledDensity = targetDensity * (sRoncompatScaledDensity / sRoncompatDennsity);
final int targetDensityDpi = (int) (targetDensity * 160);
appDisplayMetrics.density = targetDensity;
appDisplayMetrics.densityDpi = targetDensityDpi;
appDisplayMetrics.scaledDensity = targetScaledDensity;
//activity
final DisplayMetrics activityDisplayMetrics = activity.getResources().getDisplayMetrics();
activityDisplayMetrics.density = targetDensity;
activityDisplayMetrics.densityDpi = targetDensityDpi;
activityDisplayMetrics.scaledDensity = targetScaledDensity;
}
复制代码
思考一刻
这个方案的解决思路很简洁,参考资料也详细的列举了它的优点,非常吸引人。但是,最终我们公司的项目没有采用这个,而是采用下面的方案。理由很简单:一次修改,全局改变。后期维护无所适从。假如一处UI出问题了,你打算怎么改?你没法改,你怎么改。
后续
它最大的贡献是对单个 Activity 或 Fragment 可以取消适配。这个思路可以解决后期维护问题,我觉得这个方案这个时候就值得推荐和使用了。同时,它还能自定义以宽或者高为维度进行适配。
AndroidScreenAdaptation 方案
原理
- 基于设计图的宽度值(或高度值)和对应的dpi适配,即根据设备的实际宽度(或高度)相对应的缩放view的尺寸。
- 缩放比率 = value * ((float) actualWidth / (float) designWidth)
适配方案
- 给定一个宽高大小固定的标准设计图,支持以宽或高一个维度自适应适配,保持改维度和设计图一致;
- 支持dp和sp单位。
实现
遍历 ViewGroup 获取所有子 View 的尺寸参数,重新计算 View 的WidthHeightFont、Padding、LayoutMargin。
/**
* Only adapter width/height/padding/margin
* Created by zhangyuwan0 on 2018/3/21.
*/
public class SimpleConversion implements IConversion {
@Override
public void transform(View view, AbsLoadViewHelper loadViewHelper) {
if (view.getLayoutParams() != null) {
loadViewHelper.loadWidthHeightFont(view);
loadViewHelper.loadPadding(view);
loadViewHelper.loadLayoutMargin(view);
}
}
}
复制代码
Activity、Fragment、自定义 View 等加载view后,手动调用LoadViewHelper#loadView 方法重计算一遍所有view。本质的转化方法是计算缩放因子。
private float calculateValue(float value) {
if ("px".equals(unit)) {
return value * ((float) actualWidth / (float) designWidth);
} else if ("dp".equals(unit)) {
int dip = dp2pxUtils.px2dip(actualDensity, value);
value = ((float) designDpi / 160) * dip;
return value * ((float) actualWidth / (float) designWidth);
}
return 0;
}
复制代码
项目实际反馈
- 简单衡量头条和AndroidScreenAdaptation的优缺点后,我们最终选择这个方案。原因:虽然所有布局都需要手动调用 ScreenAdapterTools # getInstance() # loadView(view) 方法,工作量大;但是,优点也是这个。任何 View 的适配都是可以调整和修改的,而且不会影响其它布局。
/**
* Created by guokun on 2018/7/21.
* Description: 标准宽高640x360(16:9) density = 1.0 dpi = 160
* 1. 高度低于设计高度,以高度作标准缩放;
* 2. 高度高于设计高度,但是高度:宽度 < 9:16,以高度作标准缩放;
* 3. 其余以宽度作标准缩放;
* @param
* @return
*/
public float calculateValue(float value) {
if ("px".equals(unit)) {
return value * ((float) actualWidth / (float) designWidth);
} else if ("dp".equals(unit)) {
int dip = dp2pxUtils.px2dip(actualDensity, value);
value = ((float) designDpi / 160) * dip;
if (actualHeight < designHeight || actualWidth * designHeight / designWidth > actualHeight) {
return value * ((float) actualHeight / (float) designHeight);
}
return value * ((float) actualWidth / (float) designWidth);
}
return 0;
}
复制代码
- 自定义 View 基本不支持。每个自定义 View 你需要查看源码调用 calcualteValue 重新计算参数。幸运的是项目自定义 View 不是很多和复杂。最致命的是:wrapcontent 不适配,所有的 View 必须给定尺寸;SeekBarProgress 不支持,手动反射调用方法处理适配问题。
@Override
public void transform(View view, AbsLoadViewHelper loadViewHelper) {
/**Created by guokun on 2018/7/28.
* Description: MyLinearLayout_h381特殊处理
* 1. MyLinearLayout键盘的高度大于标准高度360;
* 2. 这里UI标准图设计bug,未加上20dp 键盘top;
* */
if (view.getTag() != null && (Integer)view.getTag() == MyLinearLayout_h381.getCustomHeight(view.getContext())) {
int defaultDesign = loadViewHelper.getDesignHeight();
loadViewHelper.setDesignHeight((Integer) view.getTag());
if (view.getLayoutParams() != null) {
loadViewHelper.loadWidthHeightFont(view);
loadViewHelper.loadPadding(view);
loadViewHelper.loadLayoutMargin(view);
}
loadViewHelper.setDesignHeight(defaultDesign);
}else {
if (view.getLayoutParams() != null) {
loadViewHelper.loadWidthHeightFont(view);
loadViewHelper.loadPadding(view);
loadViewHelper.loadLayoutMargin(view);
}
}
}
复制代码
思考一刻
放弃这个项目吧,不值得。光是缺点,你都改不过来。
大总结
android 适配一直是个悬而未决的大难题。Google 提供的思路对于国内复杂的设备环境和小团队而言,代价很高。综合项目实际场景再权衡各种方案才是解决之道,因为这些方案本身并不是很大的工程。