07.22 控件-资源适配与多主题

2025/1/20 Android 开发基础

前两节(06 布局组件、07 控件)搭建界面时,资源(图片、颜色、尺寸、文字)都来自 res/ 目录。但真实的设备世界是碎片化的:屏幕从 4 寸到 13 寸、密度从 mdpi 到 xxxhdpi、语言从中文到上百种、系统有亮色和深色两种主题。Android 通过**资源限定符(Resource Qualifier)**机制解决这个适配问题:同一份资源,按设备状态自动选择最合适的版本。

# 一、资源限定符的匹配规则

先理解系统的选择机制——所有适配手段的基础:

资源限定符匹配规则示意图

三条核心结论:

  1. 目录名 = 资源类型 + 限定符,如 drawable-xxhdpi、values-zh-rCN、layout-land,多个限定符用 - 连接且必须按优先级顺序排列
  2. 默认目录(无限定符)是兜底,必须有一套完整资源;限定符目录只放"与默认不同"的部分
  3. 系统在所有候选中从高到低逐级比较限定符优先级(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/

两条策略:

  1. 位图按桶放:主力密度(xxhdpi)放高清图,其他桶按比例缩放提供,避免低密度设备放大高图导致模糊、内存浪费
  2. 矢量图优先:图标类用 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+ 默认支持)

# 六、常见问题与陷阱

  1. dp ≠ 绝对一致:dp 只保证"物理尺寸接近",不同宽高比设备上布局仍可能被拉伸/裁剪,关键布局用 ConstraintLayout 百分比/约束自适应(见 06.07)。
  2. sp 缩放的边界:老人机字体超大时 sp 文字可能撑爆布局,autolineSize(TextView 自动缩放)或布局留白可缓解。
  3. strings.xml 里的占位符:%1$s/%2$d 带位置编号(多语言语序不同),不要用不带编号的 %s。
  4. 默认目录必须兜底:只建了 values-zh 没有默认 values,英文系统上会崩(资源缺失)。
  5. 深色模式出现写死颜色残留:全局搜索 Color.parse 和 shape 里的 solid 写死色值,逐一替换为颜色资源。
  6. configChanges 的代价:声明后系统不再帮你重建,资源(如 layout-land)仍会切换,但状态保存全靠自己,慎用。

# 结束

本节介绍了 Android 资源适配体系:资源限定符的匹配规则(兜底默认目录 + 最匹配选择)、屏幕密度适配(dp/sp 单位体系、密度桶、px 转换)、多语言国际化(strings.xml 外置与格式化占位符)、深色模式(DayNight 主题 + night 资源目录 + setDefaultNightMode)、屏幕方向与多窗口(配置变更与状态保存)。适配的本质是"默认目录兜底 + 限定符特例",掌握这个心智模型后,任何限定符(-night、-land、-zh-rCN、-xxhdpi)都是同一个套路的重复应用。更多内容请参考官方文档:支持不同的像素密度 (opens new window) 与 深色主题 (opens new window)。