bootanimation.zip开机动画你不知道的事 | desc.txt文件详解

2,499 阅读5分钟

接触过Android ROM定制开发的同学,大多有涉及过开机动画 bootanimation.zip 的制作与导入。

关于如何制作 bootanimation.zip 文件,网上已经有很多教程,大家可自行搜索。

本文将带你着重了解:

  • desc.txt 定义的开机动画究竟是如何运行的?
  • pc 类型的动画片段到底有什么区别?

首先,我们回顾一下 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() 函数,关键流程已在代码中添加注释。

  1. 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;
}
  1. 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;
}
  1. 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;
}
  1. 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. 直接跳过,动画播放结束