Android屏幕适配这个东西,真是每个Andorid开发者从入行开始就比较关注也比较头疼的问题,从多套dimens到百分比布局再到鸿神的AutoLayout还有smallestWidth最小宽度限定符适配,还有的公司重写了系统常用的布局容器,然后在onMeasure中进行等比例的缩放。这些都能达到一些挺好的效果,不过也有不少bug存在,适配起来酣畅淋漓的感觉。
后来有人想到了修改系统Density用起来比较爽,这篇文章非常不错,可惜知道的人比较少,直到今日头条在其公众号发表了一篇适配的文章一种极低成本的Android屏幕适配方式,核心思想跟上面是一样的,这种方式才迅速让更多的开发者知道,于是不少优秀的的框架出现AndroidAutoSize和AndroidUtilCode
本篇文章就把上面的原理在熟悉一下,记个笔记。如需源码请去上面开源库中寻找。
无论我们使用什么单位适配,除了px剩下的单位都会根据一定的规则进行转换,布局文件转换位置在android.util下的TypedValue#applyDimension。
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;
}
Bitmap的转换在 BitmapFactory#decodeResourceStream
public static Bitmap decodeResourceStream(Resources res, TypedValue value,
InputStream is, Rect pad, Options opts) {
...
if (opts.inTargetDensity == 0 && res != null) {
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}
...
}
上面的代码可以看到,根据不同单位转换的时候,都用到了DisplayMetrics这个类,通过metrics.density,metrics.scaledDensity等值来转化。density就是屏幕的密度,scaledDensity是字体的缩放因子,正常情况下跟density值是相等的,当改变系统字体的大小的时候这个值会变。
如果我们是用dp,上面代码中的value就是我们的dp值,可以看到px=dp*density
所以,适配的时候,保持dp值不变,我们可以修改系统的density值让它跟我们的设计图中的density值相等就好了。
通常情况下,我们适配宽或者高中的一个就行了,因为每个设备的宽高比是不一样的,有的是16:9,有的是4:3等等,所以完全显示一直是不可能的。
下面使用宽度适配一下
public class ScreenAdapter {
/**
* 参考设计图的宽 单位dp
* 比如设计图是1920*1080 按360dp为基准
* 相同分辨率的手机,屏幕的尺寸不同也会导致最后的dp值不同。
* 比如1920*1080分辨率,屏幕尺寸为5,最后是392.7dp
*/
private static float WIDTH = 320;
public static void adapter(Activity activity){
//系统的DisplayMetrics
final DisplayMetrics systemDM = Resources.getSystem().getDisplayMetrics();
//根据设计图的WIDTH计算当前的density, scaleDensity, densityDpi
float targetDensity = systemDM.widthPixels/WIDTH;
float targetScaleDensity = targetDensity *(systemDM.scaledDensity/systemDM.density);
//px = density * dp; density = dpi / 160; px = dp * (dpi / 160);
int targetDensityDpi = (int) (targetDensity*160);
//替换Activity的density, scaleDensity, densityDpi
//Activity的DisplayMetrics
final DisplayMetrics activityDM = activity.getResources().getDisplayMetrics();
activityDM.density = targetDensity;
activityDM.scaledDensity = targetScaleDensity;
activityDM.densityDpi = targetDensityDpi;
}
}
由于 API 26 及以上的 Activity#getResources()#getDisplayMetrics()
和 Application#getResources()#getDisplayMetrics()
是不同的引用,所以在 API 26 及以上适配是没有影响的,但在API26以下Activity#getResources()#getDisplayMetrics()
和 Application#getResources()#getDisplayMetrics()
是相同的引用,导致适配有问题,而Resources#getSystem()#getDisplayMetrics()
没有这个问题所以使用它了。
如果使用Application#getResources()#getDisplayMetrics()
,在手机的设置中改变字体的大小的时候,APP中无法改变,需要注册监听registerComponentCallbacks
float appScaleDensity;
//添加字体变化监听回调
application.registerComponentCallbacks(new ComponentCallbacks() {
@Override
public void onConfigurationChanged(Configuration newConfig) {
//字体发生更改,重新对scaleDensity进行赋值
if (newConfig != null && newConfig.fontScale > 0){
appScaleDensity = application.getResources().getDisplayMetrics().scaledDensity;
}
}
@Override
public void onLowMemory() {
}
});
不过如果使用Resources#getSystem()#getDisplayMetrics()
的话就没这个问题了,不需要注册上面的监听也能跟随系统改变。最后在Activity的setContentViw()方法之前调用上面类中的adapter方法就可以愉快的适配了。
缺点:
修改了系统的density值之后,所有的地方都适配了,比如第三方库中的dp值,系统库中的dp值,这就会出现一个问题,如果第三方库和系统库中的UI设计的尺寸跟我们的基准尺寸不一样,那适配肯定就会出问题了,比如AlertDialog,Toast的尺寸会变形。
可以在调用这些控件的时候先取消适配。
public static void cancelAdapter(final Activity activity) {
final DisplayMetrics systemDm = Resources.getSystem().getDisplayMetrics();
final DisplayMetrics activityDm = activity.getResources().getDisplayMetrics();
activityDm.density = systemDm.density;
activityDm.scaledDensity = systemDm.scaledDensity;
activityDm.densityDpi = systemDm.densityDpi;
}
有时候系统会重置density的值,比如WebView初始化的时候会重置DisplayMetrics#density的值导致适配失效。可以重写VebView的setOverScrollMode方法
@Override
public void setOverScrollMode(int mode) {
super.setOverScrollMode(mode);
//在这重新执行adapter的方法
}
还有一些其他的问题,可以去这两个框架的issue中查看。
从最开始的代码中可以看到除了dp,sp之外,还有一些别的单位,pt,in,mm,Android源代码和第三方库中一般都是使用的dp,sp来做单位,所以我们可以更改pt这个单位,pt,它表示一个点,是屏幕的物理尺寸,其大小为 1 英寸的 1 / 72,也就是 72pt 等于 1 英寸
//designWidth是我们设计图的px尺寸
public static void adaptWidth(Activity activity, int designWidth) {
DisplayMetrics dm = Resources.getSystem().getDisplayMetrics();
final DisplayMetrics activityDM = activity.getResources().getDisplayMetrics();
activityDM.xdpi= dm.xdpi = (dm.widthPixels * 72f) / designWidth;
}
修改DisplayMetrics中的xdpi的值,在布局文件中使用pt做单位,这样我们的修改就不会对系统和第三方的控件的大小改变了。
当然是用这种偏门的单位,对代码的入侵性就有点大,如果以后有了更好的适配方式,更改起来比较麻烦,不过也有处理的方式
从最开始那个代码中我们可以看到,如果以pt为单位,返回的是return value * metrics.xdpi * (1.0f/72);
,以dp为单位返回的是return value * metrics.density;
放到一块对比一下
return value * metrics.xdpi * (1.0f/72);
return value * metrics.density;
从上面看,如果我们把metrics.xdpi * (1.0f/72)
变成metrics.density
,那么我们在布局文件中使用dp和pt的效果就一样了。
所以,假如真有更好的方式的时候,可以通过一个取消pt适配的方式,把上面给改了,到时候dp和pt效果一样,我们就不用去布局文件中一个一个的更改了。取消方式
public static void cancelPtAdapter(final Activity activity) {
final DisplayMetrics dm = Resources.getSystem().getDisplayMetrics();
final DisplayMetrics activityDm = activity.getResources().getDisplayMetrics();
activityDm.xdpi = dm.xdpi = dm.density * 72;
}