安卓

屏幕适配 - pt 思路

2018-12-31  本文已影响2人  前行的乌龟

ps: 18 年收官了, 今年的最后一篇了,祝愿自己来年顺顺利利的,事业大步向前进!!!

我之前公司的适配方案是 sw 方案,之前的一个前辈自己手写的 java 脚本可以自动生成相应的 values 文件夹,可惜哪个脚本找不到了,到现在我还是相当佩服前辈高超的编码水平

但是有个问题,有时 UI 反应,适配不如 IOS 精准,sw 最小宽度适配发是去跟前周边去找对应的 values 文件夹,或多或少有 sw 最小宽度dp 值有些差距,另外不能以高度做适配,所以我现在换成 pt 适配发了

pt 适配发的思路来源于:一种粗暴快速的Android全屏幕适配方案,作者思路之巧妙前所未有啊,我真是佩服的五体投地啊

本文项目地址:BW_Libs


适配效果

宽度适配 高度适配

高度适配这里有点坑,坑的地方是,系统给你 window 的高度时包含状态栏,但不含底部导航栏高度的,看上图2,我的高度以 1920 为基准,我不显示状态栏,然后让底部导航栏透明,大家看看正好是这个位置

但是我们实际不知道用户的手机有没有底部导航栏这个玩意,所以高度适配天然的不合适。


pt 核心思路

一切的起点都是起源于 -> 系统单位转换公式

TypedValue.applyDimension(int unit, float value, DisplayMetrics metrics)

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

大家还记得我们学数据库时的数据注入吗,这里用的就是这个思路

我们设想这样一个场景,Ui 出图了,是用 px 标记的,然后也没说屏幕尺寸,我们无从计算 dp 值,也不知道屏幕密度是几,这时候怎么办,明显 dp 思路不行了

那么我们就走到死胡同没有路了嘛,不是啊,大家还记得前几年洪洋大神的百分比布局吗,我们可以用百分比的思路来做啊,既然 UI 给的 px 标记,那我们就把 view 的 px 看成屏幕宽高的百分比

比如我们以宽度为基准,width = 750 px ,view width = 200,那么 view 的宽度我们就可以看成未来安装设备屏幕宽度的 750 分之 200,这样只要我们能在 app 中计算出来这个比例设置给 view 就能达成我们想要的效果了

但是我们不能写一个 view 就去计算一次吧,这样平凡的刷新 UI 一个是性能底下,一个是效率极低,难么有没有一个地方可以让我们统一设置呢

有啊,这个就是屏幕矩阵了 displayMetrics,GUP 最终也是拿到系统计算出的每一帧的 bitmap 去渲染显示出来的,所以看到矩阵不要惊讶,本来就是如此

displayMetrics 里面有哪些参数:


看到没有,和显示相关的都在这里面,我们只要改了 displayMetrics 里面的值,整个 app 都能生效了

然后从哪里获取呢,有3个地方:

Resources.getSystem().displayMetrics
Activity.getSystem().displayMetrics
Application.getSystem().displayMetrics

注意啊,api 26 以上 Activity.getSystem().displayMetrics 和 Application.getSystem().displayMetrics 获取的同一个类,但是 26 以下就不是一个类了,Resources.getSystem().displayMetrics 这个不能改,这个改了整个手机都跟着变

然后我们现在再去看系统的单位转换公式,这时我们就能去做了,可以看到里面有很多单位的,我们操作哪一个呢,dp 不能动,dp 动了系统自带的部分就要出问题了,所以我们选择没什么影响的 pt

看 pt 的公式:

        case COMPLEX_UNIT_PT:
            return value * metrics.xdpi * (1.0f/72);

这里我们能操作的就是 metrics.xdpi 了,大家想,我们想用 pt 表示我们的百分比,那么理想的公式就是:

        // UIWidth = UI 出图的屏幕宽度
       wantValue = displayMetrics.x  * (ptValue / UIWidth )

这个我们要用 metrics.xdpi 把我们这个公式补全,那么 metrics.xdpi 就变成了一个等式了:

        metrics.xdpi = 当前屏幕宽度 / UI 屏幕宽度 * 72f

这里 * 72 是补平公式,因为 原生 pt 转换公式里 / 72 了

最终:

    fun resizeDisplayMetrics(context: Context?, autoWidth: Float, baseLine: Int) {

        if (context == null) {
            return
        }

        if (!isCanResize(context, baseLine)) {
            return
        }

        val screenPoint = Point()
        var resources = context.resources
        var displayMetricsonMiui = getMetricsOnMiui(resources)

        (context.getSystemService(WINDOW_SERVICE) as WindowManager).defaultDisplay.getSize(screenPoint)

        when (baseLine) {

            BASE_LINE_WIDTH -> {
                resources.displayMetrics.xdpi = screenPoint.x / autoWidth * 72f
                if (displayMetricsonMiui != null) {
                    resources.displayMetrics.xdpi = screenPoint.x / autoWidth * 72f
                }
            }

            BASE_LINE_HRIGHT -> {
                resources.displayMetrics.xdpi = screenPoint.y / autoWidth * 72f
                if (displayMetricsonMiui != null) {
                    resources.displayMetrics.xdpi = screenPoint.y / autoWidth * 72f
                }
            }
        }
    }

不管以宽为基准,还是以高为基准,更换的都是 displayMetrics.xdpi 这个参数

xml 使用:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.bloodcrown.bw.screenauto.WidthActivity">

    <TextView
        android:id="@+id/tx01"
        android:layout_width="1000pt"
        android:layout_height="300pt"
        android:background="@color/colorAccent"
        android:gravity="center"
        android:text=" 1000pt/200pt "
        tools:layout_height="300px"
        tools:layout_width="1080px"/>

    <TextView
        android:id="@+id/tx02"
        android:layout_width="500pt"
        android:layout_height="300pt"
        android:background="@color/colorPrimary"
        android:gravity="center"
        android:text=" 500pt/200pt "
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toBottomOf="@id/tx01"
        tools:layout_height="300px"
        tools:layout_width="500px"/>

    <TextView
        android:id="@+id/tx03"
        android:layout_width="500pt"
        android:layout_height="300pt"
        android:background="@color/colorPrimaryDark"
        android:gravity="center"
        android:text=" 500pt/200pt "
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/tx01"
        tools:layout_height="300px"
        tools:layout_width="500px"/>

</android.support.constraint.ConstraintLayout>

使用 pt 之后,xml 预览就肯定不对了,这里我们使用 tools 设置预览时 view 宽高的 px 值,只要选对分辨率的设备,效果还是一样的,就是麻烦一些


api 使用

我的代码不多,自己手写下来,其实和上面我参考的文章代码差不多,区别是我添加了高度基准适配,设置可以灵活一些

  1. 若 app 只需要一种适配基准,比如宽度,那么我们统一在 application 中设置一下即可,为了兼容在 ActivityLifecycleCallbacks 回掉里再设置一下
class MyApplication : Application() {

    override fun onCreate() {
        super.onCreate()

        ScreenAutoManager.instance.init(this, 1080.0f, ScreenAutoManager.BASE_LINE_WIDTH)

        this.registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {

            override fun onActivityCreated(activity: Activity?, savedInstanceState: Bundle?) {
                ScreenAutoManager.instance.onActivityCreated(activity)
                Log.d("AA", "onActivityCreated")
            }

            override fun onActivityResumed(activity: Activity?) {
                ScreenAutoManager.instance.onActivityResumed(activity)
                Log.d("AA", "onActivityResumed")
            }

            override fun onActivityStarted(activity: Activity?) {
                ScreenAutoManager.instance.onActivityStarted(activity)
                Log.d("AA", "onActivityStarted")
            }
        })
    }

其实 Resumed 和 Started 基本不用写

  1. app 内有2种适配

啃爹的来了,这么设计绝对反人类,要是我我肯定不干。一种思路是大家在 base 基类里写一个适配的方法作为大部分页面适配的基础,然后不一样的代码,再重写该方法,另一种思路就是每个页面都写一遍

宽度为基准

ScreenAutoManager.instance.cleanResize(this)
ScreenAutoManager.instance.resizeDisplayMetrics(this,1008f,ScreenAutoManager.BASE_LINE_WIDTH)

高度为基准

ScreenAutoManager.instance.cleanResize(this)
ScreenAutoManager.instance.resizeDisplayMetrics(this,1920f,ScreenAutoManager.BASE_LINE_HRIGHT)

为了排除相互影响,在设置自己页面的适配基准之前,先清除之前的适配基准,在 setContentView 之前调用


代码参上

class ScreenAutoManager {

    lateinit var application: Application
    var autoWidth: Float = 1080f
    var baseLine: Int = BASE_LINE_WIDTH

    companion object {

        var BASE_LINE_WIDTH: Int = 1
        var BASE_LINE_HRIGHT: Int = 2

        var instance: ScreenAutoManager = ScreenAutoManager()
    }

    fun init(application: Application, autoWidth: Float, baseLine: Int) {
        instance.application = application
        instance.autoWidth = autoWidth
        instance.baseLine = baseLine
        instance.resizeDisplayMetrics(application, autoWidth, baseLine)
    }

    fun isCanResize(context: Context?, baseLine: Int): Boolean {

        if (context == null) {
            return false
        }

        when (baseLine) {
            BASE_LINE_WIDTH -> {
                if (Resources.getSystem().displayMetrics.xdpi == context.resources.displayMetrics.xdpi) {
                    return true
                }
            }
            BASE_LINE_HRIGHT -> {
                if (Resources.getSystem().displayMetrics.ydpi == context.resources.displayMetrics.ydpi) {
                    return true
                }
            }
        }

        return false
    }

    fun resizeDisplayMetrics(context: Context?, autoWidth: Float, baseLine: Int) {

        if (context == null) {
            return
        }

        if (!isCanResize(context, baseLine)) {
            return
        }

        val screenPoint = Point()
        var resources = context.resources
        var displayMetricsonMiui = getMetricsOnMiui(resources)

        (context.getSystemService(WINDOW_SERVICE) as WindowManager).defaultDisplay.getSize(screenPoint)

        when (baseLine) {

            BASE_LINE_WIDTH -> {
                resources.displayMetrics.xdpi = screenPoint.x / autoWidth * 72f
                if (displayMetricsonMiui != null) {
                    resources.displayMetrics.xdpi = screenPoint.x / autoWidth * 72f
                }
            }

            BASE_LINE_HRIGHT -> {
                resources.displayMetrics.xdpi = screenPoint.y / autoWidth * 72f
                if (displayMetricsonMiui != null) {
                    resources.displayMetrics.xdpi = screenPoint.y / autoWidth * 72f
                }
            }
        }
    }

    fun getMetricsOnMiui(resources: Resources): DisplayMetrics? {
        if ("MiuiResources" == resources.javaClass.simpleName || "XResources" == resources.javaClass.simpleName) {
            try {
                val field = Resources::class.java.getDeclaredField("mTmpMetrics")
                field.isAccessible = true
                return field.get(resources) as DisplayMetrics
            } catch (e: Exception) {
                return null
            }
        }
        return null
    }

    fun cleanResize(context: Context) {

        context.resources.displayMetrics.xdpi = Resources.getSystem().displayMetrics.xdpi

        val metrics = getMetricsOnMiui(context.resources)
        metrics?.xdpi = Resources.getSystem().displayMetrics.xdpi
    }

    fun onActivityCreated(activity: Activity?) {
        //通常情况下application与activity得到的resource虽然不是一个实例,但是displayMetrics是同一个实例,只需调用一次即可
        //为了面对一些不可预计的情况以及向上兼容,分别调用一次较为保险
        resizeDisplayMetrics(application, autoWidth, baseLine)
        resizeDisplayMetrics(activity, autoWidth, baseLine)
    }

    fun onActivityStarted(activity: Activity?) {
        resizeDisplayMetrics(application, autoWidth, baseLine)
        resizeDisplayMetrics(activity, autoWidth, baseLine)
    }

    fun onActivityResumed(activity: Activity?) {
        resizeDisplayMetrics(application, autoWidth, baseLine)
        resizeDisplayMetrics(activity, autoWidth, baseLine)
    }

}
上一篇下一篇

猜你喜欢

热点阅读