前 4 周我们一直在写具体控件:TextView、EditText、按钮、ImageView。第 5 周看起来突然变成了 Shape、Selector、Style、Theme、attrs.xml、values-night、多语言和资源优化。
真实项目里,资源体系解决的是这些问题:
- 页面多了以后,颜色、圆角、间距还能不能统一改;
- 深色模式、品牌换肤、多语言上线时,会不会到处改布局;
- 按钮、卡片、标签、输入框这些 UI 资产能不能沉淀成规范;
- 包体积变大以后,资源有没有可治理的入口;
- 自定义 View 能不能像系统控件一样通过 XML 配置。
大型 App 的 UI 资源管理,本质上不是"写得好看",而是:让设计、开发、测试、性能和发布链路都有一个稳定的资源协议。
相关资料
-
Android Developers:
Styles and themesStyle、Theme、TextAppearance、widget style、属性优先级、android:前缀边界。
-
Android Developers:
Dark themeDayNight、AppCompatDelegate、UiModeManager#setApplicationNightMode、Force Dark、uiMode配置变化。
-
Android Developers:
Providing resources- 默认资源、备用资源、限定符顺序、资源匹配规则、多语言和密度资源选择。
-
Android Developers:
Reduce app size- App Bundle、删除未使用资源、
resourceConfigurations、XML Drawable、WebP、VectorDrawable、图片资源复用。
- App Bundle、删除未使用资源、
-
Android Developers:
R8 / shrink codeminifyEnabled、shrinkResources、R8、keep 规则、mapping、资源缩减和资源压缩的边界。
-
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_background、week5_card_background、week5_text_primary 这些资源名模拟了轻量级语义 token。
相关技术清单
- 资源目录:
res/values、res/values-night、res/drawable、res/mipmap、res/layout - 资源访问:
R.color.xxx、@color/xxx、@dimen/xxx、@string/xxx - 资源限定符:
night、en、hdpi、v21等 - 常见风险:没有默认资源、限定符顺序错误、硬编码色值、资源重复、动态资源引用导致 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-en、values-zh-rCN - 深色:
values-night - 密度:
drawable-xhdpi、drawable-xxhdpi - 版本:
values-v21、drawable-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" />
它解决的是:多个标题不用重复写 textSize、textStyle、textColor。
但有一个容易误解的点:style 不像 CSS,不会自动传递给子 View。
也就是说:
xml
<LinearLayout style="@style/SomeStyle">
<TextView ... />
</LinearLayout>
TextView 不会自动继承 LinearLayout 的 style。
如果想影响某个 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",而是应用级语义和窗口能力
Theme 和 Style 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_primary、week5_text_primary、week5_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_width、padding、maxLines、某些段落行为,不应该都塞到 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
而不是每个页面随手写 13dp、17dp。
这样设计、研发、测试才能对齐:页面间距错了,不是看个人审美,而是看是否符合 spacing token。
十三、资源优化:要区分"组织优化"和"发布优化"
资源优化至少分三层:
源码组织优化
构建期资源缩减
资源内容压缩 / 分发优化
1. 源码组织优化
本周 Demo 已落地的部分:
- 颜色放
colors.xml和values-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解决文本体系;Shape和Selector解决简单图形和状态背景;values-night和DayNight解决深色模式;attrs.xml让自定义 View 具备 XML 配置能力;- 默认资源和限定符规则保证应用在不同设备配置下稳定运行;
- R8、
shrinkResources、AAB、WebP、VectorDrawable 进入发布阶段的资源治理。
大型项目里,资源不是"最后整理一下",而是一开始就应该设计好的协作协议。
一句话总结:
布局负责结构,资源负责差异,样式负责复用,主题负责语义,构建负责裁剪,规范负责长期不失控。