15、Recovery升级UI定制:自定义进度条、显示升级日志、中文与多语言支持
各位好,我是老张。今天我们来聊聊Recovery的UI定制。说实话,这部分内容在官方文档里写得不多,但实际项目中几乎每个定制ROM团队都会碰到。我记得第一次接手这个任务时,客户要求把那个默认的绿色进度条改成蓝色,还要在升级过程中实时显示日志——嗯,当时我翻了一整天源码才找到关键入口。
Recovery的UI,说白了就是一套基于minui的图形系统。它不像Android Framework那样有丰富的控件库,所有东西都得自己画。你想想看,在这样一个受限环境下,要实现自定义进度条、显示日志、支持多语言,其实挺考验基本功的。
15.1 Recovery UI架构概览
先看整体结构。Recovery的UI代码主要集中在bootable/recovery目录下,核心文件有这些:
| 文件/目录 | 作用 |
|---|---|
| screen_ui.cpp | 屏幕绘制核心,管理所有UI元素 |
| ui.cpp | UI抽象层,处理按键和绘制请求 |
| minui/ | 底层图形库,负责像素操作和字体渲染 |
| res-*dpi/ | 资源目录,存放图片和字体 |
我个人习惯把Recovery UI分成三层:底层是minui,负责画点画线;中间是ScreenRecoveryUI,管理布局和动画;上层是具体的UI实现,比如我们常改的进度条和日志区域。
核心思路:Recovery的UI是单线程的,所有绘制都在主循环里完成。这意味着你不能搞复杂的动画框架,得用状态机来控制UI更新。
15.2 自定义进度条
进度条是升级过程中用户最直观的反馈。默认的Recovery进度条是一个简单的矩形填充动画,说实话有点丑。我在项目中做过几次定制,这里分享一个比较通用的方案。
15.2.1 进度条绘制原理
进度条的核心在screen_ui.cpp的draw_progress()方法里。它其实就是在屏幕上画两个矩形:背景矩形和前景填充矩形。关键代码长这样:
// 默认进度条绘制(简化版)
void ScreenRecoveryUI::draw_progress() {
int width = screen_width - 2 * kProgressMargin;
int height = 30;
int x = kProgressMargin;
int y = screen_height - kProgressMargin - height;
// 画背景
draw_rectangle(x, y, width, height, kProgressBackgroundColor);
// 画填充
int fill_width = static_cast<int>(width * progress_fraction);
draw_rectangle(x, y, fill_width, height, kProgressFillColor);
}
要自定义样式,我建议重写draw_progress()方法。比如改成圆角进度条,或者带渐变效果的:
// 自定义圆角进度条
void MyRecoveryUI::draw_progress() {
int width = screen_width - 2 * kProgressMargin;
int height = 24;
int x = kProgressMargin;
int y = screen_height - kProgressMargin - height;
// 画圆角背景
draw_rounded_rect(x, y, width, height, 12, kProgressBackgroundColor);
// 画渐变填充
int fill_width = static_cast<int>(width * progress_fraction);
if (fill_width > 0) {
draw_gradient_rect(x, y, fill_width, height,
kStartColor, kEndColor, 12);
}
// 显示百分比文字
char pct[8];
snprintf(pct, sizeof(pct), "%d%%",
static_cast<int>(progress_fraction * 100));
draw_text_centered(pct, x + width/2, y + height/2);
}
我的经验:进度条不要做得太花哨。Recovery环境下的CPU资源有限,复杂的渐变或动画会导致界面卡顿。我曾经试过用粒子效果做进度条,结果升级过程明显变慢,最后老老实实改回了简单渐变。
15.2.2 进度条动画控制
进度条的更新是通过progress_fraction这个变量控制的。系统在升级过程中会调用SetProgress()来更新这个值。但有个坑——默认的进度更新是线性的,而实际升级各阶段耗时不同,导致进度条经常卡在某个位置不动。
我建议的做法是:
- 分段映射:把升级过程分成几个阶段(解包、刷写、验证),每个阶段分配不同的进度权重
- 平滑插值:在阶段内部用缓动函数让进度条平滑移动
- 保底机制:每个阶段至少显示5%的进度,避免用户以为死机了
// 分段进度控制示例
void MyRecoveryUI::SetProgress(float fraction) {
// 分段映射:解包占30%,刷写占50%,验证占20%
float mapped_fraction;
if (fraction < 0.3f) {
// 解包阶段:0~30%
mapped_fraction = fraction / 0.3f * 0.3f;
} else if (fraction < 0.8f) {
// 刷写阶段:30%~80%
mapped_fraction = 0.3f + (fraction - 0.3f) / 0.5f * 0.5f;
} else {
// 验证阶段:80%~100%
mapped_fraction = 0.8f + (fraction - 0.8f) / 0.2f * 0.2f;
}
// 平滑插值
progress_fraction = progress_fraction * 0.7f + mapped_fraction * 0.3f;
}
15.3 显示升级日志
升级日志是调试的利器。默认Recovery会把日志写到/cache/recovery/log,但用户看不到。我做过一个项目,客户要求升级过程中实时显示日志,方便售后排查问题。
15.3.1 日志缓冲区设计
要在屏幕上显示日志,首先得有个日志缓冲区。Recovery的日志是通过LOG()宏输出的,默认走的是stdout。我们需要把它重定向到自己的缓冲区:
// 日志缓冲区实现
class LogBuffer {
public:
static constexpr int kMaxLines = 100;
static constexpr int kLineLength = 256;
void Append(const char* msg) {
// 环形缓冲区
strncpy(lines_[write_pos_], msg, kLineLength - 1);
write_pos_ = (write_pos_ + 1) % kMaxLines;
if (count_ < kMaxLines) count_++;
}
const char* GetLine(int index) const {
int pos = (write_pos_ - count_ + index + kMaxLines) % kMaxLines;
return lines_[pos];
}
private:
char lines_[kMaxLines][kLineLength];
int write_pos_ = 0;
int count_ = 0;
};
注意:Recovery环境内存有限,日志缓冲区不要开太大。100行每行256字节,总共也就25KB,足够了。我曾经见过有人开了1000行,结果低内存设备直接OOM。
15.3.2 日志显示区域
在屏幕上划出一块区域专门显示日志。我一般放在进度条上方,占屏幕高度的三分之一左右。绘制时只显示最新的几行,并支持滚动:
// 日志区域绘制
void MyRecoveryUI::draw_log_area() {
int log_y = kLogAreaTop;
int line_height = 18;
int max_visible = kLogAreaHeight / line_height;
// 从缓冲区取最新的max_visible行
int start = max(0, log_buffer_.GetCount() - max_visible);
for (int i = 0; i < max_visible && start + i < log_buffer_.GetCount(); i++) {
const char* line = log_buffer_.GetLine(start + i);
draw_text(2, log_y + i * line_height, line, kLogTextColor);
}
}
15.4 中文与多语言支持
这部分是很多国内厂商的刚需。默认Recovery只支持英文,因为它的字体是硬编码的位图字体。要支持中文,得自己搞定中文字体渲染。
15.4.1 字体文件准备
Recovery的minui支持加载PNG格式的字体图片。我们需要把中文字体转换成位图字体。我常用的工具是mkfont,它可以把TTF字体转成minui能识别的格式:
# 生成中文字体位图
mkfont -s 24 -o res-xxhdpi/font_24.png /usr/share/fonts/NotoSansSC-Regular.ttf
# 生成字体映射文件
mkfont -s 24 -m res-xxhdpi/font_24.map /usr/share/fonts/NotoSansSC-Regular.ttf
生成的.map文件记录了每个字符在图片中的位置。minui加载时会根据字符编码去查找对应的位图区域。
关键点:中文字体文件通常很大,全量转换会导致资源包膨胀。我建议只转换常用汉字(约3500个),加上升级过程中会用到的英文和数字。这样字体文件能控制在2MB以内。
15.4.2 多语言切换机制
多语言支持的核心是字符串资源管理。我在项目中是这样做的:
- 定义字符串ID枚举,比如
kStrInstalling、kStrError等 - 为每种语言准备一个字符串表,放在
res-*/lang/目录下 - 启动时根据系统语言设置加载对应的字符串表
// 多语言字符串管理
class LanguageManager {
public:
bool LoadLanguage(const char* lang_code) {
char path[128];
snprintf(path, sizeof(path), "/res/lang/%s.txt", lang_code);
FILE* fp = fopen(path, "r");
if (!fp) return false;
// 解析键值对
char key[32], value[256];
while (fscanf(fp, "%s=%[^\n]", key, value) == 2) {
strings_[GetStringId(key)] = strdup(value);
}
fclose(fp);
return true;
}
const char* GetString(StringId id) {
auto it = strings_.find(id);
return (it != strings_.end()) ? it->second : "?";
}
};
语言文件示例(zh_CN.txt):
installing=正在安装...
error=错误
reboot=重启
wipe_data=清除数据
apply_update=应用更新
success=升级成功
15.4.3 语言检测与回退
系统语言可以通过读取/persist/sys/language或/data/property/persist.sys.language获取。如果找不到对应的语言文件,我建议回退到英文:
// 语言检测逻辑
const char* DetectLanguage() {
// 尝试从persist分区读取
char lang[16] = {0};
if (read_file("/persist/sys/language", lang, sizeof(lang)) > 0) {
if (lang_manager_.LoadLanguage(lang)) {
return lang;
}
}
// 回退到英文
lang_manager_.LoadLanguage("en");
return "en";
}
避坑指南:我曾经遇到过一个问题——某些设备在Recovery模式下无法访问/data分区,因为/data可能被加密了。所以语言配置最好放在/persist或/misc分区,这些分区在Recovery下是可读的。
15.5 实战:整合所有定制
最后,我把上面这些内容整合成一个完整的自定义UI类。这个类继承自ScreenRecoveryUI,重写了关键方法:
class MyCustomUI : public ScreenRecoveryUI {
public:
MyCustomUI() {
// 初始化语言
current_lang_ = DetectLanguage();
}
// 重写进度条绘制
void draw_progress() override {
// ... 自定义圆角渐变进度条
}
// 重写日志显示
void draw_text_layer() override {
draw_log_area();
// 调用父类绘制其他文字
ScreenRecoveryUI::draw_text_layer();
}
// 重写字符串获取
const char* GetLocalizedString(StringId id) override {
return lang_manager_.GetString(id);
}
private:
LogBuffer log_buffer_;
LanguageManager lang_manager_;
const char* current_lang_;
};
然后在recovery.cpp的入口处替换默认UI:
// 在main函数中
int main(int argc, char** argv) {
// 原来用的是ScreenRecoveryUI
// ScreenRecoveryUI ui;
// 换成我们的自定义UI
MyCustomUI ui;
// ... 其余逻辑不变
}
嗯,到这里Recovery UI定制的主要内容就讲完了。说实话,这部分工作虽然看起来简单,但实际调试起来挺费时间的——因为Recovery环境没有adb,看不到logcat,所有问题都得靠屏幕上的显示来排查。我建议你在开发时先在模拟器里验证,确认逻辑没问题后再烧录到真机上。
最后提醒一句:定制UI时一定要考虑不同屏幕尺寸的适配。我见过有人把进度条写死了像素位置,结果在平板设备上显示错位。用screen_width和screen_height做相对布局,才是稳妥的做法。
公众号:蓝海资料掘金营,微信deep3321