【Android】屏幕适配一点都不全面的合集
本文参考:
一文读懂 Android 主流屏幕适配方案_字节 屏幕适配 远离-CSDN博客
Android 屏幕适配全攻略 - 简书
什么是dp?sp?px?为啥要文字要用sp单位?
由于Android设备的多样性------从低分辨率的老旧机型到4K屏幕的高端设备,从传统直板手机到可折叠的动态屏幕------开发者必须确保应用在不同尺寸、密度和形态的屏幕上都能呈现一致的视觉效果和交互体验。
dp?sp?px?
理解屏幕适配的基础,需从物理单位与Android的抽象单位体系入手。
物理像素(px) 是屏幕上最小的物理显示单元,每一个像素对应硬件上的一个发光点。
直接使用像素作为单位看似直观,却存在致命缺陷:假设一个按钮宽度设为100px,在低密度屏幕(如480x800像素的5英寸设备)上可能占据屏幕宽度的1/5,而在高密度屏幕(如1440x3120像素的6.5英寸设备)上可能仅占1/10,导致界面比例严重失调。
这种绝对单位的特性决定了它只适合处理与屏幕物理特性强相关的需求(例如相机预览画面布局),而在常规UI开发中应尽量避免使用。
为此,Android引入了密度无关像素(dp)和缩放独立像素(sp)。dp基于屏幕密度动态转换为实际像素,1dp在160dpi(基准密度)的设备上等于1px,而在更高密度的设备中会自动放大;sp则专用于文字,可跟随系统字体大小设置调整。
密度无关像素(dp) 是Android为适配不同密度屏幕设计的抽象单位。其核心逻辑是:1dp在160dpi(基准密度)的设备上等于1px,在其他密度设备上会根据比例自动缩放。例如,在320dpi(xhdpi)设备中,1dp=2px。这种机制使得使用dp定义尺寸的元素,能在不同设备上保持相近的物理尺寸(例如10dp的线条在多数手机上都接近1.5毫米宽)。
缩放独立像素(sp) 专为字体设计,继承dp的密度适配特性,同时叠加用户字体偏好设置的影响。当用户在系统设置中将字体大小调整为"大"或"超大"时,所有使用sp单位的文字会按比例缩放。这种设计平衡了可读性与用户自主权,但也带来潜在风险:若过度依赖sp,可能导致极端设置下文本溢出布局。因此,对于必须严格控制的文本(如导航栏标题),可采用dp单位或动态计算缩放比例,而对正文等需适配用户偏好的场景则坚持使用sp。
ppi?dpi?

像素密度(ppi与dpi) 是理解屏幕物理特性的关键指标。
**ppi(Pixels Per Inch)**是物理密度单位,通过屏幕分辨率和物理尺寸计算得出。例如,6英寸1080x1920像素的手机,其ppi为√(1080² + 1920²)/6 ≈ 440ppi,数值越高代表像素点越密集,显示越细腻。
**dpi(Dots Per Inch)**在Android中被重新定义为一种逻辑密度等级,用于将屏幕归类到预设的密度分组(如mdpi、xhdpi)。开发者通过DisplayMetrics.densityDpi
获取的dpi值可能与实际ppi不同------系统允许厂商在一定范围内自定义dpi值以优化显示效果。
代码示例:
kotlin
val displayMetrics = applicationContext.resources.displayMetrics
Log.e("TAG", "densityDpi: " + displayMetrics.densityDpi)
Log.e("TAG", "density: " + displayMetrics.density)
Log.e("TAG", "widthPixels: " + displayMetrics.widthPixels)
Log.e("TAG", "heightPixels: " + displayMetrics.heightPixels)
屏幕适配方案
本部分照搬一种极低成本的Android屏幕适配方式
该文档为2018年5月25日发布,时至今日可能已经不具备时效性,在此处作为屏幕适配方案的参考。
dp适配的缺陷
android中的dp在渲染前会将dp转为px,计算公式:
- px = density * dp;
- density = dpi / 160;
- px = dp * (dpi / 160);
dp到px的转换逻辑看似完美,理论上可以让10dp的按钮在不同设备上保持相近的物理尺寸。但现实远比公式复杂,这种理想化的适配模型在实际开发中遭遇多重挑战,其根源在于硬件参数的非标准化与厂商定制化带来的混乱。
以一台6英寸1080×1920像素的手机为例,按物理公式计算其dpi应为√(1080² + 1920²)/6 ≈ 440。
若严格遵循系统定义,此时1dp=440/160=2.75px,屏幕宽度dp值为1080/2.75≈392.7dp。然而,多数设备的
DisplayMetrics.densityDpi
值并不精确等于物理计算的ppi,而是被归入预设的密度等级(如xhdpi=320dpi)。更棘手的是,厂商可能故意修改dpi值:例如将实际440ppi的屏幕报告为480dpi(xxhdpi),导致1dp=3px,此时屏幕宽度dp值变为1080/3=360dp。这种人为干预虽能让屏幕宽度"恰好"匹配设计稿的360dp,却破坏了dp单位与物理尺寸的对应关系。
兼容适配(今日头条方案)
通过动态重置density值,强行统一所有设备的逻辑dp尺寸。
今日头条提出的方案:在应用启动时,基于设备的真实物理宽度(像素值)和设计基准宽度(如360dp)重新计算density值。
例如1080px宽的设备,若设计基准为360dp,则设置density=1080/360=3
,此后所有dp单位都基于此新基准转换。这种"视口缩放"模式类似Web开发中的viewport meta标签,虽能实现像素级精确适配,但可能引发系统控件(如Dialog)尺寸异常,需额外处理第三方库的兼容性。
通过修改系统DisplayMetrics
的密度参数,强制统一所有设备的逻辑像素基准。假设设计稿宽度为360dp,则动态计算:
kotlin
val targetDensity = screenWidthPx / 360f
此时任意设备的屏幕宽度在代码中都会被识别为360dp,实现设计图与代码的1:1映射。此方案本质是将物理像素宽度与设计稿dp宽度进行动态绑定,类似于Web开发中的视口(viewport)缩放机制。
代码示例:
kotlin
object DensityAdaptEngine {
// 设计基准尺寸(单位:dp)
private const val DESIGN_WIDTH_DP = 360f
private const val DESIGN_HEIGHT_DP = 640f
// 原始密度值缓存
private var originalDensity: Float = 0f
private var originalScaledDensity: Float = 0f
fun adapt(activity: Activity, isWidthBased: Boolean = true) {
val appMetrics = activity.application.resources.displayMetrics
val activityMetrics = activity.resources.displayMetrics
// 计算目标密度
val screenWidthPx = appMetrics.widthPixels
val screenHeightPx = appMetrics.heightPixels
val targetDensity = if (isWidthBased) {
screenWidthPx / DESIGN_WIDTH_DP
} else {
screenHeightPx / DESIGN_HEIGHT_DP
}
// 保留原始比例关系
val originalScaleRatio = appMetrics.scaledDensity / appMetrics.density
originalDensity = appMetrics.density
originalScaledDensity = appMetrics.scaledDensity
// 更新全局密度
appMetrics.density = targetDensity
appMetrics.densityDpi = (targetDensity * 160).toInt()
appMetrics.scaledDensity = targetDensity * originalScaleRatio
// 更新Activity级密度
activityMetrics.density = targetDensity
activityMetrics.densityDpi = (targetDensity * 160).toInt()
activityMetrics.scaledDensity = targetDensity * originalScaleRatio
// 注册字体变化监听
activity.application.registerComponentCallbacks(object : ComponentCallbacks {
override fun onConfigurationChanged(config: Configuration) {
if (config.fontScale != activity.resources.configuration.fontScale) {
appMetrics.scaledDensity = targetDensity * (config.fontScale * originalScaleRatio)
activityMetrics.scaledDensity = targetDensity * (config.fontScale * originalScaleRatio)
}
}
override fun onLowMemory() {}
})
}
fun reset(activity: Activity) {
val metrics = activity.resources.displayMetrics
metrics.density = originalDensity
metrics.densityDpi = (originalDensity * 160).toInt()
metrics.scaledDensity = originalScaledDensity
}
}
双维度适配支持
- 宽度基准模式 (
isWidthBased=true
):保证所有设备的宽度逻辑值等于设计稿宽度(360dp) - 高度基准模式 (
isWidthBased=false
):保证所有设备的高度逻辑值等于设计稿高度(640dp)
字体缩放补偿
- 通过
originalScaleRatio
保留原始scaledDensity/density
比例 - 动态监听系统字体缩放系数(
fontScale
),实时更新scaledDensity
作用域控制
- 同时修改Application级 和Activity级的DisplayMetrics
- 避免部分第三方库通过不同Context获取Metrics导致的适配失效
宽高限定符
宽高限定符的核心思路是为特定分辨率设备生成专属尺寸资源。
假设设计稿基于1920×1080像素,开发者需创建默认的values/dimens.xml
,其中包含从x1
到x1080
的宽度像素值和y1
到y1920
的高度像素值。例如,针对1440×720像素的设备,需生成values-1440x720/dimens.xml
,将宽度均分为1080份(每份720/1080=0.666px),高度均分为1920份(每份1440/1920=0.75px)。布
局中直接引用设计稿的像素值(如@dimen/x100
),系统根据设备分辨率自动匹配资源。
SmallestWidth
SmallestWidth(最小宽度)方案通过设备逻辑宽度(单位:dp)的渐进适配,显著提升了容错率。其核心在于以设计稿宽度为基准(假设为1080px),转换为逻辑宽度(如360dp),随后为不同逻辑宽度的设备生成阶梯化的尺寸资源。
实现关键步骤:
基准转换 :若设计稿为1080px宽,在xhdpi设备(320dpi)中,逻辑宽度为
1080px / (320dpi/160) = 540dp
。资源生成 :以10dp为步长创建资源文件(如
values-sw360dp
、values-sw540dp
),每个文件包含按比例缩放的dp值。例如,在values-sw540dp
中,x100
对应的值为(100 × 540dp) / 1080 = 50dp
。