屏幕适配 - pt 思路
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 使用
我的代码不多,自己手写下来,其实和上面我参考的文章代码差不多,区别是我添加了高度基准适配,设置可以灵活一些
- 若 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 基本不用写
- 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)
}
}