Android屏幕适配

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

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

单个 Activity 适配

1
2
3
4
5
class MainActivity : ComponentActivity() {
override fun attachBaseContext(newBase: Context) {
super.attachBaseContext(newBase.createCustomDensityContext())
}
}

全局适配

1
2
3
4
5
6
7
8
9
10
11
12
13
class MainActivity : ComponentActivity() {
override fun attachBaseContext(newBase: Context) {
newBase.setCustomDensity()
super.attachBaseContext(newBase)
}
}

class MainApplication: Application() {
override fun attachBaseContext(base: Context) {
base.setCustomDensity()
super.attachBaseContext(base)
}
}

只适配 这个 View 及其 子 view

谨慎使用,createCustomDensityContext 返回的 context 不是 Activity,
如果 子view 有地方拿 context 当做 Activity 使用就会有问题

1
2
3
class FooView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : FrameLayout(context.createCustomDensityContext(), attrs)

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
package com.xechoz.lib

import android.app.Application
import android.content.ComponentCallbacks
import android.content.Context
import android.content.res.Configuration
import android.util.AttributeSet
import android.util.DisplayMetrics
import android.view.ContextThemeWrapper
import android.widget.FrameLayout

/**
* 以 uiWidthDp 为基准,按比例适配 整个 Layout 里的元素
*
* 原理是 用 uiWidthDp 跟实际的屏幕 dp 比例,
* 计算出新的 density, 替换 DisplayMetrics 的 density
*
* usage:
* 在 xml 布局里用
* <CustomDensityLayout>
* <your view>
* </CustomDensityLayout>
*
* 在自定义 View里用:
* FooView: BarView(context = context.asDensityContext(your_target_width_dp))
*
* 参考:
* [一种极低成本的Android屏幕适配方式](https://mp.weixin.qq.com/s/d9QCoBP6kV9VSWvVldVVwA?mode=light)
*/


/**
* 适用 全局适配的 场景
* 适配 整个 Resources, Activity resources 是共享的,改了一个会全部改掉。
*
* // 有些代码使用的 application 获取 resource, 这个也要适配
* class MainApplication: Application() {
* override fun attachBaseContext(base: Context) {
* base.setCustomDensity()
* super.attachBaseContext(base)
* }
* }
*
* // 全部 Activity 都适配。 或者在 BaseActivity
* class MainActivity : ComponentActivity() {
* override fun attachBaseContext(newBase: Context) {
* newBase.setCustomDensity()
* super.attachBaseContext(newBase)
* }
* }
*/
fun Context.setCustomDensity(uiWidthDp: Int = 360) {
resources.displayMetrics.setCustomDensity(newCustomDensity(uiWidthDp))
}

/**
* 创建一个 新的 context 适配 Density.
*
* [uiWidthDp] 设计稿屏幕 宽度 的dp,默认 360 dp
*
* usage:
*
* // 单个 Activity 适配
* class MainActivity : ComponentActivity() {
* override fun attachBaseContext(newBase: Context) {
* super.attachBaseContext(newBase.createCustomDensityContext())
* }
* }
*
* // 全部 Activity 都换掉
* class MainActivity : ComponentActivity() {
* override fun attachBaseContext(newBase: Context) {
* newBase.setCustomDensity()
* super.attachBaseContext(newBase)
* }
* }
*
* // 只适配 这个 View 及其 子 view
* // 谨慎使用,createCustomDensityContext 返回的 context 不是 Activity,
* // 如果 子view 有地方拿 context 当做 Activity 使用就会有问题
*
* class FooView @JvmOverloads constructor(
* context: Context, attrs: AttributeSet? = null
* ) : FrameLayout(context.createCustomDensityContext(), attrs)
*
* @see setCustomDensity
*/
fun Context.createCustomDensityContext(uiWidthDp: Int = 360): Context {
val custom = newCustomDensity(uiWidthDp)

val copyConfiguration = Configuration()
copyConfiguration.densityDpi = custom.targetDensityDpi
copyConfiguration.setTo(resources.configuration)

val copyContext = createConfigurationContext(copyConfiguration)

val copyMetrics = DisplayMetrics()
copyMetrics.setTo(resources.displayMetrics)
copyMetrics.setCustomDensity(custom)

copyContext.resources.displayMetrics.setTo(copyMetrics)
return if (this is ContextThemeWrapper) ContextThemeWrapper(copyContext, theme) else copyContext
}

/**
*
* 只适配 这个 View 及其 子 view
* 谨慎使用,createCustomDensityContext 返回的 context 不是 Activity,
* 如果 子view 有地方拿 context 当做 Activity 使用就会有问题
*
*/
class CustomDensityLayout @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : FrameLayout(context.createCustomDensityContext(uiWidthDp), attrs) {
companion object {
var uiWidthDp = 360 // 设计稿屏幕 宽度 的dp,默认 360 dp
}
}

/// private ////
private var nonCompatDensity = 0f
private var nonCompatScaledDensity = 0f

private fun Context.newCustomDensity(toWidthDp: Int): CustomDensity {
(this as? Application ?: applicationContext as? Application)?.let(::updateNonCompatDensity)

val targetDensity = 1f * resources.displayMetrics.widthPixels / toWidthDp
val targetScaleDensity = 1f * targetDensity * (nonCompatScaledDensity / nonCompatDensity)
val targetDensityDpi = (160 * targetScaleDensity).toInt()

return CustomDensity(
targetDensity = targetDensity,
targetScaledDensity = targetScaleDensity,
targetDensityDpi = targetDensityDpi
)
}

private fun updateNonCompatDensity(application: Application) {
val displayMetrics = application.resources.displayMetrics
if (nonCompatDensity == 0f) {
nonCompatDensity = displayMetrics.density
nonCompatScaledDensity = displayMetrics.scaledDensity

application.registerComponentCallbacks(object : ComponentCallbacks {
override fun onConfigurationChanged(newConfig: Configuration) {
if (newConfig.fontScale > 0) {
nonCompatScaledDensity = application.resources.displayMetrics.scaledDensity
}
}

override fun onLowMemory() {
}
})
}
}

private fun DisplayMetrics.setCustomDensity(custom: CustomDensity) {
density = custom.targetDensity
scaledDensity = custom.targetScaledDensity
densityDpi = custom.targetDensityDpi
}

private data class CustomDensity(
val targetDensity: Float,
val targetScaledDensity: Float,
val targetDensityDpi: Int
)