第5周:XML 资源、样式和主题,真正解决的是“页面以后还改不改得动”

71 阅读17分钟

前 4 周我们一直在写具体控件:TextViewEditText、按钮、ImageView。第 5 周看起来突然变成了 ShapeSelectorStyleThemeattrs.xmlvalues-night、多语言和资源优化。

真实项目里,资源体系解决的是这些问题:

  • 页面多了以后,颜色、圆角、间距还能不能统一改;
  • 深色模式、品牌换肤、多语言上线时,会不会到处改布局;
  • 按钮、卡片、标签、输入框这些 UI 资产能不能沉淀成规范;
  • 包体积变大以后,资源有没有可治理的入口;
  • 自定义 View 能不能像系统控件一样通过 XML 配置。

大型 App 的 UI 资源管理,本质上不是“写得好看”,而是:让设计、开发、测试、性能和发布链路都有一个稳定的资源协议。

相关资料

  1. Android Developers:Styles and themes

    • StyleThemeTextAppearance、widget style、属性优先级、android: 前缀边界。
  2. Android Developers:Dark theme

    • DayNightAppCompatDelegateUiModeManager#setApplicationNightModeForce DarkuiMode 配置变化。
  3. Android Developers:Providing resources

    • 默认资源、备用资源、限定符顺序、资源匹配规则、多语言和密度资源选择。
  4. Android Developers:Reduce app size

    • App Bundle、删除未使用资源、resourceConfigurations、XML Drawable、WebP、VectorDrawable、图片资源复用。
  5. Android Developers:R8 / shrink code

    • minifyEnabledshrinkResources、R8、keep 规则、mapping、资源缩减和资源压缩的边界。
  6. Material Design 3 色彩系统页面


一、资源系统:不要把资源当成“文件夹分类”

很多初学者理解 res/ 的方式是:

layout 放布局
values 放字符串和颜色
drawable 放图片

这只是表层。

Android 资源系统真正重要的是:同一个资源名,可以在不同设备配置下解析成不同资源。

例如本周 Demo 中:

res/values/colors.xml
res/values-night/colors.xml

两边都可以定义:

<color name="week5_page_background">...</color>

布局只需要写:

android:background="@color/week5_page_background"

浅色模式下,系统选 values/colors.xml;深色模式下,系统选 values-night/colors.xml

也就是说,布局不关心“现在是浅色还是深色”,它只关心“我要页面背景色”。这就是资源系统的价值。

image-20260524111715812

大型 App 场景映射

大型 App 往往会遇到:

  • 首页、详情页、搜索页、支付页都要适配深色模式;
  • 不同业务线有不同品牌色;
  • 海外版本要支持多语言;
  • 运营活动页要换肤;
  • 平板、折叠屏、车机等设备形态需要不同资源。

如果每个页面都硬编码颜色和尺寸,改一次主题就是全局搜索替换,风险很高。

成熟团队更常见的做法是:

页面 XML 只引用语义资源
  → 资源文件负责浅色/深色/语言/密度差异
  → 主题负责全局设计语义
  → 构建和发布阶段再做资源裁剪与压缩

本周 Demo 用 week5_page_backgroundweek5_card_backgroundweek5_text_primary 这些资源名模拟了轻量级语义 token。

相关技术清单

  • 资源目录:res/valuesres/values-nightres/drawableres/mipmapres/layout
  • 资源访问:R.color.xxx@color/xxx@dimen/xxx@string/xxx
  • 资源限定符:nightenhdpiv21
  • 常见风险:没有默认资源、限定符顺序错误、硬编码色值、资源重复、动态资源引用导致 shrink 误删

二、默认资源:大型项目里最容易被忽视的兜底

Android 官方明确要求:如果提供备用资源,也要提供默认资源。

例如本周 Demo 有默认字符串:

<!-- res/values/strings.xml -->
<string name="week5_localized_message">默认资源:如果设备没有匹配到指定语言,会回退到 values/strings.xml。</string>

也有英文字符串:

<!-- res/values-en/strings.xml -->
<string name="week5_localized_message">English resource: when the device locale matches English, Android selects values-en/strings.xml automatically.</string>

布局只写:

android:text="@string/week5_localized_message"

如果设备语言是英文,系统优先选 values-en。如果不是英文,系统回退到默认 values

为什么默认资源不能少

假设只提供:

res/values-en/strings.xml

没有:

res/values/strings.xml

当设备语言不是英文时,系统可能找不到资源,出现运行时异常。

这类问题在大型 App 中并不少见,尤其发生在:

  • 海外版本快速加语言包;
  • 某个业务模块只补了局部语言;
  • 动态配置切换语言;
  • 插件化 / 多模块项目资源合并;
  • 低版本设备不认识某些新限定符。

成熟团队实践映射

成熟团队通常会把“默认资源完整性”当成资源治理的底线:

  • 每个关键 string 必须有默认 values/strings.xml
  • 每个关键 drawable 必须有默认 drawable/ 版本;
  • 夜间资源、语言资源、密度资源都是补充,不是唯一来源;
  • CI 或 lint 阶段检查缺失资源;
  • 多语言上线前做伪本地化和截断检查。

本周 Demo 只做了最小多语言展示,但规则要记住:备用资源是增强,默认资源是保命。


三、资源限定符:不是“匹配越多越优先”

资源限定符是必须讲清楚的点。

常见目录:

values-en/
values-zh-rCN/
values-night/
drawable-hdpi/
drawable-night-hdpi/
values-v21/

很多人会误以为:目录匹配的条件越多,就越优先。

这是错的。

Android 官方规则是:限定符优先级比匹配数量更重要。

比如设备同时满足:

en
night
notouch
12key

你有两个目录:

drawable-en/
drawable-night-notouch-12key/

直觉上第二个匹配 3 个条件,好像更应该选它。但官方规则里,语言限定符优先级高于夜间模式、触摸屏和输入法,所以系统可能会优先选择:

drawable-en/

多限定符顺序也不能乱

如果一个目录有多个限定符,顺序必须符合官方表格。

正确示例:

drawable-en-night-hdpi/

错误示例:

drawable-hdpi-night-en/

顺序错了,资源可能被忽略。

大型 App 场景映射

大型 App 资源经常同时涉及:

  • 多语言:values-envalues-zh-rCN
  • 深色:values-night
  • 密度:drawable-xhdpidrawable-xxhdpi
  • 版本:values-v21drawable-v24
  • 平板:layout-sw600dp

如果没有资源目录规范,很容易出现“某个地区 + 某个夜间模式 + 某个低版本设备”才触发的问题。

成熟团队通常会维护资源目录规范:

默认资源必须存在
限定符顺序必须正确
同一资源名跨目录语义一致
业务模块不能随意创建奇怪限定符目录
上线前覆盖语言 / 深色 / 密度 / 版本组合测试

这就是资源系统的工程性,而不是只会创建一个 values-night 文件夹。


四、Style:复用单个 View 的外观,但不会自动传给子 View

Style 是一组 View 属性集合。

本周 Demo 中的标题样式:

<style name="Week5TitleText" parent="TextAppearance.AppCompat.Title">
    <item name="android:textSize">24sp</item>
    <item name="android:textStyle">bold</item>
    <item name="android:textColor">@color/week5_text_primary</item>
</style>

布局里引用:

<TextView
    style="@style/Week5TitleText"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

它解决的是:多个标题不用重复写 textSizetextStyletextColor

但有一个容易误解的点:style 不像 CSS,不会自动传递给子 View。

也就是说:

<LinearLayout style="@style/SomeStyle">
    <TextView ... />
</LinearLayout>

TextView 不会自动继承 LinearLayoutstyle

如果想影响某个 View 层级及其子 View,要考虑 android:theme 或主题属性,而不是误用 style

样式继承:parent 和点号继承

本周 Demo 有:

<style name="Week5TitleText.Highlight">
    <item name="android:textColor">@color/week5_primary</item>
</style>

Week5TitleText.Highlight 会继承 Week5TitleText,再覆盖颜色。

这种点号继承适合项目内部样式。

如果继承系统、AppCompat、Material 样式,更推荐显式写 parent

<style name="Week5TitleText" parent="TextAppearance.AppCompat.Title">

这样阅读代码的人知道它从哪里来。

实践

大型团队一般不会允许页面随意写一堆孤立样式。常见做法是:

TextAppearance.Title.Large
TextAppearance.Title.Medium
TextAppearance.Body.Normal
Widget.App.Button.Primary
Widget.App.Button.Secondary
Widget.App.Card.Default

这样做的目的不是为了“命名好看”,而是为了让设计系统、组件库和业务页面之间有共同语言。

一旦设计改了标题字号,只改 TextAppearance;按钮圆角变了,只改 Widget.App.Button.Primary;页面不用逐个改。

相关技术

  • XML:<style>parent、点号继承
  • View 引用:style="@style/..."
  • 文本:TextAppearance
  • Widget:Widget.MaterialComponents.Button
  • 常见坑:以为 style 会继承给子 View;把所有属性都写进一个巨大 style;直接在布局覆盖 style 导致样式失效

五、Theme:不是“全局 style”,而是应用级语义和窗口能力

ThemeStyle XML 写法很像,但作用范围不同。

当前项目主题是:

<style name="Theme.MyApplication" parent="Theme.MaterialComponents.DayNight.NoActionBar" />

Theme 可以应用到:

<application android:theme="@style/Theme.MyApplication" />

也可以应用到单个 Activity

<activity android:theme="@style/SomeActivityTheme" />

还可以在某个 View 层级局部应用:

<LinearLayout android:theme="@style/SomeLocalTheme">

Theme 影响的不只是 View

官方文档里提到,Theme 还可以影响:

  • 窗口背景:android:windowBackground
  • 状态栏 / 导航栏颜色
  • Activity 转场
  • 默认 widget style
  • 控件可读取的主题属性

所以不能把 Theme 简化成“全局 style”。更准确的说法是:

Style 管单个 View 外观
Theme 管 App / Activity / View 层级的语义属性和窗口属性
Widget style 管某类控件默认外观
TextAppearance 管文本外观

平台属性和库属性前缀不能乱用

框架属性通常带 android:

<item name="android:windowBackground">...</item>

AppCompat / Material 属性通常不带:

<item name="colorPrimary">...</item>

不要机械地给所有属性都加 android:

成熟团队实践映射:设计 token 和语义角色

成熟团队做主题,通常不会让组件直接引用具体色值:

android:textColor="#111827"

更推荐的方向是语义化:

primary:主品牌高强调色
on-primary:放在 primary 上的文字或图标
surface:界面承载面
on-surface:放在 surface 上的主要内容
error:错误/危险状态
outline:边框/分割线

本周 Demo 用 week5_primaryweek5_text_primaryweek5_card_background 模拟了轻量级 token。它还不是完整设计系统,但已经比到处写 #FFFFFF 更接近工程实践。

大型 App 的设计系统常见分层是:

基础色阶 primitive token
  → 语义颜色 system token
  → 组件 token
  → 页面引用

这样品牌换肤、深色模式、活动皮肤和组件升级才不会互相污染。


六、属性优先级:样式不生效,先查谁覆盖了它

本周 Demo 故意放了一个覆盖例子:

<TextView
    android:id="@+id/tvPriorityNote"
    style="@style/Week5TitleText.Highlight"
    android:textColor="@color/week5_accent" />

Week5TitleText.Highlight 里已经设置了文字颜色,但 XML 上又直接写了 android:textColor

最终生效的是直接属性。

官方样式优先级可以简化理解为:

TextView span / 代码动态设置
  > View XML 直接属性
  > style
  > 默认 widget style
  > theme
  > TextAppearance 中较低优先级属性

这就是为什么很多人说“改了 theme 没生效”。不一定是 theme 错了,可能是更高优先级覆盖了它。

成熟团队实践映射

大型项目排查 UI 问题时,通常不会只看一个文件,而要按优先级查:

代码有没有动态 setTextColor / setBackground
XML 上有没有直接写属性
style 是否被覆盖
theme 是否正确应用到 Activity
values-night 是否有同名资源
多模块是否有资源名冲突

这类排查链路应该写进团队 UI 规范,否则主题迁移和深色模式适配会非常痛苦。


七、TextAppearance:它只管文本外观,不等于完整 TextView 样式

本周 Demo 中:

<style name="Week5BodyText" parent="TextAppearance.AppCompat.Body1">
    <item name="android:textSize">@dimen/week5_body_text_size</item>
    <item name="android:textColor">@color/week5_text_secondary</item>
    <item name="android:lineSpacingExtra">4dp</item>
</style>

这里使用 TextAppearance.AppCompat.Body1 作为父样式。

但要注意:TextAppearance 主要用于文本外观,比如:

  • 字号
  • 字体
  • 字重
  • 文字颜色
  • 字符级样式

它不是完整的 TextView 布局样式。像 layout_widthpaddingmaxLines、某些段落行为,不应该都塞到 TextAppearance 里。

成熟团队实践映射

成熟团队会把文本体系单独沉淀:

Title / Body / Caption / Label / Button

并明确:

  • 标题多大;
  • 正文多大;
  • 辅助说明用什么颜色;
  • 深色模式下文字对比度怎么保证;
  • 不同语言文本变长后怎么处理。

这不是 UI 洁癖,而是为了避免每个业务线自己定义一套字号,最后页面像拼起来的。


八、Shape:用 XML 描述简单图形,减少图片资源

本周卡片背景使用 shape

<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="@color/week5_card_background" />
    <corners android:radius="@dimen/week5_card_radius" />
    <stroke
        android:width="1dp"
        android:color="@color/week5_border" />
</shape>

这个资源对应:

res/drawable/bg_week5_card.xml

它描述了:

  • 背景色;
  • 圆角;
  • 边框宽度;
  • 边框颜色。

对于简单卡片、按钮背景、分割线、标签背景,shape 通常比 PNG 更适合。

为什么它适合大型项目

如果每个按钮状态都切一张图:

button_normal.png
button_pressed.png
button_disabled.png
button_dark_normal.png
button_dark_pressed.png

资源数量会快速膨胀。

shape 后,只需要维护颜色和尺寸资源。深色模式时同名颜色在 values-night 中替换,背景 XML 不用复制。

边界

shape 适合简单几何图形,不适合复杂插画、大面积纹理、照片类资源。复杂图形应该考虑 VectorDrawable、WebP、远程图片或设计资源压缩策略。


九、Selector:状态资源不是只能靠代码 if/else

本周按钮背景使用 selector

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true">
        <shape>
            <solid android:color="@color/week5_primary_dark" />
            <corners android:radius="12dp" />
        </shape>
    </item>
    <item android:state_enabled="false">
        <shape>
            <solid android:color="#94A3B8" />
            <corners android:radius="12dp" />
        </shape>
    </item>
    <item>
        <shape>
            <solid android:color="@color/week5_primary" />
            <corners android:radius="12dp" />
        </shape>
    </item>
</selector>

selector 根据 View 状态选择不同资源:

  • state_pressed:按下态;
  • state_enabled=false:禁用态;
  • 默认 item:普通态。

这里有个细节:顺序很重要。

selector 会从上往下匹配,先匹配到的 item 生效。所以默认项一般放最后。

实践

大型 App 中,按钮状态通常不是散落在业务代码里:

if (pressed) setBackgroundColor(...)

更常见的是:

状态颜色 / 背景 / 字体颜色 → selector 或 ColorStateList
交互状态 → 控件自身状态驱动
业务代码 → 只负责 enabled / selected / checked

这样测试和设计验收也更清楚:按钮禁用态不对,查资源;业务能不能点,查逻辑。


十、DayNight:深色模式不是把白色改黑色

Demo 的主题是:

Theme.MaterialComponents.DayNight.NoActionBar
+ values/colors.xml
+ values-night/colors.xml
+ AppCompatDelegate.setDefaultNightMode()

Activity 中恢复用户选择:

private fun restoreNightMode() {
    val mode = getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
        .getInt(KEY_NIGHT_MODE, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
    AppCompatDelegate.setDefaultNightMode(mode)
}

点击按钮切换:

private fun setNightMode(mode: Int) {
    getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
        .edit()
        .putInt(KEY_NIGHT_MODE, mode)
        .apply()
    AppCompatDelegate.setDefaultNightMode(mode)
}

三个模式:

AppCompatDelegate.MODE_NIGHT_NO
AppCompatDelegate.MODE_NIGHT_YES
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM

官方建议默认提供“跟随系统”。这对大型 App 很重要,因为用户通常希望全局系统设置能统一生效,而不是每个 App 都单独打架。

夜间模式

深色模式

浅色模式

AppCompatDelegate 会带来 Activity 重建

从 AppCompat 1.1.0 开始,setDefaultNightMode() 会自动重新创建已启动的 Activity。

这不是 bug。默认让 Activity 重建,系统可以重新加载正确的资源、主题和颜色。

不要为了“切换不闪一下”就随便在 Manifest 里加:

android:configChanges="uiMode"

只有确实需要自己处理,比如视频播放页、复杂编辑页、长任务页面,才考虑接管 uiMode,并在 onConfigurationChanged() 中手动刷新 UI。

API 31 以后的选择

官方提到,API 31 及以上可以使用:

UiModeManager#setApplicationNightMode

好处是系统能在启动画面阶段更好地匹配应用主题。

但本周 Demo 面向基础学习,使用 AppCompatDelegate 更直观,也兼容更低版本。

Force Dark 的边界

Force Dark 是 Android 10 的快速适配能力,可以把浅色界面自动转换成深色。

但它不是正式长期方案。

原因是:

  • 自动转换不一定符合设计;
  • 品牌色、图片、图标可能表现异常;
  • 已经使用 DayNight 的应用不会再应用 Force Dark;
  • 复杂页面仍需要人工测试和夜间资源。

成熟团队正式适配深色模式,优先路线应该是:

DayNight 主题
+ 语义颜色 token
+ values-night / drawable-night
+ 设计验收
+ 测试覆盖核心页面

十一、attrs.xml:让自定义 View 像系统控件一样可配置

本周新增了 Week5BadgeView,它不是为了做一个复杂控件,而是为了演示自定义属性闭环。

流程是:

attrs.xml 声明属性
  → XML 中 app:xxx 使用
  → 自定义 View obtainStyledAttributes 读取
  → TypedArray recycle
  → 属性变化后 requestLayout / invalidate

attrs.xml

<declare-styleable name="Week5BadgeView">
    <attr name="badgeText" format="string" />
    <attr name="badgeFillColor" format="color" />
    <attr name="badgeStrokeColor" format="color" />
    <attr name="badgeCornerRadius" format="dimension" />
    <attr name="badgeMode" format="enum">
        <enum name="normal" value="0" />
        <enum name="highlight" value="1" />
    </attr>
</declare-styleable>

布局使用:

<com.study.all.Week5BadgeView
    android:id="@+id/badgeCustom"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:badgeCornerRadius="18dp"
    app:badgeFillColor="@color/week5_badge_normal"
    app:badgeMode="normal"
    app:badgeStrokeColor="@color/week5_badge_stroke"
    app:badgeText="attrs.xml badge" />

自定义 View 中读取:

context.obtainStyledAttributes(
    attrs,
    R.styleable.Week5BadgeView,
    defStyleAttr,
    0
).use { typedArray ->
    badgeText = typedArray.getString(R.styleable.Week5BadgeView_badgeText) ?: badgeText
    badgeMode = typedArray.getInt(R.styleable.Week5BadgeView_badgeMode, badgeMode)
    badgeFillColor = typedArray.getColor(R.styleable.Week5BadgeView_badgeFillColor, badgeFillColor)
    badgeStrokeColor = typedArray.getColor(R.styleable.Week5BadgeView_badgeStrokeColor, badgeStrokeColor)
    badgeCornerRadius = typedArray.getDimension(
        R.styleable.Week5BadgeView_badgeCornerRadius,
        badgeCornerRadius
    )
}

这里代码用了 AndroidX 的 TypedArray.use {},它会在结束时调用 recycle()。如果不用 use,就必须写:

try {
    // read attrs
} finally {
    typedArray.recycle()
}

invalidate()requestLayout() 的区别

Demo 中:

fun updateBadge(text: String, highlight: Boolean) {
    badgeText = text
    badgeMode = if (highlight) 1 else 0
    applyModeIfNeeded()
    requestLayout()
    invalidate()
}

区别是:

  • invalidate():只重新绘制,适合颜色、文字内容、透明度这类绘制变化;
  • requestLayout():重新测量和布局,适合尺寸、文字长度、padding 这类可能影响大小的位置变化。

本 Demo 更新文字后调用两个方法,是为了演示完整流程。真实项目里要按变化类型选择,避免不必要的布局开销。

实践

大型团队做自定义组件时,通常不会让业务只靠 Kotlin 代码配置。更常见的是:

组件样式能力 → attrs.xml
组件默认外观 → defStyleAttr / theme
业务页面配置 → XML app:xxx
运行态变化 → 公开方法或状态驱动

这样组件才能被设计系统、业务页面和测试工具稳定使用。


十二、dimens:尺寸资源不是为了少写几个 dp

本周 Demo 中有:

<dimen name="week5_page_padding">20dp</dimen>
<dimen name="week5_card_radius">18dp</dimen>
<dimen name="week5_card_padding">16dp</dimen>
<dimen name="week5_body_text_size">14sp</dimen>

布局引用:

android:padding="@dimen/week5_card_padding"

dimens 的目的不是少写几个 dp,而是:

  • 统一间距;
  • 统一圆角;
  • 方便不同屏幕尺寸覆盖;
  • 方便设计系统变更;
  • 减少布局里的魔法数字。

大型 App 场景映射

成熟团队往往会把间距体系设计成固定刻度:

space_4
space_8
space_12
space_16
space_24
space_32

而不是每个页面随手写 13dp17dp

这样设计、研发、测试才能对齐:页面间距错了,不是看个人审美,而是看是否符合 spacing token。


十三、资源优化:要区分“组织优化”和“发布优化”

资源优化至少分三层:

源码组织优化
构建期资源缩减
资源内容压缩 / 分发优化

1. 源码组织优化

本周 Demo 已落地的部分:

  • 颜色放 colors.xmlvalues-night/colors.xml
  • 尺寸放 dimens.xml
  • 卡片背景放 shape
  • 按钮状态放 selector
  • 文本样式放 themes.xml
  • 自定义属性放 attrs.xml
  • 多语言放 values / values-en

这解决的是可维护性。

2. 构建期资源缩减

发布阶段才会考虑:

android {
    buildTypes {
        release {
            isMinifyEnabled = true
            isShrinkResources = true
        }
    }
}

这里要讲清楚:

  • minifyEnabled:启用 R8,主要处理代码缩减、优化和混淆;
  • shrinkResources:移除未使用资源;
  • shrinkResources 通常需要和 minifyEnabled 配合;
  • 它不是图片压缩,也不是 WebP 转换。

R8 也不是“只混淆”。它还会做:

  • 不可达代码删除;
  • 方法内联;
  • 类合并;
  • 优化 DEX;
  • 重命名类、方法、字段。

所以开启 R8 后要保存 mapping 文件,线上崩溃才能还原。

3. 资源内容压缩和分发优化

官方减少包体积建议还包括:

  • 使用 App Bundle,让应用商店按设备分发语言、密度、ABI 等资源;
  • resourceConfigurations 限制打包语言和密度;
  • 用 WebP 替换部分 PNG/JPEG;
  • 小图标优先考虑 VectorDrawable
  • 简单背景用 XML Drawable;
  • 避免逐帧动画,优先考虑 AnimatedVectorDrawableCompat
  • 用 Lint 和 Analyze APK 找未使用资源和体积大户;
  • 谨慎引入带大量资源的第三方库。

成熟团队实践映射

大型 App 的资源治理一般不会只靠某个开发手动整理。常见做法是:

开发阶段:资源命名规范 + 默认资源兜底 + 语义 token
Review 阶段:禁止硬编码颜色 / 尺寸,检查 selector / style 复用
CI 阶段:Lint、未使用资源检查、包体积阈值
发布阶段:R8、shrinkResources、AAB、mapping 保存
运营阶段:监控包体积、下载转化、安装成功率、低端机性能

这才是“资源优化”的完整链路。


十四、drawable、mipmap、VectorDrawable、WebP

drawable

普通图片、XML Drawable、shape、selector、vector 通常放这里。

mipmap

启动器图标推荐放 mipmap,因为系统启动器可能在不同密度下使用不同图标资源。

不要把所有普通图片都放进 mipmap

VectorDrawable

适合小图标:

  • 分辨率无关;
  • 一份资源适配多个密度;
  • 包体积通常比多套 PNG 小。

边界:复杂大图、复杂路径、频繁动画可能带来渲染成本。

WebP

适合替代部分 PNG/JPEG:

  • 压缩率通常更好;
  • 支持透明;
  • Android Studio 支持转换。

边界:需要关注最低系统版本、图片质量、设计验收和构建链路。

实践

大型团队通常会明确图片资源策略:

小图标:VectorDrawable
简单背景:shape / selector
运营插图:WebP / 远程资源
启动器图标:mipmap
照片内容:远程图片 + 图片加载框架
逐帧动画:尽量避免,改用矢量动画或 Lottie 等方案

如果没有这种策略,资源目录很快会变成“图片垃圾场”。


十五、把所有技术点清零:第5周技术总表

技术它是什么本周 Demo 落点真实项目价值常见坑
colors.xml颜色资源集合week5_* 颜色统一品牌色、深色模式布局硬编码 #FFFFFF
values-night夜间模式资源目录values-night/colors.xml深色模式自动替换只改背景不改文字对比度
dimens.xml尺寸资源集合padding、radius、text size间距和圆角统一随手写魔法数字
strings.xml字符串资源默认中文文案多语言兜底只提供限定语言无默认资源
values-en英文备用资源英文文案国际化文本变长导致布局溢出
shapeXML 几何 Drawable卡片背景减少图片资源复杂图形强行 shape
selector状态资源选择器按钮按下/禁用/默认态状态样式统一默认 item 放太前面
style单个 View 外观集合Week5TitleText控件样式复用误以为会传给子 View
ThemeApp/Activity/View 层级主题Theme.MyApplication全局设计语义、窗口属性当成“全局 style”
TextAppearance文本外观样式Week5BodyText字号字重体系当成完整 TextView style
Widget Style某类控件默认样式Week5ModeButton统一按钮/输入框外观每个按钮单独写样式
attrs.xml自定义属性声明Week5BadgeView组件可配置属性定义和读取不一致
TypedArray读取 XML 属性obtainStyledAttributes自定义 View 初始化忘记 recycle()
invalidate()重新绘制badge 更新绘制变化刷新尺寸变化只 invalidate
requestLayout()重新测量布局badge 文字变化尺寸变化刷新只改颜色也 requestLayout
AppCompatDelegate应用内夜间模式切换三个模式按钮主题偏好管理忽略 Activity 重建
Force Dark系统自动变暗能力文章边界说明旧项目过渡当正式适配方案
R8代码缩减/优化/混淆发布阶段说明减包、性能、混淆不保存 mapping
shrinkResources移除未用资源发布阶段说明减少包体积误以为是图片压缩
App Bundle按设备分发资源发布阶段说明下载包更小不理解和 APK 区别

十六、最终结论

第 5 周不是“学几个 XML 标签”。

它真正讲的是 Android UI 工程化的底层能力:

  • Style 解决单个 View 外观复用;
  • Theme 解决应用级语义和窗口能力;
  • TextAppearance 解决文本体系;
  • ShapeSelector 解决简单图形和状态背景;
  • values-nightDayNight 解决深色模式;
  • attrs.xml 让自定义 View 具备 XML 配置能力;
  • 默认资源和限定符规则保证应用在不同设备配置下稳定运行;
  • R8、shrinkResources、AAB、WebP、VectorDrawable 进入发布阶段的资源治理。

大型项目里,资源不是“最后整理一下”,而是一开始就应该设计好的协作协议。

一句话总结:

布局负责结构,资源负责差异,样式负责复用,主题负责语义,构建负责裁剪,规范负责长期不失控。