07.22 控件-资源适配与多主题
前两节(06 布局组件、07 控件)搭建界面时,资源(图片、颜色、尺寸、文字)都来自 res/ 目录。但真实的设备世界是碎片化的:屏幕从 4 寸到 13 寸、密度从 mdpi 到 xxxhdpi、语言从中文到上百种、系统有亮色和深色两种主题。Android 通过**资源限定符(Resource Qualifier)**机制解决这个适配问题:同一份资源,按设备状态自动选择最合适的版本。
# 一、资源限定符的匹配规则
先理解系统的选择机制——所有适配手段的基础:
三条核心结论:
- 目录名 = 资源类型 + 限定符,如
drawable-xxhdpi、values-zh-rCN、layout-land,多个限定符用-连接且必须按优先级顺序排列 - 默认目录(无限定符)是兜底,必须有一套完整资源;限定符目录只放"与默认不同"的部分
- 系统在所有候选中从高到低逐级比较限定符优先级(mcc/mnc > 语言 > 深色模式 > 屏幕方向 > 密度 > …),选出一个最匹配的
完整限定符优先级表见官方文档:提供资源 (opens new window)。
# 二、屏幕密度适配
# 2.1 密度单位:dp 与 sp
Android 用密度无关单位屏蔽物理像素差异:
| 单位 | 含义 | 使用场景 |
|---|---|---|
dp(dip) | 密度无关像素,1dp 在 mdpi(160dpi)设备上等于 1px | 布局尺寸(宽高、间距) |
sp | 可缩放像素,随用户字体大小设置缩放 | 文字大小(尊重系统字体设置) |
px | 物理像素 | 基本不用(除分隔线等 1px 场景) |
各密度桶的换算关系(以 1dp 为基准):
| 密度桶 | 密度 | 1dp = |
|---|---|---|
| mdpi | 160dpi(基准) | 1px |
| hdpi | 240dpi | 1.5px |
| xhdpi | 320dpi | 2px |
| xxhdpi | 480dpi | 3px |
| xxxhdpi | 640dpi | 4px |
实践规则:布局尺寸一律用 dp、文字一律用 sp;图片按密度桶各放一套(或用一张高分辨率图 + SVG/Vector 矢量图)。
# 2.2 图片的多密度适配
res/
├── drawable/ # 默认兜底(vector或xml)
├── drawable-mdpi/ # 可省略(基准由默认承担)
├── drawable-xhdpi/
├── drawable-xxhdpi/ # 主力密度(多数设备)
└── drawable-xxxhdpi/
两条策略:
- 位图按桶放:主力密度(xxhdpi)放高清图,其他桶按比例缩放提供,避免低密度设备放大高图导致模糊、内存浪费
- 矢量图优先:图标类用 VectorDrawable(
drawable/一份全密度通用),体积小、不失真;复杂照片类才用位图
# 2.3 代码中获取像素值
布局 xml 用 dp/sp 即可;代码中 API 需要 px 时必须转换:
// dp → px(View 的API都是px单位,如setTranslationX、layout参数)
int dp2px(float dp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
dp, getResources().getDisplayMetrics());
}
// sp → px(文字类API)
int sp2px(float sp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
sp, getResources().getDisplayMetrics());
}
// 获取屏幕信息
DisplayMetrics dm = getResources().getDisplayMetrics();
Log.i(TAG, "宽=" + dm.widthPixels + " 高=" + dm.heightPixels
+ " 密度=" + dm.density + "(1dp=" + dm.density + "px)");
# 三、多语言国际化(i18n)
# 3.1 字符串资源外置
国际化的前提是所有文案进 strings.xml,布局和代码中不写死中文:
<!-- res/values/strings.xml(默认,建议默认放英文) -->
<resources>
<string name="app_name">MyApp</string>
<string name="btn_login">Login</string>
<string name="welcome_msg">Welcome, %1$s! You have %2$d new messages.</string>
</resources>
<!-- res/values-zh-rCN/strings.xml(简体中文) -->
<resources>
<string name="app_name">我的应用</string>
<string name="btn_login">登录</string>
<string name="welcome_msg">欢迎你,%1$s!你有 %2$d 条新消息。</string>
</resources>
# 3.2 使用与格式化
<!-- 布局中引用 -->
<TextView android:text="@string/btn_login" ... />
// 代码中引用与带参数格式化
String appName = getString(R.string.app_name);
// %1$s:第一个字符串参数;%2$d:第二个整数参数
String msg = String.format(getString(R.string.welcome_msg), "JiangMing", 3);
目录命名:values-zh-rCN(简中)、values-zh(中文通用)、values-en、values-zh-rTW(繁中);语言限定符比密度优先级高,语言命中即覆盖默认文案(图片仍按密度规则选)。
# 四、深色模式(Dark Theme)
Android 10(API 29)起系统级支持深色主题。应用适配的核心是 DayNight 主题 + night 资源目录:
# 4.1 使用 DayNight 主题
<!-- 主题继承DayNight,系统亮色模式下用亮色资源、深色模式下用暗色资源 -->
<style name="Theme.MyApp" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<item name="colorPrimary">@color/primary</item>
<item name="android:windowBackground">@color/background</item>
</style>
# 4.2 提供夜色资源目录
颜色等资源通过 -night 限定符提供暗色版本:
<!-- res/values/colors.xml(亮色) -->
<resources>
<color name="background">#FFFFFF</color>
<color name="text_primary">#333333</color>
</resources>
<!-- res/values-night/colors.xml(暗色) -->
<resources>
<color name="background">#121212</color>
<color name="text_primary">#EEEEEE</color>
</resources>
布局、drawable 同样支持 -night 目录(如深色模式用不同 logo)。写法上无需任何 if/else——引用 @color/background 时系统自动按模式选择。
# 4.3 主题模式控制
// AppCompatDelegate控制主题模式(通常在Application/设置页调用)
// MODE_NIGHT_FOLLOW_SYSTEM:跟随系统(默认推荐)
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
// MODE_NIGHT_NO:强制亮色 / MODE_NIGHT_YES:强制深色
适配检查清单:写死颜色的地方(代码 Color.parseColor("#333")、shape xml)要换成 @color 引用;图片类注意深色背景下的观感(提供 -night 版本或使用透明底图标)。
# 五、屏幕方向与多窗口适配
# 5.1 横竖屏资源
layout-land 提供横屏专属布局:
res/
├── layout/ # 竖屏(默认)
└── layout-land/ # 横屏
旋转屏幕 = 配置变更,默认会销毁重建 Activity(生命周期详见 13.01 Activity 章节)。两种应对策略:
<!-- 策略一:接受重建,用onSaveInstanceState保存状态(推荐,配合ViewModel) -->
<activity android:name=".DemoActivity" />
<!-- 策略二:声明不重建(仅特定场景,如游戏横屏锁定) -->
<activity android:name=".GameActivity"
android:screenOrientation="landscape"
android:configChanges="orientation|screenSize" />
// 保存与恢复状态
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString("draft", mEtDraft.getText().toString());
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
// 重建后恢复
String draft = savedInstanceState.getString("draft");
}
}
# 5.2 多窗口模式
Android 7.0+ 支持分屏,Activity 可能"可见但不在前台":
- 分屏下生命周期与全屏不同(仍需了解:失去焦点但不走 onStop,具体行为随版本变化)
- 布局要考虑高度可能只有一半,避免写死与屏幕等高的尺寸
- 清单中可声明
android:resizeableActivity="true"(targetSdk 24+ 默认支持)
# 六、常见问题与陷阱
- dp ≠ 绝对一致:dp 只保证"物理尺寸接近",不同宽高比设备上布局仍可能被拉伸/裁剪,关键布局用 ConstraintLayout 百分比/约束自适应(见 06.07)。
- sp 缩放的边界:老人机字体超大时 sp 文字可能撑爆布局,
autolineSize(TextView 自动缩放)或布局留白可缓解。 - strings.xml 里的占位符:
%1$s/%2$d带位置编号(多语言语序不同),不要用不带编号的%s。 - 默认目录必须兜底:只建了
values-zh没有默认values,英文系统上会崩(资源缺失)。 - 深色模式出现写死颜色残留:全局搜索
Color.parse和 shape 里的solid写死色值,逐一替换为颜色资源。 - configChanges 的代价:声明后系统不再帮你重建,资源(如 layout-land)仍会切换,但状态保存全靠自己,慎用。
# 结束
本节介绍了 Android 资源适配体系:资源限定符的匹配规则(兜底默认目录 + 最匹配选择)、屏幕密度适配(dp/sp 单位体系、密度桶、px 转换)、多语言国际化(strings.xml 外置与格式化占位符)、深色模式(DayNight 主题 + night 资源目录 + setDefaultNightMode)、屏幕方向与多窗口(配置变更与状态保存)。适配的本质是"默认目录兜底 + 限定符特例",掌握这个心智模型后,任何限定符(-night、-land、-zh-rCN、-xxhdpi)都是同一个套路的重复应用。更多内容请参考官方文档:支持不同的像素密度 (opens new window) 与 深色主题 (opens new window)。