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更新。

Recovery UI 三层架构 上层:UI实现层 进度条、日志、菜单、语言切换 screen_ui.cpp / recovery_ui.cpp 中间层:UI管理引擎 布局管理、动画状态机、事件分发 ScreenRecoveryUI / RecoveryUI 底层:minui图形库 像素操作、字体渲染、图片解码 graphics.cpp / fonts.cpp

15.2 自定义进度条

进度条是升级过程中用户最直观的反馈。默认的Recovery进度条是一个简单的矩形填充动画,说实话有点丑。我在项目中做过几次定制,这里分享一个比较通用的方案。

15.2.1 进度条绘制原理

进度条的核心在screen_ui.cppdraw_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()来更新这个值。但有个坑——默认的进度更新是线性的,而实际升级各阶段耗时不同,导致进度条经常卡在某个位置不动。

我建议的做法是:

  1. 分段映射:把升级过程分成几个阶段(解包、刷写、验证),每个阶段分配不同的进度权重
  2. 平滑插值:在阶段内部用缓动函数让进度条平滑移动
  3. 保底机制:每个阶段至少显示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 多语言切换机制

多语言支持的核心是字符串资源管理。我在项目中是这样做的:

  1. 定义字符串ID枚举,比如kStrInstallingkStrError
  2. 为每种语言准备一个字符串表,放在res-*/lang/目录下
  3. 启动时根据系统语言设置加载对应的字符串表
// 多语言字符串管理
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_widthscreen_height做相对布局,才是稳妥的做法。


公众号:蓝海资料掘金营,微信deep3321