跳至内容
返回

安卓系统的开机动画是如何绘制时间

发布于:  at  08:12 上午

前言

上一篇我研究了 BootAnimation.cpp 里默认 Android logo 的 shine 动画. 那段逻辑的核心是两张纹理: 一张很长的高光图, 一张带 alpha 的 mask.

继续往下看源码时, 会发现开机动画还可以在画面上叠加时间. 它并不是调用系统字体渲染, 也不是把时间预先做成图片帧, 而是使用一张 clock_font.png 字体图集, 从里面裁剪出每个字符, 然后逐字绘制到屏幕上.

这篇文章依旧只关注一个问题: BootAnimation 是如何把当前时间绘制到开机动画上的.

资源获取

时间绘制用到的资源是 clock_font.png, 我把它和上一篇的资源放在同一个目录:

public/images/archives/shine-logo/clock_font.png

这张图片尺寸是 640 x 576. 表面上看它只是黑底白字, 但源码会把它当成一个固定规格的字符表:

字体初始化

BootAnimation.cpp 中字体初始化入口是 initFont:

status_t BootAnimation::initFont(Font* font, const char* fallback) {
    ...
    if (status == NO_ERROR) {
        font->char_width = font->texture.w / FONT_NUM_COLS;
        font->char_height = font->texture.h / FONT_NUM_ROWS / 2;
    }
    return status;
}

这里最关键的是 char_height 为什么要再除以 2. 源码注释里写得很直接:

// There are bold and regular rows

也就是说, 字体图集不是简单的 16 x 6, 而是每个字符所在的格子高度里同时塞了 regular 和 bold 两个版本.

时间格式

真正绘制时间的函数是 drawClock:

void BootAnimation::drawClock(const Font& font, const int xPos, const int yPos,
                              const Display& display) {
    static constexpr char TIME_FORMAT_12[] = "%l:%M";
    static constexpr char TIME_FORMAT_24[] = "%H:%M";
    static constexpr int TIME_LENGTH = 6;

    time_t rawtime;
    time(&rawtime);
    struct tm* timeInfo = localtime(&rawtime);

    char timeBuff[TIME_LENGTH];
    const char* timeFormat = mTimeFormat12Hour ? TIME_FORMAT_12 : TIME_FORMAT_24;
    size_t length = strftime(timeBuff, TIME_LENGTH, timeFormat, timeInfo);
    ...
    char* out = timeBuff[0] == ' ' ? &timeBuff[1] : &timeBuff[0];
    int x = xPos;
    int y = yPos;
    drawText(out, font, false, &x, &y, display);
}

这里有两个细节:

  1. 24 小时制使用 %H:%M, 会得到类似 08:30
  2. 12 小时制使用 %l:%M, 小于 10 点时前面会有空格, 所以后面通过 timeBuff[0] == ' ' 把这个空格跳过

最终时间字符串会交给 drawText.

逐字绘制

drawText 的思路非常直接:

const int len = strlen(str);
const int strWidth = font.char_width * len;

先根据字符数量算出整段文字宽度. 如果 xy 是居中标记, 就换算成屏幕坐标:

if (*x == TEXT_CENTER_VALUE) {
    *x = (display.width - strWidth) / 2;
}
if (*y == TEXT_CENTER_VALUE) {
    *y = (display.height - font.char_height) / 2;
}

如果坐标是负数, 则表示从屏幕右侧或底部反向定位:

if (*x < 0) {
    *x = display.width + *x - strWidth;
}
if (*y < 0) {
    *y = display.height + *y - font.char_height;
}

接下来就是每个字符的图集裁剪:

const int charPos = (c - FONT_BEGIN_CHAR);
const int row = charPos / FONT_NUM_COLS;
const int col = charPos % FONT_NUM_COLS;

float v0 = (row + (bold ? 0.5f : 0.0f)) / FONT_NUM_ROWS;
float u0 = ((float)col) / FONT_NUM_COLS;
float v1 = v0 + 1.0f / FONT_NUM_ROWS / 2;
float u1 = u0 + 1.0f / FONT_NUM_COLS;
glUniform4f(mTextCropAreaLocation, u0, v0, u1, v1);
drawTexturedQuad(*x, *y, font.char_width, font.char_height, display);
*x += font.char_width;

换句话说:

  1. 用 ASCII 码减去空格, 得到字符在图集里的序号
  2. 通过 / 16% 16 算出行列
  3. regular 字体取当前行上半区
  4. bold 字体取当前行下半区
  5. 绘制一个字符后, x += char_width, 继续绘制下一个字符

迁移到网页

在网页版本中, 我仍然使用 Canvas 2D 来复现这段逻辑. 对应 OpenGL 的 uCropArea, Canvas 可以直接用 drawImage 的 9 参数形式:

ctx.drawImage(
  font,
  sx,
  sy,
  charWidth,
  charHeight,
  drawX,
  drawY,
  charWidth * scale,
  charHeight * scale
);

其中 sxsy 就对应源码里的 u0/v0, 只是 Canvas 使用像素坐标而不是 0 到 1 的 UV 坐标.

为了方便观察, 我也加了 lil-gui:

这里的 scale 我特意限制成整数倍. clock_font.png 本质上是位图字体, 如果用 1.41.5 这种非整数缩放, 浏览器会把源像素插值到目标像素上, 看起来就会发虚. 源码里的 OpenGL 使用 GL_NEAREST, 所以网页版本也通过整数缩放、整数坐标和 image-rendering: pixelated 来尽量保持同样的锐利边缘.

下面就是迁移后的实时效果:

::boot-clock::

Android 开机时间绘制复现

根据 BootAnimation.cpp 中 drawClock() / drawText() 的字体图集裁剪逻辑迁移


在以下平台分享此文章: