接触过Android ROM定制开发的同学,大多有涉及过开机动画 bootanimation.zip 的制作与导入。
关于如何制作 bootanimation.zip 文件,网上已经有很多教程,大家可自行搜索。
本文将带你着重了解:
desc.txt定义的开机动画究竟是如何运行的?p和c类型的动画片段到底有什么区别?
首先,我们回顾一下 desc.txt 文件格式,了解每一行描述的具体含义。然后,我们一起深入源码,从 开机动画播放流程 分析desc.txt是如何控制开机动画播放的。最后,我将结合一条实例,总结开机动画与desc.txt文件的关系。
desc.txt 文件格式
开机动画描述文件 desc.txt,由 1 + N 行内容构成。
第 1 行指定开机动画的 分辨率 与 帧率。
WIDTH HEIGHT FPS
- WIDTH: animation width (pixels)
- HEIGHT: animation height (pixels)
- FPS: frames per second, e.g. 60
接下来的 N 行,每一行分别描述一组动画的播放逻辑。
TYPE COUNT PAUSE PATH [#RGBHEX CLOCK]
- TYPE: a single char indicating what type of animation segment this is:
- p – this part will play unless interrupted by the end of the boot (系统一旦完成启动,立即结束动画)
- c – this part will play to completion, no matter what (无论发生什么,都会一直播放动画)
- COUNT: how many times to play the animation, or 0 to loop forever until boot is complete
- PAUSE: number of FRAMES to delay after this part ends (该行结束后,间隔多少帧执行下一组动画)
- PATH: directory in which to find the frames for this part (e.g. part0)
- RGBHEX: (OPTIONAL) a background color, specified as #RRGGBB
- CLOCK: (OPTIONAL) the y-coordinate at which to draw the current time (for watches)
1280 720 20 // 1280 720表示动画在屏幕中的显示宽、高,20表示帧率为【1/20 s】
p 0 1 part0 // 第一组动画,0表示无限循环播放,1表示与下一次动画播放之间间隔的时间【1帧 * fps】,part0表示播放part0目录中的所有图像帧
c 1 0 part1 // 第二组动画,1表示播放1次,0表示本次动画播放结束后,立即播放下一次动画,part1表示表示播放part1目录中的所有图像帧
开机动画播放流程
下面将结合代码,详细介绍一下开机动画的播放逻辑,以及 desc.txt 的文件内容是如何影响开机动画的播放的。
播放开机动画的核心代码,位于 frameworks/base/cmds/bootanimation/BootAnimation.cpp 中,我们重点关注 threadLoop()
-> movie() -> playAnimation(const Animation& animation) -> checkExit() 函数,关键流程已在代码中添加注释。
- threadLoop()
// BootAnimation.cpp 的程序入口由 main() 进入,初始化完线程后,将循环调用 threadLoop 方法,直到 threadLoop 返回 false
bool BootAnimation::threadLoop() {
bool result;
initShaders();
// We have no bootanimation file, so we use the stock android logo
// animation.
if (mZipFileName.isEmpty()) {
result = android();
} else {
// 1. 若配置了bootanimation.zip文件,将会进入此处。注意,若 movie() 返回了false,将退出 threadLoop() 的循环调用,整个开机动画退出。
result = movie();
}
mCallbacks->shutdown();
eglMakeCurrent(mDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
eglDestroyContext(mDisplay, mContext);
eglDestroySurface(mDisplay, mSurface);
mFlingerSurface.clear();
mFlingerSurfaceControl.clear();
eglTerminate(mDisplay);
eglReleaseThread();
IPCThreadState::self()->stopProcess();
return result;
}
- movie()
bool BootAnimation::movie() {
if (mAnimation == nullptr) {
// 2. 载入bootanimation.zip动画文件内容
mAnimation = loadAnimation(mZipFileName);
}
if (mAnimation == nullptr)
return false;
// 省略了部分代码
...
// 3. 开始播放动画
playAnimation(*mAnimation);
if (mTimeCheckThread != nullptr) {
mTimeCheckThread->requestExit();
mTimeCheckThread = nullptr;
}
if (clockFontInitialized) {
glDeleteTextures(1, &mAnimation->clockFont.texture.name);
}
releaseAnimation(mAnimation);
mAnimation = nullptr;
// 动画播放结束后,返回false,触发上层退出循环
return false;
}
- playAnimation(const Animation& animation)
// desc.txt文件配置的播放逻辑在该函数中执行
bool BootAnimation::playAnimation(const Animation& animation) {
// pcount 表示动画片段个数,即通过 p或c 指定的行数
const size_t pcount = animation.parts.size();
// 计算播放每帧的时间
nsecs_t frameDuration = s2ns(1) / animation.fps;
SLOGD("%sAnimationShownTiming start time: %" PRId64 "ms", mShuttingDown ? "Shutdown" : "Boot",
elapsedRealtime());
int fadedFramesCount = 0;
int lastDisplayedProgress = 0;
// 4. 第一层循环,每一次执行一组动画片段,对应 desc.txt 中的一行动画描述
for (size_t i=0 ; i<pcount ; i++) {
const Animation::Part& part(animation.parts[i]);
// fcount 表示此组动画片段,对应资源文件夹中的图片张数
// 例如:p 2 0 part0,表示part0文件夹中图片数量
const size_t fcount = part.frames.size();
// Handle animation package
if (part.animation != nullptr) {
playAnimation(*part.animation);
if (exitPending())
break;
continue; //to next part
}
// process the part not only while the count allows but also if already fading
// 5. 第二层循环,part.count 表示本组动画片段的播放次数,若 part.count = 0 表示无线循环播放
// 例如:p 2 0 part0,表示播放2次part0中的帧图片
for (int r=0 ; !part.count || r<part.count || fadedFramesCount > 0 ; r++) {
/*
这里为了方便阅读,我这里直接把 shouldStopPlayingPart 的代码实现贴过来:
// exitPending() 表示开机流程完成,系统已将「service.bootanim.exit」标志位置为true
// part.playUntilComplete 若动画片段类型为p,此值为false;若为c,此值为true,从而c类型动画片段将不受影响
if( exitPending() && !part.playUntilComplete
&& fadedFramesCount >= part.framesToFadeCount
&&(lastDisplayedProgress == 0 || lastDisplayedProgress == 100))
*/
if (shouldStopPlayingPart(part, fadedFramesCount, lastDisplayedProgress)) break;
mCallbacks->playPart(i, part, r);
glClearColor(
part.backgroundColor[0],
part.backgroundColor[1],
part.backgroundColor[2],
1.0f);
// For the last animation, if we have progress indicator from
// the system, display it.
int currentProgress = android::base::GetIntProperty(PROGRESS_PROP_NAME, 0);
bool displayProgress = animation.progressEnabled &&
(i == (pcount -1)) && currentProgress != 0;
// 6. 第三层循环,每一次循环,将绘制一帧图片
for (size_t j=0 ; j<fcount ; j++) {
// 7. 同上面该函数的解释,检查是否应该退出; 不退出将开始绘制一帧图片
if (shouldStopPlayingPart(part, fadedFramesCount, lastDisplayedProgress)) break;
// Color progress is
// - the animation progress, normalized from
// [colorTransitionStart,colorTransitionEnd] to [0, 1] for the dynamic coloring
// part.
// - 0 for parts that come before,
// - 1 for parts that come after.
float colorProgress = part.useDynamicColoring
? fmin(fmax(
((float)j - animation.colorTransitionStart) /
fmax(animation.colorTransitionEnd -
animation.colorTransitionStart, 1.0f), 0.0f), 1.0f)
: (part.postDynamicColoring ? 1 : 0);
processDisplayEvents();
const int animationX = (mWidth - animation.width) / 2;
const int animationY = (mHeight - animation.height) / 2;
const Animation::Frame& frame(part.frames[j]);
nsecs_t lastFrame = systemTime();
if (r > 0) {
glBindTexture(GL_TEXTURE_2D, frame.tid);
} else {
glGenTextures(1, &frame.tid);
glBindTexture(GL_TEXTURE_2D, frame.tid);
int w, h;
// Set decoding option to alpha unpremultiplied so that the R, G, B channels
// of transparent pixels are preserved.
initTexture(frame.map, &w, &h, false /* don't premultiply alpha */);
}
const int xc = animationX + frame.trimX;
const int yc = animationY + frame.trimY;
glClear(GL_COLOR_BUFFER_BIT);
// specify the y center as ceiling((mHeight - frame.trimHeight) / 2)
// which is equivalent to mHeight - (yc + frame.trimHeight)
const int frameDrawY = mHeight - (yc + frame.trimHeight);
float fade = 0;
// if the part hasn't been stopped yet then continue fading if necessary
if (exitPending() && part.hasFadingPhase()) {
fade = static_cast<float>(++fadedFramesCount) / part.framesToFadeCount;
if (fadedFramesCount >= part.framesToFadeCount) {
fadedFramesCount = MAX_FADED_FRAMES_COUNT; // no more fading
}
}
glUseProgram(mImageShader);
glUniform1i(mImageTextureLocation, 0);
glUniform1f(mImageFadeLocation, fade);
if (animation.dynamicColoringEnabled) {
glUniform1f(mImageColorProgressLocation, colorProgress);
}
glEnable(GL_BLEND);
drawTexturedQuad(xc, frameDrawY, frame.trimWidth, frame.trimHeight);
glDisable(GL_BLEND);
if (mClockEnabled && mTimeIsAccurate && validClock(part)) {
drawClock(animation.clockFont, part.clockPosX, part.clockPosY);
}
if (displayProgress) {
int newProgress = android::base::GetIntProperty(PROGRESS_PROP_NAME, 0);
// In case the new progress jumped suddenly, still show an
// increment of 1.
if (lastDisplayedProgress != 100) {
// Artificially sleep 1/10th a second to slow down the animation.
usleep(100000);
if (lastDisplayedProgress < newProgress) {
lastDisplayedProgress++;
}
}
// Put the progress percentage right below the animation.
int posY = animation.height / 3;
int posX = TEXT_CENTER_VALUE;
drawProgress(lastDisplayedProgress, animation.progressFont, posX, posY);
}
handleViewport(frameDuration);
eglSwapBuffers(mDisplay, mSurface);
nsecs_t now = systemTime();
// 8. 计算 绘制一帧的指定用时 与 绘制一帧的实际用时 之间的 间隔
nsecs_t delay = frameDuration - (now - lastFrame);
//SLOGD("%lld, %lld", ns2ms(now - lastFrame), ns2ms(delay));
lastFrame = now;
// 若实际绘制过快,将延时等待
if (delay > 0) {
struct timespec spec;
spec.tv_sec = (now + delay) / 1000000000;
spec.tv_nsec = (now + delay) % 1000000000;
int err;
do {
err = clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &spec, nullptr);
} while (err == EINTR);
}
// 9. 每绘制完一帧,更新一下动画播放状态
checkExit();
}
// 10. 当播放完一次资源文件夹中的所有帧后,暂停指定的时间后,再播放下一次动画
// 例如:p 2 0 part0,表示播完一次part0中的图片帧后,间隔 0 帧(立即播放)下一次动画
usleep(part.pause * ns2us(frameDuration));
// 11. 判断是否结束播放动画
// !part.count 为了防止c类型片段无限播放
// 例如:c 0 0 part0,动画播放一次就将退出
// 可见,对于c类型片段,设置无限循环播放是无效的
if (exitPending() && !part.count && mCurrentInset >= mTargetInset &&
!part.hasFadingPhase()) {
if (lastDisplayedProgress != 0 && lastDisplayedProgress != 100) {
android::base::SetProperty(PROGRESS_PROP_NAME, "100");
continue;
}
break; // exit the infinite non-fading part when it has been played at least once
}
}
}
// Free textures created for looping parts now that the animation is done.
for (const Animation::Part& part : animation.parts) {
if (part.count != 1) {
const size_t fcount = part.frames.size();
for (size_t j = 0; j < fcount; j++) {
const Animation::Frame& frame(part.frames[j]);
glDeleteTextures(1, &frame.tid);
}
}
}
return true;
}
- checkExit()
// 检测是否应该退出开机动画播放
void BootAnimation::checkExit() {
// Allow surface flinger to gracefully request shutdown
char value[PROPERTY_VALUE_MAX];
// 通过读取属性「service.bootanim.exit」判断是否退出动画
property_get(EXIT_PROP_NAME, value, "0");
int exitnow = atoi(value);
if (exitnow) {
// 调用该行代码后,exitPending()的返回值将变为true
requestExit();
}
}
总结
从上一个小节的核心代码流程,可得出结论:
-
在开机动画播放过程中,在绘制每一帧之前 都会检测是否应该退出动画播放,一旦为true:
-
若当前正在播放p类型动画片段,则立即退出本段动画片段播放,直接进入到下一行c类型的动画片段播放。此时剩余的未播放的p类型动画都不会播放。
-
若当前正在播放c类型动画片段,将正常播放完本段动画,然后进入到下一行c类型的动画片段播放。
-
- c类型的动画片段设置为无限循环播放是不生效的,此时动画片段只会播放一次。
p 1 0 part0 // 1. part0播放过程中,检测到退出播放
p 2 0 part1 // 2. 直接跳过
c 2 0 part2 // 3. 播放2次part2中的动画
c 0 0 part3 // 4. 播放1次part3中的动画
p 1 0 part4 // 5. 直接跳过,动画播放结束