在Android开发中,由于Android碎片化严重,屏幕分辨率千奇百怪,而想要在各种分辨率的设备上显示基本一致的效果,。因此,屏幕的适配是Android开发者不可缺少的一部分工作。

今天,记录的是今日头条的适配方案的总结,在学习适配前可阅读下面的文章了解适配:

一种极低成本的Android屏幕适配方式

Android 目前稳定高效的UI适配方案

骚年你的屏幕适配方式该升级了!-今日头条适配方案传统dp适配方式的缺点

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

px = density * dp;

density = dpi / 160;

px = dp * (dpi / 160);

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

今日头条屏幕适配方案的核心原理在于,根据以下公式算出density

density = 当前设备屏幕总宽度(单位为像素)/ 设计图总宽度(单位为 dp) 

density 的意思就是 1 dp 占当前设备多少像素。

为什么要算出 density,这和屏幕适配有什么关系呢?

如图所示:

android 头条屏幕适配 今日头条屏幕适配原理_android 头条屏幕适配

通过系统的applyDimension()方法知道,无论你的布局文件中填写的是什么单位,最终都会转换为 px

所以我们常用的 px 转 dp 的公式 dp = px / density,就是根据上面的方法得来的,density 在公式的运算中扮演着至关重要的一步。

而今天总结的是PT的适配方案,所以需要转换成 pt 单位进行计算:公式如下:

PT = (sqrt(高^2+宽^2))/ 72

适配方案

此次适配只要是把设计图的宽度适配,而不是使用屏幕的宽度,举个栗子:

当前屏幕的宽是720 PX,设计图的宽度是375 PT,则我们就需要按照公式,按照屏幕的宽的值去转换,转换方法如下:

/**
 * 以pt为单位重新计算大小 designWidth 是设计图的宽度
 */
public static void resetDensity(Context context, float designWidth) {
    if (context == null)
        return;
    Point size = new Point();
    ((WindowManager) context.getSystemService(WINDOW_SERVICE)).getDefaultDisplay().getSize(size);
    Resources resources = context.getResources();
    resources.getDisplayMetrics().xdpi = size.x / designWidth * 72f;
    DisplayMetrics metrics = getMetricsOnMIUI(context.getResources());
    if (metrics != null)
        metrics.xdpi = size.x / designWidth * 72f;
}
/**
 * 解决MIUI屏幕适配问题
 *
 * @param resources
 * @return
 */
private static DisplayMetrics getMetricsOnMIUI(Resources resources) {
    if ("MiuiResources".equals(resources.getClass().getSimpleName()) || "XResources".equals(resources.getClass().getSimpleName())) {
        try {
            Field field = Resources.class.getDeclaredField("mTmpMetrics");
            field.setAccessible(true);
            return (DisplayMetrics) field.get(resources);
        } catch (Exception e) {
            return null;
        }
    }
    return null;
}

单位的转换也就到此为止,下面开始屏幕适配,代码如下:

public class App  extends MultiDexApplication {
    private static final int DESIGN_WIDTH = 375;
    private static App mApp;
    private static Context mContext;

    public static App getInstance() {
        if (mApp == null) {
            synchronized (App.class) {
                if (mApp == null) {
                    mApp = new App();
                }
            }
        }
        return mApp;
    }
    @Override
    public void onCreate() {
        super.onCreate();
        mContext = getApplicationContext();
        final Context context = this;
        FormatStrategy mFormatStrategy = PrettyFormatStrategy.newBuilder()
                .showThreadInfo(false)  // (可选)是否显示线程信息。默认值true
//                .methodCount(5)         // (可选)要显示的方法行数。默认值2
//                .methodOffset(7)        // (可选)隐藏内部方法调用到偏移量。默认值5
//                .logStrategy() // (可选)更改要打印的日志策略。默认LogCat
                .tag("UIAdaptive")   // (可选)每个日志的全局标记。默认PRETTY_LOGGER .build
                .build();
        //log日志打印框架Logger
        Logger.addLogAdapter(new AndroidLogAdapter(mFormatStrategy));
        registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
            @Override
            public void onActivityCreated(Activity activity, Bundle bundle) {
                activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); //限制竖屏
                Log.e("TAG","状态栏高度"+getStatusBarHeight());
//                a(activity);
                resetDensity(context, DESIGN_WIDTH);
                resetDensity(activity, DESIGN_WIDTH);
                setImmersiveStatusBar(activity);
                Log.e("TAG","状态栏高度"+getStatusBarHeight());
                Logger.e("大小"+getNotchSize(context).length);
                Logger.d(getNotchSize(context));


                if (activity instanceof IActivityBase) {
                    ((IActivityBase) activity).initView();
                    ((IActivityBase) activity).initData();
                }
            }

            @Override
            public void onActivityStarted(Activity activity) {
                setToolBar(activity);
                resetDensity(context, DESIGN_WIDTH);
                resetDensity(activity, DESIGN_WIDTH);
            }

            @Override
            public void onActivityResumed(Activity activity) {
                resetDensity(context, DESIGN_WIDTH);
                resetDensity(activity, DESIGN_WIDTH);
            }

            @Override
            public void onActivityPaused(Activity activity) {

            }

            @Override
            public void onActivityStopped(Activity activity) {

            }

            @Override
            public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {

            }

            @Override
            public void onActivityDestroyed(Activity activity) {

            }
        });
    }

    /**
     * 以pt为单位重新计算大小
     */
    public static void resetDensity(Context context, float designWidth) {
        if (context == null)
            return;
        Point size = new Point();
        ((WindowManager) context.getSystemService(WINDOW_SERVICE)).getDefaultDisplay().getSize(size);
        Resources resources = context.getResources();
        resources.getDisplayMetrics().xdpi = size.x / designWidth * 72f;
        DisplayMetrics metrics = getMetricsOnMIUI(context.getResources());
        if (metrics != null)
            metrics.xdpi = size.x / designWidth * 72f;
    }

    /**
     * 解决MIUI屏幕适配问题
     *
     * @param resources
     * @return
     */
    private static DisplayMetrics getMetricsOnMIUI(Resources resources) {
        if ("MiuiResources".equals(resources.getClass().getSimpleName()) || "XResources".equals(resources.getClass().getSimpleName())) {
            try {
                Field field = Resources.class.getDeclaredField("mTmpMetrics");
                field.setAccessible(true);
                return (DisplayMetrics) field.get(resources);
            } catch (Exception e) {
                return null;
            }
        }
        return null;
    }


    /**
     * 设置状态栏
     *
     * @param activity
     */
    private void setImmersiveStatusBar(Activity activity) {
        if (activity instanceof IActivityStatusBar) {
            if (((IActivityStatusBar) activity).getStatusBarColor() != 0) {
                setTranslucentStatus(activity);
                addImmersiveStatusBar(activity, ((IActivityStatusBar) activity).getStatusBarColor());
            }
        }
    }

    /**
     * 设置状态栏为透明
     *
     * @param activity
     */
    private void setTranslucentStatus(Activity activity) {
        //******** 5.0以上系统状态栏透明 ********
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            Window window = activity.getWindow();
            window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
            window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                    | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
            window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
            window.setStatusBarColor(Color.TRANSPARENT);
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
        }
    }

    /**
     * 添加自定义状态栏
     *
     * @param activity
     */
    @TargetApi(Build.VERSION_CODES.KITKAT_WATCH)
    private void addImmersiveStatusBar(Activity activity, int color) {
        ViewGroup contentFrameLayout = activity.findViewById(android.R.id.content);
        View contentView = contentFrameLayout.getChildAt(0);
        if (contentView != null && Build.VERSION.SDK_INT >= 14) {
            contentView.setFitsSystemWindows(true);
        }

        View statusBar = new View(activity);
        ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        Window window=activity.getWindow();
        if (BangScreenUtil.getBangScreenInstance().hasBangScreen(window)){
            Logger.e("是刘海屏");
            params.height=getNotchSize(activity)[1];
        }else {
            Logger.e("不是刘海屏");
            params.height = getStatusBarHeight();
        }
//        params.height = getStatusBarHeight();
        statusBar.setLayoutParams(params);
        statusBar.setBackgroundColor(color);
        contentFrameLayout.addView(statusBar);
    }

    /**
     * 获取状态栏高度
     *
     * @return
     */
    private int getStatusBarHeight() {
        int statusBarHeight = 0;
        int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
        if (resourceId > 0) {
            statusBarHeight = getResources().getDimensionPixelSize(resourceId);
        }
        return statusBarHeight;
    }

    /**
     * 设置ToolBar
     *
     * @param activity
     */
    private void setToolBar(final Activity activity) {
        if (activity.findViewById(R.id.tool_bar) != null && ((AppCompatActivity) activity).getSupportActionBar() == null) {
            Toolbar toolbar = activity.findViewById(R.id.tool_bar);
            if (!TextUtils.isEmpty(activity.getTitle())) {
                toolbar.setTitle(activity.getTitle());
            } else {
                toolbar.setTitle("");
            }

            if (((IActivityStatusBar) activity).getStatusBarColor() != 0) {
                toolbar.setBackgroundColor(((IActivityStatusBar) activity).getStatusBarColor());
            } else {
                toolbar.setBackgroundColor(getResources().getColor(R.color.colorPrimary));
            }

            ((AppCompatActivity) activity).setSupportActionBar(toolbar);
            ActionBar actionBar = ((AppCompatActivity) activity).getSupportActionBar();
            if (actionBar != null) {
                actionBar.setHomeButtonEnabled(true);
                actionBar.setDisplayHomeAsUpEnabled(true);
            }
            toolbar.setNavigationOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    activity.onBackPressed();
                }
            });
        }
    }

    //获取华为刘海的高宽
    public static int[] getNotchSize(Context context) {
        int[] ret = new int[]{0, 0};
        try {
            ClassLoader cl = context.getClassLoader();
            Class HwNotchSizeUtil = cl.loadClass("com.huawei.android.util.HwNotchSizeUtil");
            Method get = HwNotchSizeUtil.getMethod("getNotchSize");
            ret = (int[]) get.invoke(HwNotchSizeUtil);
        } catch (ClassNotFoundException e) {
            Log.e("haha", "getNotchSize ClassNotFoundException");
        } catch (NoSuchMethodException e) {
            Log.e("haha", "getNotchSize NoSuchMethodException");
        } catch (Exception e) {
            Log.e("haha", "getNotchSize Exception");
        } finally {
            return ret;
        }
    }
}

注意:因为适配是按照设计图的宽度去进行适配的,而不是使用屏幕的适配,所以,还需要步骤,就是在每一次加班布局文件的时候,

setContentView(R.layout.activity_main);

总是需要放在

super.onCreate(savedInstanceState);之前

如图:

android 头条屏幕适配 今日头条屏幕适配原理_屏幕适配_02

到此,屏幕适配就完成了。

以上代码中还涉及到了刘海屏的适配,只是针对华为的刘海屏进行了适配,所以代码块只是使用了华为的刘海屏适配方案。

因为在华为的刘海屏手机上,如果没有进行刘海屏的适配的话,就会在屏幕上有一条白条,如图所示:

android 头条屏幕适配 今日头条屏幕适配原理_屏幕适配_03

这个白条是因为在设置系统状态栏的时候,获取系统的状态栏的高度出现了问题,在不是刘海屏的手机中,获取系统的状态栏的高度跟刘海屏的手机中获取的状态栏的高度是不一样的,在刘海屏的手机中,如果开启了刘海屏,要获取刘海屏的的状态栏的高度,如何使用这个高度去设置状态栏,白条就可以解决了。

刘海屏的适配主要的是避开刘海屏的刘海危险区域,在刘海区域下面进行我们的UI书写,也就是把刘海区域设置成状态栏,但是这个状态栏的高度就需要自己去自己获取了。这样就可以完美适配刘海屏。

刘海屏的适配在下一篇博文中在总结了,这里就简单总结了。