上篇一口气没把所有的适配方案介绍完,今天来收个尾。
smallestwidth限定符
smallestwidth限定符和我们上篇提到的宽高限定符的套路一样, 只不过是以dp
作为单位,并以设备最短边的dp值作为匹配依据。
设计稿以iphone6
为标准,屏幕尺寸为4.7
英寸,分辨率为750x1334
,density
为2
。
我们先将设计稿宽度像素值转换dp
值。
px
怎么转dp
还记得否?公式再来一次。
px = dp * densityDpi / 160 = dp * density
750px = 750 / density = 375dp
接着,我们计算待适配手机的最小宽度dp
值,我们还是以小米9
和Pixel xl
为例:
机型 | 分辨率 | 屏幕尺寸 | ppi | dpi |
---|---|---|---|---|
小米9 | 1080x2340 | 6.39英寸 | 403 | 440 |
Pixel xl | 1440x2560 | 5.5英寸 | 534 | 560 |
1080<2340
, 所以小米9
最小宽度取1080px
, 转换为dp
值为:
1080px = 1080 / (densityDpi / 160 ) = 1080 / (440 / 160) = 393dp
1440<2560
, 所以Pixel xl
最小宽度取1440px
, 转换为dp
值为:
1440px = 1440 / (densityDpi / 160 ) =1440 / (560 / 160) = 411dp
如设计稿中某控件宽度为10dp
,为了保证比例,我们需要将设计稿dp
值转换为实际dp值,转换公式为:
实际dp = 设计稿尺寸 * (设备最小宽度/设计稿最小宽度)
那么在小米9
上应该是10.48dp = (10 * 395/375)
;
Pixel xl
上应该是10.96dp =(10* 560/375)
。
接着我们在values-sw393dp
的dimens
文件和values-sw411dp
的dimens
文件中分别定义10dp
转换后的值。
res/
values/
dimens.xml
values-sw393dp/
dimens.xml --> <dimen name="dp_10">10.48dp</dimen>
values-sw411dp/
dimens.xml --> <dimen name="dp_10">10.96dp</dimen>
这样app
在运行时,就根据最小宽度取对应资源文件夹下的尺寸了。且视觉效果保持一致:
10/375 = 10.48/393 = 10.93/411 ≈ 2.66%
在实际开发中我们怎么操作呢?跟宽高限定符类似。
- 计算设计稿的最小宽度dp值
N
; - 枚举所有待适配继续最小宽度的dp值
W
, 创建values-sw${W}dp
文件夹; - 将最小宽度分成
N
份,通过转换公式,计算每份在各机型上的实际dp
值;
同样上述的步骤随便写个脚本就可以自动生成, 我们把上篇的demo
改改
package cc.wecando.dpadaptergenerator
import java.io.BufferedWriter
import java.io.File
import java.io.FileWriter
import java.math.BigDecimal
class DpAdapterGenerator(private val designWidth: Int, private val adapterSmallWidths: List<Int>) {
fun generate() {
adapterSmallWidths.forEach {
val endFix = "sw${it}dp"
val dir = File("values-${endFix}")
if (!dir.exists() || !dir.isDirectory) {
dir.mkdirs()
}
val dimensFile = File(dir, "dimens.xml")
if (dimensFile.exists()) {
dimensFile.delete()
}
val bufferedWriter = BufferedWriter(FileWriter(dimensFile))
bufferedWriter.write("""<?xml version = "1.0" encoding = "utf-8"?>""")
bufferedWriter.newLine()
bufferedWriter.write(""" <resources >""")
bufferedWriter.newLine()
for (i in 1..designWidth) {
val dp = BigDecimal(it).multiply(BigDecimal(i)).divide(BigDecimal(designWidth))
.setScale(2, BigDecimal.ROUND_HALF_UP).toFloat()
bufferedWriter.write(""" <dimen name="dp_${i}">${dp}dp</dimen>""")
bufferedWriter.newLine()
}
bufferedWriter.write(""" </resources>""")
bufferedWriter.close()
}
}
}
fun main() {
val adapterSizes = listOf(
393,
411
)
val generator = DpAdapterGenerator(375, adapterSizes)
generator.generate()
}
smallestwidth限定符
原理与宽高限定符
相同,都是利用Android
资源匹配规则,实现在不同设备上使用不同的dp值。我们需要做的就是针对最小宽度
的设备,计算每份对应的dp值。
同样,他的缺点跟宽高限定符
差不多, 并且只能以最小宽度
一个维度进行适配。
今日头条方案
我们屏幕适配的核心是什么? 无非是将设计稿上的尺寸,经过转换,保证在各种分辨率上 转换后的尺寸所占比例与设计稿保持一致。
这个转换过程,宽高限定符
,smallestwidth限定符
是利用Android
资源匹配策略。AndroidAutoLayout
是通过自定义控件,收集所有尺寸相关的属性动态转换。
回过头想想, Android Api
本身不就有这种转换过程吗?
第一篇文章我们已经分析过了dp
转px
,sp
转px
,Android
框架中计算尺寸最终都会调用TypedValue#applyDimension
方法。 将各种单位的尺寸转换为px
。
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;
}
其实今日头条的方案原理超级简单,就是利用这里的转换做文章。
拿dp
举例,布局文件中的dp值,最终会转换为px。
case COMPLEX_UNIT_DIP:
return value * metrics.density;
假如宽度为designWidth = 750px
,density
为2
,designWidthDpi=375dp
的设计稿中,定义一个空间宽度为designDp =10dp
。
在布局文件中直接使用designDp
,若要完美适配,则在宽度为targetWidth
的设备上,dp
转换为targetPx
后,应满足如下公式:
因为
代入等式后:
设计稿确定后,designWidthDpi
也就固定了,设备的宽度targetWidth
也是固定值没法修改,要想保持等式成立,那么只能修改metrics.density
。
恰好DisplayMetrics#density
是public
的,因此我们只需要根据公式,算出density
,然后修改设备的density
值,就可以完成dp
的适配了。
根据公式 计算出density
小米9
的density = targetWidht/designWidthDpi = 1080/375=2.88
Pixel xl
的density= targetWidht/designWidthDpi = 1440/375=3.84
假设设计稿中存在控件宽度分别是20px
,40px
,布局文件中设置:android:layout_width=10dp
,android:layout_width=20dp
。
修改小计9
,Pixel xl
的density,dp
转换为px
后分别是29px
,58px
和38px
,77px
20/750 = 29/1080 = 38/1440 = 2.6%
40/750 = 58/1080 = 77/1440 = 5.3%
同理,我们修改scaledDensity
就可以适配sp
了。当调整设置中的缩放比例时,需要重新计算scaledDensity
,在自定义application
中通过registerComponentCallbacks
注册相应的callbacks
就行了。
对于图片的decode
,参考BitmapFactory#decodeResourceStream
方法:
public static Bitmap decodeResourceStream(@Nullable Resources res, @Nullable TypedValue value,
@Nullable InputStream is, @Nullable Rect pad, @Nullable Options opts) {
if (opts.inTargetDensity == 0 && res != null) {
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}
}
所以还修改densityDpi
.
总结一下,今日头条的方案实际很简单,就是修改设备density
、’scaleDensity
、densityDpi
的值,这里提供下demo
:
class BaseApplication : Application() {
private val isBaseOnWidth = true
private var mInitDensity = -1f
private var mInitDensityDpi = -1
private var mInitScaledDensity = -1f
private var mInitWidth = -1
private var mInitHeight = -1
private val activityLifecycleCallback = object : AdapterActivityLifecycleCallbacks() {
override fun onActivityStarted(activity: Activity) {
Log.d("yaocai", "BaseApplication onActivityStarted")
adapter(activity)
}
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
Log.d("yaocai", "BaseApplication onActivityCreated")
adapter(activity)
}
}
private val componentCallbacks = object : ComponentCallbacks {
override fun onLowMemory() {
}
override fun onConfigurationChanged(newConfig: Configuration) {
if (newConfig.fontScale > 0) {
if (newConfig.fontScale > 0) {
mInitScaledDensity =
Resources.getSystem().displayMetrics.scaledDensity
}
val systemService = getSystemService(WindowManager::class.java)
val metrics = DisplayMetrics()
systemService.defaultDisplay.getMetrics(metrics)
mInitWidth = metrics.widthPixels
mInitHeight = metrics.heightPixels
}
Log.d("yaocai", "BaseApplication onConfigurationChanged")
}
}
override fun onCreate() {
super.onCreate()
initDisplayMetricsInfo()
registerActivityLifecycleCallbacks(activityLifecycleCallback)
registerComponentCallbacks(componentCallbacks)
}
private fun initDisplayMetricsInfo() {
val systemDisplayMetrics = Resources.getSystem().displayMetrics
val systemService = getSystemService(WindowManager::class.java)
val metrics = DisplayMetrics()
systemService.defaultDisplay.getMetrics(metrics)
mInitDensity = systemDisplayMetrics.density
mInitDensityDpi = systemDisplayMetrics.densityDpi
mInitScaledDensity = systemDisplayMetrics.scaledDensity
mInitWidth = metrics.widthPixels
mInitHeight = metrics.heightPixels
}
private fun adapter(activity: Activity) {
val targetDensity: Float
if (isBaseOnWidth) {
targetDensity = mInitWidth / 375f
} else {
targetDensity = mInitHeight / 667f
}
val targetDensityDpi = (targetDensity * 160).toInt()
val scaledDensity =
targetDensity * mInitScaledDensity / mInitDensity
// 修改application displayMetrics
val applicationDisplayMetrics = this.resources.displayMetrics
applicationDisplayMetrics.density = targetDensity
applicationDisplayMetrics.densityDpi = targetDensityDpi
applicationDisplayMetrics.scaledDensity = scaledDensity
// 修改activity displayMetrics
val activityDisplayMetrics = activity.resources.displayMetrics
activityDisplayMetrics.density = targetDensity
activityDisplayMetrics.densityDpi = targetDensityDpi
activityDisplayMetrics.scaledDensity = scaledDensity
}
}
同样是动态计算,该方案比AndroidAutoLayout
要简单很多,完全基于系统Api
。不过也一些问题:
- 只能以设计稿宽或者高为基准;
- 系统控件和第三库控件可能会变形, 因为他们的设计稿尺寸可能与我们的不同;
- 部分Rom可能修改了
Android
源码,对Resource
的操作略有不同;
当然在实际开发过程中还需要考虑一些边界问题,譬如横竖屏切换、跨进程适配、部分页面以宽为维度、部分页面以高为维度等等。
这里推荐大家使用基于今日头条方案的AndroidAutoSize库,解决了上述提到的各种问题。
到这里整个系列就结束,屏幕适配就是一个血泪史,好在随着技术的沉淀,涌现出各式各样的适配方案,感谢同行们的分享。
本文由 sadhu 创作,采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为:
2020/06/15 01:19