屏幕适配
Android 屏幕适配:dp 与 sp 原理、smallestWidth 限定符、布局别名、小屏 / 大屏 / 折叠屏适配、WindowMetrics API、ConstraintLayout 响应式布局与 Jetpack WindowManager。
学习目标
- 理解 dp / sp 的换算原理
- 掌握 smallestWidth 限定符与布局别名
- 了解小屏 / 大屏 / 折叠屏适配策略
- 掌握 WindowMetrics API 与正确获取屏幕尺寸
- 能用 ConstraintLayout 实现响应式布局
学习目标
- 理解 dp / sp 的换算原理;
- 掌握 smallestWidth 限定符与布局别名;
- 了解小屏 / 大屏 / 折叠屏适配策略;
- 掌握 WindowMetrics API 与正确获取屏幕尺寸;
- 能用 ConstraintLayout 实现响应式布局。
屏幕适配
dp 与 sp 原理
Android 物理屏幕千差万别:手机 5–7 英寸、平板 8–13 英寸、折叠屏 6–8 英寸展开后更大。直接写 px 会在不同密度上看起来比例失衡。
关键概念
- px(pixel):物理像素;
- dpi(dots per inch):每英寸像素数;
- density:
density = dpi / 160,作为 dp 到 px 的换算系数; - dp / dip(density-independent pixel):在 160dpi 屏幕(即 density=1)下 1dp = 1px。换算:
px = dp * density; - sp(scale-independent pixel):类似 dp,但额外受 用户字体缩放 影响,用于文字大小。
密度桶
Android 把屏幕密度划为若干桶:
| 类别 | dpi | density | 示例 |
|---|---|---|---|
| ldpi | 120 | 0.75 | 几乎绝迹 |
| mdpi | 160 | 1.0 | 老 320x480 |
| hdpi | 240 | 1.5 | 老 720p |
| xhdpi | 320 | 2.0 | 1080p |
| xxhdpi | 480 | 3.0 | 1440p |
| xxxhdpi | 640 | 4.0 | 4K |
布局文件用 dp / sp,系统按当前 density 自动换算,保证不同密度看起来“长度一致”。
字号 sp 的特殊性
sp 同时受 fontScale(用户系统字体大小)影响:
Configuration config = getResources().getConfiguration();
float fontScale = config.fontScale; // 1.0 是默认;2.0 是超大字号
// sp -> px: px = sp * density * fontScale
所以正文文字一定要用 sp;图标尺寸、固定 UI 用 dp,避免跟随用户字号变化。
资源限定符
屏幕尺寸(旧版)
| 限定符 | 含义 |
|---|---|
small |
~320x422 dp |
normal |
~320x470 dp |
large |
~480x640 dp |
xlarge |
~720x960 dp |
这是早期 API 的方案,已不推荐,因为粗略且无法表达具体宽度。
屏幕方向
-port(竖屏) / -land(横屏):
res/layout/main.xml (默认)
res/layout-land/main.xml (横屏替换)
smallestWidth 限定符(API 13+)
更精确的尺寸适配方式。sw<N>dp 表示 可用最小宽度(设备旋转后保持的最小边):
| 限定符 | 典型设备 |
|---|---|
sw320dp |
普通手机 |
sw600dp |
7 寸平板 |
sw720dp |
10 寸平板 |
sw840dp |
12 寸+ 平板 / 折叠屏展开 |
例:
res/layout/main.xml (默认 sw<600)
res/layout-sw600dp/main.xml (平板以上)
res/layout-sw840dp/main.xml (超大屏 / 折叠展开)
注意:smallestWidth 一旦匹配,会忽略其他较宽的目录,不会重复叠加。例如 800dp 宽的平板只会匹配 sw600dp。
布局别名
适配多尺寸时通常维护 基础布局 + 别名 模式,避免重复:
res/layout/main_twopane.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<FrameLayout
android:id="@+id/list_container"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent" />
<FrameLayout
android:id="@+id/detail_container"
android:layout_width="0dp"
android:layout_weight="2"
android:layout_height="match_parent" />
</LinearLayout>
res/values-large.xml(旧式别名):
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="main_layout" type="layout">@layout/main_twopane</item>
</resources>
res/values-sw600dp/layouts.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="main_layout" type="layout">@layout/main_twopane</item>
</resources>
res/values/layouts.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="main_layout" type="layout">@layout/main_singlepane</item>
</resources>
代码里统一引用别名:
setContentView(R.layout.main_layout); // 自动按 sw600 选择不同布局
小屏 / 大屏 / 折叠屏适配
小屏(手机)
- 单栏布局;
- 用
ScrollView兜底溢出内容; - 避免过密的列表项。
大屏(平板)
- 双栏(列表 + 详情);
- 充分利用横向空间;
- 注意
match_parent在横屏下不会自动给两侧加 padding,需用Guideline或maxWidth。
折叠屏
折叠屏的关键问题是 折叠状态变化。展开 / 折叠时 Configuration 改变,需要响应:
AndroidManifest.xml:
<activity
android:name=".MainActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout" />
让 Activity 自己处理旋转和折叠,避免重建:
public class MainActivity extends AppCompatActivity {
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
updateLayoutForFold();
}
private void updateLayoutForFold() {
WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
if (wm == null) return;
WindowMetrics metrics = wm.getCurrentWindowMetrics();
Rect bounds = metrics.getBounds();
boolean isFoldedExpanded = bounds.width() >= 600 * density();
if (isFoldedExpanded) {
showTwoPane();
} else {
showSinglePane();
}
}
private float density() {
return getResources().getDisplayMetrics().density;
}
private void showTwoPane() { /* 切换到双栏 */ }
private void showSinglePane() { /* 切换到单栏 */ }
}
Jetpack WindowManager
Google 提供的 androidx.window 库抽象了折叠屏特性:
dependencies {
implementation "androidx.window:window:1.3.0"
implementation "androidx.window:window-java:1.3.0"
}
监听折叠状态:
import androidx.window.layout.WindowInfoTracker;
import androidx.window.layout.FoldingFeature;
import androidx.lifecycle.lifecycleScope;
public class MainActivity extends AppCompatActivity {
private WindowInfoTracker tracker;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
tracker = WindowInfoTracker.getOrCreate(this);
tracker.windowLayoutInfo(this)
.addOnLayoutInfoChangedListener(info -> {
for (DisplayFeature feature : info.getDisplayFeatures()) {
if (feature instanceof FoldingFeature) {
FoldingFeature fold = (FoldingFeature) feature;
boolean isHalfOpen = fold.getState()
== FoldingFeature.State.HALF_OPENED;
boolean isVertical = fold.getOrientation()
== FoldingFeature.Orientation.VERTICAL;
if (isHalfOpen && isVertical) {
showBookModeLayout(fold.getBounds());
}
}
}
}, ContextCompat.getMainExecutor(this));
}
private void showBookModeLayout(Rect foldBounds) {
// 在 fold 两侧分别布局
}
}
WindowMetrics API
Android 11(API 30)引入 WindowMetrics 替代已弃用的 Display.getMetrics:
@RequiresApi(Build.VERSION_CODES.R)
public class ScreenSizeHelper {
public static int widthDp(Activity activity) {
WindowManager wm = activity.getWindowManager();
WindowMetrics metrics = wm.getCurrentWindowMetrics();
float density = activity.getResources().getDisplayMetrics().density;
return (int) (metrics.getBounds().width() / density);
}
public static int heightDp(Activity activity) {
WindowManager wm = activity.getWindowManager();
WindowMetrics metrics = wm.getCurrentWindowMetrics();
float density = activity.getResources().getDisplayMetrics().density;
return (int) (metrics.getBounds().height() / density);
}
}
低版本兼容:
public class CompatScreen {
public static int widthDp(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
if (wm != null) {
WindowMetrics metrics = wm.getCurrentWindowMetrics();
float density = context.getResources().getDisplayMetrics().density;
return (int) (metrics.getBounds().width() / density);
}
}
DisplayMetrics dm = context.getResources().getDisplayMetrics();
return (int) (dm.widthPixels / dm.density);
}
}
getMaximumWindowMetrics 用于多窗口场景下获取最大可用尺寸:
WindowMetrics max = wm.getMaximumWindowMetrics();
// 多窗口拖拽后返回真实可见宽度
注意:不要再用 Display.getHeight / getMetrics,它们在多窗口模式下返回的是错的。
ConstraintLayout 响应式
ConstraintLayout 通过 Guideline、Barrier、DimensionRatio、ConstraintSet 让响应式布局更高效。
Guideline 分屏
<androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guide_half"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:orientation="vertical"
app:layout_constraintGuide_percent="0.5" />
<FrameLayout
android:id="@+id/left_pane"
android:layout_width="0dp"
android:layout_height="match_parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/guide_half" />
<FrameLayout
android:id="@+id/right_pane"
android:layout_width="0dp"
android:layout_height="match_parent"
app:layout_constraintStart_toEndOf="@id/guide_half"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Barrier 跟随最远元素
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="end"
app:constraint_referenced_ids="tv_name,tv_desc" />
Barrier 会跟随 tv_name 和 tv_desc 中最宽的那个,让右侧按钮始终在两者右侧。
DimensionRatio 保持比例
<ImageView
android:id="@+id/iv_avatar"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@id/guide_half"
app:layout_constraintBottom_toTopOf="@id/tv_name" />
Flow(Flow 实现响应式标签流)
Flow 是 ConstraintLayout 2.0 提供的虚拟布局,能像 Flexbox 一样自动换行:
<androidx.constraintlayout.helper.widget.Flow
android:layout_width="0dp"
android:layout_height="wrap_content"
app:flow_wrapMode="aligned"
app:flow_horizontalGap="8dp"
app:flow_verticalGap="8dp"
app:constraint_referenced_ids="tag1,tag2,tag3,tag4,tag5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
ConstraintSet 运行时切换
ConstraintLayout cl = findViewById(R.id.root);
ConstraintSet set = new ConstraintSet();
set.clone(this, R.layout.activity_main_expanded); // 不同布局文件
set.applyTo(cl);
适合折叠屏展开时切换布局形态。
字体缩放与无障碍
正文文字使用 sp 才能跟随用户字号。固定大小用 dp,可缩放用 sp。如果想给某些大字标题“留出字号变化空间但限制最大”,可用:
<style name="TextAppearance.Title" parent="TextAppearance.Material3.HeadlineSmall">
<item name="android:textSize">20sp</item>
</style>
代码内可以监听 fontScale:
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (newConfig.fontScale > 1.3f) {
// 用户开了超大字号,可能挤掉 UI 元素,考虑调整布局
adjustLayoutForLargeFont();
}
}
常见坑与最佳实践
px写死:写死的px在不同密度设备上看起来大小不一。一律用dp/sp,自动换算。- 使用
Display.getHeight():API 13 弃用,多窗口模式下返回错值。用WindowMetrics。 getResources().getDisplayMetrics()在多窗口下失真:同上,应使用WindowMetrics。- smallestWidth 写成
sw600dp写成sw_600dp:下划线错误,正确是sw600dp,无下划线。 - 布局别名忘了
type="layout":<item name="main_layout">@layout/main_twopane</item>不会自动当作 layout,需要type="layout"。 match_parent在 ConstraintLayout 不生效:ConstraintLayout 子 View 必须设约束,宽度match_parent应改用0dp+constraintStart/End,否则约束不会算进去。- 横屏用同一布局导致拥挤:横屏应单独写
layout-land或用 ConstraintSet 动态切换。 - 字号不随用户缩放:正文写
dp而非sp,会让视障用户开大字号无效果,违反无障碍要求。 - 折叠屏未声明 configChanges:每次折叠都会重建 Activity,体验差。声明
configChanges="screenSize|smallestScreenSize|screenLayout"自行处理。 WindowMetrics.getBounds与旧式DisplayMetrics混用:两者在多窗口下行为不一致,应统一用WindowMetrics。
章节小结
本章覆盖了 Android 屏幕适配:dp 与 sp 的换算原理(px = dp * density)、密度桶与 smallestWidth 限定符、布局别名(<item type="layout">)、小屏 / 大屏 / 折叠屏适配策略、Android 11+ WindowMetrics API、ConstraintLayout 响应式(Guideline、Barrier、DimensionRatio、Flow、ConstraintSet)、Jetpack WindowManager 监听折叠状态、字体缩放与无障碍。核心要点:布局一律用 dp/sp;横屏与超大屏写专门资源或用 ConstraintSet 切换;折叠屏配合 androidx.window;多窗口场景用 WindowMetrics 不要用旧 DisplayMetrics。
下一章预告
下一章将进入性能优化领域:内存优化(LeakCanary、Bitmap 优化、弱引用)、启动优化(冷 / 温 / 热启动、延迟初始化)、渲染优化(过度绘制、RecyclerView 缓存)、ANR 分析与 StrictMode。