屏幕适配方案
2026/9/14大约 3 分钟
屏幕适配方案
Android 设备屏幕尺寸和密度各异,屏幕适配是开发的基础功课。本章介绍 dp / sp 等单位换算、头条适配方案以及刘海屏适配。
一、常用单位
适配前先厘清四个基本单位——它们的换算关系如下:
| 单位 | 全称 | 说明 |
|---|---|---|
| dpi | dot per inch | 每英寸像素数 |
| dp | 密度无关像素 | 基于屏幕物理密度的抽象单元,相对于 160 dpi 屏幕:1 dp = 160 dpi 屏幕上的 1 px |
| sp | 与比例无关的像素 | 与 dp 类似,但会随用户字体大小偏好缩放,建议用于字体大小 |
换算公式:
dpi = px / inch
density = dpi / 160
dp = px / density简单说:dpi 是物理值,dp/sp 是逻辑值。同一 dp 在不同密度的屏幕上对应不同 px 数,这正是系统帮我们做的适配工作。
二、头条适配方案
核心思路:修改系统的 Density 值,让设计稿宽度(如 360dp)在所有设备上保持一致,从而所有 dp 单位自动适配。
实现上要同时改 Application 和 Activity 的 DisplayMetrics,并监听字体切换保证 sp 同步缩放:
Java
private static float sNoncompatDensity;
private static float sNoncompatScaledDensity;
private static void setCustomDensity(Activity activity, Application application) {
final DisplayMetrics appDisplayMetrics =
application.getResources().getDisplayMetrics();
if (sNoncompatDensity == 0) {
sNoncompatDensity = appDisplayMetrics.density;
sNoncompatScaledDensity = appDisplayMetrics.scaledDensity;
// 监听字体切换
application.registerComponentCallbacks(new ComponentCallbacks() {
@Override
public void onConfigurationChanged(Configuration newConfig) {
if (newConfig != null && newConfig.fontScale > 0) {
sNoncompatScaledDensity =
application.getResources().getDisplayMetrics().scaledDensity;
}
}
@Override
public void onLowMemory() {
}
});
}
// 适配后的 dpi 统一为 360
final float targetDensity = appDisplayMetrics.widthPixels / 360f;
final float targetScaledDensity =
targetDensity * (sNoncompatScaledDensity / sNoncompatDensity);
final int targetDensityDpi = (int) (160 * targetDensity);
appDisplayMetrics.density = targetDensity;
appDisplayMetrics.scaledDensity = targetScaledDensity;
appDisplayMetrics.densityDpi = targetDensityDpi;
final DisplayMetrics activityDisplayMetrics =
activity.getResources().getDisplayMetrics();
activityDisplayMetrics.density = targetDensity;
activityDisplayMetrics.scaledDensity = targetScaledDensity;
activityDisplayMetrics.densityDpi = targetDensityDpi;
}Kotlin
private var sNoncompatDensity = 0f
private var sNoncompatScaledDensity = 0f
private fun setCustomDensity(activity: Activity, application: Application) {
val appDisplayMetrics = application.resources.displayMetrics
if (sNoncompatDensity == 0f) {
sNoncompatDensity = appDisplayMetrics.density
sNoncompatScaledDensity = appDisplayMetrics.scaledDensity
// 监听字体切换
application.registerComponentCallbacks(object : ComponentCallbacks {
override fun onConfigurationChanged(newConfig: Configuration) {
if (newConfig != null && newConfig.fontScale > 0) {
sNoncompatScaledDensity =
application.resources.displayMetrics.scaledDensity
}
}
override fun onLowMemory() {
}
})
}
// 适配后的 dpi 统一为 360
val targetDensity = appDisplayMetrics.widthPixels / 360f
val targetScaledDensity =
targetDensity * (sNoncompatScaledDensity / sNoncompatDensity)
val targetDensityDpi = (160 * targetDensity).toInt()
appDisplayMetrics.density = targetDensity
appDisplayMetrics.scaledDensity = targetScaledDensity
appDisplayMetrics.densityDpi = targetDensityDpi
val activityDisplayMetrics = activity.resources.displayMetrics
activityDisplayMetrics.density = targetDensity
activityDisplayMetrics.scaledDensity = targetScaledDensity
activityDisplayMetrics.densityDpi = targetDensityDpi
}优缺点: 侵入性小(几行代码全局生效),所有 dp 单位自动按宽度缩放;但设计稿宽高比与设备不一致时可能出现拉伸问题。
三、刘海屏适配
Android P(9.0)及以上
通过 DisplayCutout 类确定非功能区域(凹口)的位置和形状,使用 getDisplayCutout() 获取。先看它能提供哪些信息:
| DisplayCutout 方法 | 说明 |
|---|---|
getBoundingRects() | 返回 Rects 列表,每个都是显示屏上非功能区域的边界矩形 |
getSafeInsetLeft() | 安全区域距屏幕左边的距离(px) |
getSafeInsetRight() | 安全区域距屏幕右边的距离(px) |
getSafeInsetTop() | 安全区域距屏幕顶部的距离(px) |
getSafeInsetBottom() | 安全区域距屏幕底部的距离(px) |
WindowManager.LayoutParams 新增属性 layoutInDisplayCutoutMode,三种模式决定窗口能否延伸到凹口区域:
| 模式 | 说明 |
|---|---|
LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT | 只有当 DisplayCutout 完全包含在系统栏中时才允许窗口延伸到凹口区域,否则不与凹口区域重叠 |
LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER | 窗口决不允许与凹口区域重叠 |
LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES | 窗口始终允许延伸到屏幕短边上的凹口区域 |
Android P 之前
不同厂商的刘海屏适配方案不尽相同(华为、小米、OPPO、vivo 各有 API),需分别查阅各家开发者文档适配。