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

前 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/ 的方式是:

perl 复制代码
layout 放布局
values 放字符串和颜色
drawable 放图片

这只是表层。

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

例如本周 Demo 中:

bash 复制代码
res/values/colors.xml
res/values-night/colors.xml

两边都可以定义:

ini 复制代码
<color name="week5_page_background">...</color>

布局只需要写:

ini 复制代码
android:background="@color/week5_page_background"

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

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

大型 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 有默认字符串:

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

也有英文字符串:

xml 复制代码
<!-- 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>

布局只写:

ini 复制代码
android:text="@string/week5_localized_message"

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

为什么默认资源不能少

假设只提供:

bash 复制代码
res/values-en/strings.xml

没有:

bash 复制代码
res/values/strings.xml

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

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

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

成熟团队实践映射

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

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

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


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

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

常见目录:

perl 复制代码
values-en/
values-zh-rCN/
values-night/
drawable-hdpi/
drawable-night-hdpi/
values-v21/

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

这是错的。

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

比如设备同时满足:

vbnet 复制代码
en
night
notouch
12key

你有两个目录:

vbnet 复制代码
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 中的标题样式:

xml 复制代码
<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>

布局里引用:

ini 复制代码
<TextView
    style="@style/Week5TitleText"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

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

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

也就是说:

xml 复制代码
<LinearLayout style="@style/SomeStyle">
    <TextView ... />
</LinearLayout>

TextView 不会自动继承 LinearLayoutstyle

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

样式继承:parent 和点号继承

本周 Demo 有:

xml 复制代码
<style name="Week5TitleText.Highlight">
    <item name="android:textColor">@color/week5_primary</item>
</style>

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

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

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

ini 复制代码
<style name="Week5TitleText" parent="TextAppearance.AppCompat.Title">

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

实践

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

rust 复制代码
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 写法很像,但作用范围不同。

当前项目主题是:

ini 复制代码
<style name="Theme.MyApplication" parent="Theme.MaterialComponents.DayNight.NoActionBar" />

Theme 可以应用到:

ini 复制代码
<application android:theme="@style/Theme.MyApplication" />

也可以应用到单个 Activity

ini 复制代码
<activity android:theme="@style/SomeActivityTheme" />

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

ini 复制代码
<LinearLayout android:theme="@style/SomeLocalTheme">

Theme 影响的不只是 View

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

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

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

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

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

框架属性通常带 android:

ini 复制代码
<item name="android:windowBackground">...</item>

AppCompat / Material 属性通常不带:

ini 复制代码
<item name="colorPrimary">...</item>

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

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

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

ini 复制代码
android:textColor="#111827"

更推荐的方向是语义化:

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

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

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

perl 复制代码
基础色阶 primitive token
  → 语义颜色 system token
  → 组件 token
  → 页面引用

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


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

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

ini 复制代码
<TextView
    android:id="@+id/tvPriorityNote"
    style="@style/Week5TitleText.Highlight"
    android:textColor="@color/week5_accent" />

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

最终生效的是直接属性。

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

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

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

成熟团队实践映射

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

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

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


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

本周 Demo 中:

xml 复制代码
<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 里。

成熟团队实践映射

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

css 复制代码
Title / Body / Caption / Label / Button

并明确:

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

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


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

本周卡片背景使用 shape

ini 复制代码
<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>

这个资源对应:

bash 复制代码
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

xml 复制代码
<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 中,按钮状态通常不是散落在业务代码里:

scss 复制代码
if (pressed) setBackgroundColor(...)

更常见的是:

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

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


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

Demo 的主题是:

diff 复制代码
Theme.MaterialComponents.DayNight.NoActionBar
+ values/colors.xml
+ values-night/colors.xml
+ AppCompatDelegate.setDefaultNightMode()

Activity 中恢复用户选择:

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

点击按钮切换:

kotlin 复制代码
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 里加:

ini 复制代码
android:configChanges="uiMode"

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

API 31 以后的选择

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

bash 复制代码
UiModeManager#setApplicationNightMode

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

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

Force Dark 的边界

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

但它不是正式长期方案。

原因是:

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

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

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

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

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

流程是:

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

attrs.xml

ini 复制代码
<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>

布局使用:

ini 复制代码
<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 中读取:

ini 复制代码
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,就必须写:

csharp 复制代码
try {
    // read attrs
} finally {
    typedArray.recycle()
}

invalidate()requestLayout() 的区别

Demo 中:

scss 复制代码
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 中有:

ini 复制代码
<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>

布局引用:

ini 复制代码
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. 构建期资源缩减

发布阶段才会考虑:

ini 复制代码
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 英文备用资源 英文文案 国际化 文本变长导致布局溢出
shape XML 几何 Drawable 卡片背景 减少图片资源 复杂图形强行 shape
selector 状态资源选择器 按钮按下/禁用/默认态 状态样式统一 默认 item 放太前面
style 单个 View 外观集合 Week5TitleText 控件样式复用 误以为会传给子 View
Theme App/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 进入发布阶段的资源治理。

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

一句话总结:

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

相关推荐
zh_xuan6 小时前
Android 获取系统内存页大小:sysconf(_SC_PAGESIZE) 与 JNI 实现
android·jni·ndk·内存页大小
fundroid8 小时前
Google I/O 2026 | Android 全面进化:从操作系统到“智能中枢”
android·jetpack compose·google i/o 2026
zh_xuan8 小时前
Android 复用 .so 库:通过 jniLibs 集成预编译二进制库(获取 Page Size )
android·jni·ndk·内存页大小
匆忙拥挤repeat9 小时前
Android Compose 约束布局
android
好安静9 小时前
Android ShellTransitions 机制完整分析(by DeepSeekV4Pro)
android
事后不诸葛10 小时前
安卓init.rc解析
android·framework
徒手猫10 小时前
myslq 中json 格式的数据如何获取某个属性
android·json
2401_8275602010 小时前
【电脑和手机系统】解锁bl后刷LineageOS与Magisk各模块的安装(七)
android·linux·智能手机
超人也会哭️呀10 小时前
ES 混合检索(文本+向量)中的条件处理陷阱——当权限过滤遇到关键词查询
android·大数据·elasticsearch