Android屏幕适配

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;
    }

你可能感兴趣的:(android,自定义view)