Preface
In the last article, I studied the shine animation of the default Android logo in BootAnimation.cpp. The core of that logic is two textures: a very long highlight image, and a mask with alpha.
When you continue to look at the source code, you will find that the startup animation can also superimpose time on the screen. It does not call the system font rendering, nor does it pre-make the time into a picture frame. Instead, it uses a clock_font.png font atlas, cuts out each character from it, and then draws it word by word on the screen.
This article still only focuses on one question: How BootAnimation draws the current time to the boot animation.
Resource acquisition
The resource used for time drawing is clock_font.png. I put it in the same directory as the resource in the previous article:
public/images/archives/shine-logo/clock_font.png
The size of this image is 640 x 576. On the surface it looks like just white text on a black background, but the source code will treat it as a character table with fixed specifications:
- There are
16columns in total - The printable ASCII character range is spaces
' 'to'~' - The width of each character grid is
640 / 16 = 40 - The character table has a total of
6rows - Each line is divided into upper and lower halves: the upper half is regular, and the lower half is bold
- So the actual character height is
576 / 6 / 2 = 48
Font initialization
The font initialization entry in BootAnimation.cpp is 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;
}
The most important thing here is why char_height needs to be divided by 2. The source code comments are very straightforward:
// There are bold and regular rows
In other words, the font atlas is not simply 16 x 6, but both regular and bold versions are stuffed into the grid height of each character.
time format
The actual function of plotting time is 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);
}
Here are two details:
- Using
%H:%Min 24-hour format, you will get something like08:30 - The 12-hour clock uses
%l:%M. When it is less than 10 o’clock, there will be a space in front, so the space will be skipped later bytimeBuff[0] == ' '
The final time string will be given to drawText.
Draw word by word
drawText’s idea is very straightforward:
const int len = strlen(str);
const int strWidth = font.char_width * len;
First calculate the width of the entire text based on the number of characters. If x or y is a center mark, convert it to screen coordinates:
if (*x == TEXT_CENTER_VALUE) {
*x = (display.width - strWidth) / 2;
}
if (*y == TEXT_CENTER_VALUE) {
*y = (display.height - font.char_height) / 2;
}
If the coordinate is negative, it means positioning backward from the right or bottom of the screen:
if (*x < 0) {
*x = display.width + *x - strWidth;
}
if (*y < 0) {
*y = display.height + *y - font.char_height;
}
Next is the atlas cropping of each character:
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;
In other words:
- Subtract the spaces from the ASCII code to get the serial number of the character in the atlas.
- Find the ranks using
/ 16and% 16 - Regular font takes the upper half of the current line
- bold font takes the lower half of the current line
- After drawing a character,
x += char_width, continue to draw the next character
Move to web
In the web version, I still use Canvas 2D to reproduce this logic. Corresponding to OpenGL’s uCropArea, Canvas can directly use the 9-parameter form of drawImage:
ctx.drawImage(
font,
sx,
sy,
charWidth,
charHeight,
drawX,
drawY,
charWidth * scale,
charHeight * scale
);
Among them, sx and sy correspond to u0/v0 in the source code, except that Canvas uses pixel coordinates instead of UV coordinates from 0 to 1.
For the convenience of observation, I also added lil-gui:
system time: Use current system timetext: Manually enter the string to draw12h: Switch to 12-hour formatseconds: Whether to display secondsbold: Switch to the bold characters in the lower half of the galleryx/y: Adjust the drawing position,0means centeredscale: Enlarge or reduce glyphs by integer times
Here scale I deliberately limited it to an integer multiple. clock_font.png is essentially a bitmap font. If you use non-integer scaling like 1.4 and 1.5, the browser will interpolate the source pixels to the target pixels, which will look blurry. OpenGL usage in the source code GL_NEAREST, so the web version also tries to maintain the same sharp edges through integer scaling, integer coordinates and image-rendering: pixelated.
The following is the real-time effect after migration:
::boot-clock::
Android 开机时间绘制复现
根据 BootAnimation.cpp 中 drawClock() / drawText() 的字体图集裁剪逻辑迁移