android 今日头条的屏幕适配理解

6,820 阅读6分钟

前一段时间无意中看到今日头条的适配方案,使用到项目中,感觉真的是无比丝滑。所以特意写一篇文章分享给小伙伴们!

本文知识点:

  • 为什么要做屏幕适配
  • 今日头条的适配方案(划重点)
  • 今日头条的适配方案的一些问题

1. 为什么要做屏幕适配

做Android开发的都了解,由于Android屏幕碎片化严重,虽然Android官方提供了dp为单位的适配方案,但是由于各种千奇百怪的机型,所以变现往往不尽如人意。所以需要进行屏幕适配。说白了就是让所有机型都进行保持UI的设计原貌!

2. 今日头条的适配方案

终于到了本文的重点了。为了大家能深刻理解其中的含义,这里从最基本的开始说起。

2.1 传统的dp适配的流程

android中的dp在渲染前会将dp转为px,计算公式:

px = density * dp;
density = dpi / 160;
px = dp * (dpi / 160);

而dpi是根据屏幕真实的分辨率和尺寸来计算的,每个设备都可能不一样的。那么dpi是怎么计算的呢?

上面图片说明dpi是怎么计算得来的。举个例子,当屏幕分辨率为1920 * 1080屏幕尺寸为5寸的手机。计算得来的dpi为440。不信的话可以计算一下!

那么问题来了?

假设我们UI设计图是按屏幕宽度为360dp来设计的,那么在上述设备上,屏幕宽度其实为1080/(440/160)=392.7dp,也就是屏幕是比设计图要宽的。这种情况下, 即使使用dp也是无法在不同设备上显示为同样效果的。 同时还存在部分设备屏幕宽度不足360dp,这时就会导致按360dp宽度来开发实际显示不全的情况。

而且上述屏幕尺寸、分辨率和像素密度的关系,很多设备并没有按此规则来实现, 因此dpi的值非常乱,没有规律可循,从而导致使用dp适配效果差强人意。

3.2 今日头条的适配方式说明

其实,当我们拿到设计图的时候,一般都是根据苹果的6进行设计的,往往在Android中,存在16:9和4:3的一些机型,那么这些机型中的宽高比不同,如果想完全按照设计图进行适配是不可能的,也是不现实的。但是如果我们以一个维度,也就是宽这个维度来进行适配的话,如果高度超出了屏幕我们就使用可滑动的控件进行展示。这就是今日头条的适配方案。

因此,采用以宽度为标准去进行适配,保持该维度上和设计图一致

2.3 今日头条的适配方案

先科普几个内容,

  • dp和px的转换公式为:px = dp * density
  • dp转换的场景都是通过DisplayMetrics来进行计算的,
  • DisplayMetrics#density 就是上述的density
  • DisplayMetrics#densityDpi 就是上述的dpi
  • DisplayMetrics#scaledDensity 字体的缩放因子,正常情况下和density相等,但是调节系统字体大小后会改变这个值

因为所有关于dp的计算都是通过DisplayMetrics这个类进行的。所以只需要针对这个类进行操作就可以了。

我简单把DisplayMetrics类分为三个层面,第一个是System(可以理解成初始分配)的,第二个是APP(可以理解成Application)的,第三个是Activity的。当你适配的时候,尽量不要去修改第一个System中的Displaymetris的,因为可能第三方的库不会按照你的方式去适配,所以这里只修改后面两个就可以了。第一个不修改是便于之后的还原!!!

以下是三个层面获取DisplayMetrics中的代码:

// 系统的屏幕尺寸
final DisplayMetrics systemDm = Resources.getSystem().getDisplayMetrics();
// app整体的屏幕尺寸
final DisplayMetrics appDm = Utils.getApp().getResources().getDisplayMetrics();
// activity的屏幕尺寸
final DisplayMetrics activityDm = activity.getResources().getDisplayMetrics();

接下来我们看看需要怎么适配,这里就只以屏幕宽度为基准进行相应的适配了。这里模拟360dp为基准的适配,当然这个值你是可以修改成任何尺寸的!

  1. 先计算一下屏幕的宽度
//这里widthPixels代表屏幕的宽度
activityDm.density = activityDm.widthPixels / 360;
  1. 计算一下字体的density
//这里通过一个比例确定activity字体的density
activityDm.scaledDensity = activityDm.density * (systemDm.scaledDensity / systemDm.density);
  1. 计算相应的dpi
//上面有相应的公式
activityDm.densityDpi = (int) (160 * activityDm.density);
  1. 复制相应的内容
//进行相应的赋值操作
appDm.density = activityDm.density;
appDm.scaledDensity = activityDm.scaledDensity;
appDm.densityDpi = activityDm.densityDpi;

整体代码如下:

/**
 * 适配的主要代码
 *
 * @param activity        上下文
 * @param sizeInPx        你要适配的相应尺寸
 * @param isVerticalSlide 水平还是垂直为参考
 */
 private static void adaptScreen(final Activity activity,
                                    final int sizeInPx,
                                    final boolean isVerticalSlide) {
        // 系统的屏幕尺寸
        final DisplayMetrics systemDm = Resources.getSystem().getDisplayMetrics();
        // app整体的屏幕尺寸
        final DisplayMetrics appDm = Utils.getApp().getResources().getDisplayMetrics();
        // activity的屏幕尺寸
        final DisplayMetrics activityDm = activity.getResources().getDisplayMetrics();
        if (isVerticalSlide) {
            activityDm.density = activityDm.widthPixels / (float) sizeInPx;
            Log.e(TAG, "adaptScreen: "+activityDm.widthPixels );
        } else {
            activityDm.density = activityDm.heightPixels / (float) sizeInPx;
        }
        // 字体的缩放因子,这个是通过一个比例计算得来的!
        activityDm.scaledDensity = activityDm.density * (systemDm.scaledDensity / systemDm.density);
        // 计算得到相应的dpi
        activityDm.densityDpi = (int) (160 * activityDm.density);

        //进行相应的赋值操作
        appDm.density = activityDm.density;
        appDm.scaledDensity = activityDm.scaledDensity;
        appDm.densityDpi = activityDm.densityDpi;
    }

因为上面涉及到横竖屏的问题,所以这里有个if判断。上面是主要代码。

3 今日头条的适配方案的一些问题

3.1 适配之后Toast的问题?

进行上面的适配之后,Toast会变得很小。其实也不难理解,因为你修改了APP的density,所以整个图片的界面都会发生相应的变化也就很好理解了。那么怎么解决呢?其实就想上面说的,使用System的density对App和Activity进行还原。怎么说呢?其实就是在show()方法之前还原,在之后在进行适配。

怎么取消呢?看下面的代码。

    public static void cancelAdaptScreen(final Activity activity) {
        final DisplayMetrics systemDm = Resources.getSystem().getDisplayMetrics();
        final DisplayMetrics appDm = Utils.getApp().getResources().getDisplayMetrics();
        final DisplayMetrics activityDm = activity.getResources().getDisplayMetrics();
        activityDm.density = systemDm.density;
        activityDm.scaledDensity = systemDm.scaledDensity;
        activityDm.densityDpi = systemDm.densityDpi;

        appDm.density = systemDm.density;
        appDm.scaledDensity = systemDm.scaledDensity;
        appDm.densityDpi = systemDm.densityDpi;
    }

其实就是使用System的density把APP和Activity的density修改回来就可以了!

然后在show()方法之后使用下面方法重新对界面进行适配!

    public static void restoreAdaptScreen(Activity activity, boolean isVerticalSlide, int sizeInPx) {
        final DisplayMetrics systemDm = Resources.getSystem().getDisplayMetrics();
        final DisplayMetrics appDm = Utils.getApp().getResources().getDisplayMetrics();
        final DisplayMetrics activityDm = activity.getResources().getDisplayMetrics();
        if (isVerticalSlide) {
            activityDm.density = activityDm.widthPixels / (float) sizeInPx;
        } else {
            activityDm.density = activityDm.heightPixels / (float) sizeInPx;
        }
        activityDm.scaledDensity = activityDm.density * (systemDm.scaledDensity / systemDm.density);
        activityDm.densityDpi = (int) (160 * activityDm.density);

        appDm.density = activityDm.density;
        appDm.scaledDensity = activityDm.scaledDensity;
        appDm.densityDpi = activityDm.densityDpi;
    }

调用代码就变成了这个样子

//取消适配
ScreenUtils.cancelAdaptScreen(this);
//弹出Toast
Toast.makeText(this, "点击了第一个内容", Toast.LENGTH_SHORT).show();
//重新适配
ScreenUtils.restoreVerticalAdaptScreen(this, 720);

像什么Toast、dialog什么的都会出现上面的情况,所以解决办法是一样的

3.2 webview加载后发现density复原

由于 WebView 初始化的时候会还原 density 的值导致适配失效,继承 WebView,重写如下方法:

@Override
public void setOverScrollMode(int mode) {
    super.setOverScrollMode(mode);
    ScreenUtils.restoreAdaptScreen();
}

特别感谢: blankj的Android 屏幕适配从未如斯简单