Bootanimation | 开机动画zip绘制分析与实战

283 阅读7分钟

前面几篇讲解了开机动画的整体流程以及开机动画OpenGL绘制分析与实战整体的开机动画的原理差不多已经莫清楚了,zip的形式是目前的主流形式,详细讲解zip绘制原理.

1 如何使用zip

如何通过zip的形式绘制开机动画呢?一般我们都会从网络上搜索,但是如果没有网络如何去查找呢?

其实google提供的源码中已经写好了,进入到源码的bootanimation目录会发现里面有一个FORMAT.md这个文件就是告诉你如何通过zip去绘制开机动画.

这个文件的内容如下:里面已经告诉你zip这个文件放到哪里,以及zip文件中的内容布局等等.

## zipfile paths

The system selects a boot animation zipfile from the following locations, in order:

    /system/media/bootanimation-encrypted.zip (if getprop("vold.decrypt") = '1')
    /system/media/bootanimation.zip
    /oem/media/bootanimation.zip

zipfile paths 这个就不用说了,这个是告诉你zip文件可以放到哪里,一般都会放到/system/media/中。

在来看zipfile layout 部分,这就是zip的资源文件,part0 .... partN 是图片帧,里面保存的就是一帧帧图片,desc.txt 是描述文件

## zipfile layout

The `bootanimation.zip` archive file includes:

    desc.txt - a text file
    part0  \
    part1   \  directories full of PNG frames
    ...     /
    partN  /

desc.txt 的描述格式,FPS 就是帧率,例如part0 里面有120张图片,例如1S 120fps,1S中就会执行120张帧图片

## desc.txt format

The first line defines the general parameters of the animation:

    WIDTH HEIGHT FPS

  * **WIDTH:** animation width (pixels)   -- 宽
  * **HEIGHT:** animation height (pixels) -- 高
  * **FPS:** frames per second, e.g. 60   -- 帧率

It is followed by a number of rows of the form:

    TYPE COUNT PAUSE PATH [#RGBHEX [CLOCK1 [CLOCK2]]]

  * **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 : 0 表示一直循环,直到开机动画属性置位1 开机动画结束
  * **COUNT:** how many times to play the animation, or 0 to loop forever until boot is complete
      PAUSE :当一个part执行完毕后,停留几帧,1 表示 16ms,0 表示不停留
  * **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`
  * **CLOCK1, CLOCK2:** _(OPTIONAL)_ the coordinates at which to draw the current time (for watches) 

2 实现原理

在前两篇文章中分析了bootanimation的原理,在 开机动画的整体流程 中,可以指导当mZipFileName 不为空的时候执行moive方法

bool BootAnimation::movie()
{
    if (mAnimation == nullptr) {
        mAnimation = loadAnimation(mZipFileName);
    }

    if (mAnimation == nullptr)
        return false;

    // mCallbacks->init() may get called recursively,
    // this loop is needed to get the same results
    for (const Animation::Part& part : mAnimation->parts) {
        if (part.animation != nullptr) {
            mCallbacks->init(part.animation->parts);
        }
    }
    mCallbacks->init(mAnimation->parts);

   ...... opengl 初始化纹理

    playAnimation(*mAnimation);
   .......

    releaseAnimation(mAnimation);
    mAnimation = nullptr;

    return false;
}

在上述代码中,会先执行loadAnimation方法,解析zip文件的内容:

如果看过该系列第一篇文章,你会发现BootAnimation::onFirstRef() 方法中,调用了preloadAnimation 并且找到zip文件,当mZipFileName不为空的时候调用了loadAnimation方法,并且mAnimation 赋值了,到执行到moive方法可以直接开始动画,这里做了一些小优化,继续看loadAnimation 如何解析zip文件的

  1. 打开zip压缩文件
  2. 构造Animation对象
  3. parseAnimationDesc 解析描述文件desc.txt,记录执行规则
  4. preloadZip 将所有的图片加载进来
BootAnimation::Animation* BootAnimation::loadAnimation(const String8& fn)
{
    if (mLoadedFiles.indexOf(fn) >= 0) {
        SLOGE("File "%s" is already loaded. Cyclic ref is not allowed",
            fn.string());
        return nullptr;
    }
    //打开zip文件
    ZipFileRO *zip = ZipFileRO::open(fn);
    if (zip == nullptr) {
        SLOGE("Failed to open animation zip "%s": %s",
            fn.string(), strerror(errno));
        return nullptr;
    }
    //构造Animation对象
    Animation *animation =  new Animation;
    animation->fileName = fn;
    animation->zip = zip;
    animation->clockFont.map = nullptr;
    mLoadedFiles.add(animation->fileName);
    parseAnimationDesc(*animation);//解析描述文件desc.txt
    if (!preloadZip(*animation)) {//加载所有的图片进来
        return nullptr;
    }
    mLoadedFiles.remove(fn);
    return animation;
}

如下parseAnimationDesc,解析后的文本按照行数进行解析,这部分代码很简单:

bool BootAnimation::parseAnimationDesc(Animation& animation)
{
    String8 desString;

    if (!readFile(animation.zip, "desc.txt", desString)) {
        return false;
    }
    char const* s = desString.string();
    // Parse the description file
    for (;;) {
        const char* endl = strstr(s, "\n");
        if (endl == nullptr) break;
        String8 line(s, endl - s);
        const char* l = line.string();
        int fps = 0;
        int width = 0;
        int height = 0;
        int count = 0;
        int pause = 0;
        char path[ANIM_ENTRY_NAME_MAX];
        char color[7] = "000000"; // default to black if unspecified
        char clockPos1[TEXT_POS_LEN_MAX + 1] = "";
        char clockPos2[TEXT_POS_LEN_MAX + 1] = "";
        char pathType;
        if (sscanf(l, "%d %d %d", &width, &height, &fps) == 3) {
            // SLOGD("> w=%d, h=%d, fps=%d", width, height, fps);
            animation.width = width;
            animation.height = height;
            animation.fps = fps;
        } else if (sscanf(l, " %c %d %d %s #%6s %16s %16s",
                          &pathType, &count, &pause, path, color, clockPos1, clockPos2) >= 4) {
            //SLOGD("> type=%c, count=%d, pause=%d, path=%s, color=%s, clockPos1=%s, clockPos2=%s",
            //    pathType, count, pause, path, color, clockPos1, clockPos2);
            Animation::Part part;
            part.playUntilComplete = pathType == 'c';
            part.count = count;
            part.pause = pause;
            part.path = path;
            part.audioData = nullptr;
            part.animation = nullptr;
            if (!parseColor(color, part.backgroundColor)) {
                SLOGE("> invalid color '#%s'", color);
                part.backgroundColor[0] = 0.0f;
                part.backgroundColor[1] = 0.0f;
                part.backgroundColor[2] = 0.0f;
            }
            parsePosition(clockPos1, clockPos2, &part.clockPosX, &part.clockPosY);
            animation.parts.add(part);
        }
       ......
        s = ++endl;
    }

    return true;
}

然后看preloadZip 核心代码:Animation 中有变量 Vector<Part> parts,用来存储每个part的图片帧,Part结构体中有SortedVector<Frame> frames; 每一帧的图片都会存储到frames中,同时也可以看到leaf == "audio.wav" 说明开机动画是支持音频的

  for (size_t j = 0; j < pcount; j++) {
                if (path == animation.parts[j].path) {
                    uint16_t method;
                    // supports only stored png files
                    if (zip->getEntryInfo(entry, &method, nullptr, nullptr, nullptr, nullptr, nullptr)) {
                        if (method == ZipFileRO::kCompressStored) {
                            FileMap* map = zip->createEntryFileMap(entry);
                            if (map) {
                                Animation::Part& part(animation.parts.editItemAt(j));
                                if (leaf == "audio.wav") {
                                    // a part may have at most one audio file
                                    part.audioData = (uint8_t *)map->getDataPtr();
                                    part.audioLength = map->getDataLength();
                                } else if (leaf == "trim.txt") {
                                    part.trimData.setTo((char const*)map->getDataPtr(),
                                                        map->getDataLength());
                                } else {
                                    Animation::Frame frame;
                                    frame.name = leaf;
                                    frame.map = map;
                                    frame.trimWidth = animation.width;
                                    frame.trimHeight = animation.height;
                                    frame.trimX = 0;
                                    frame.trimY = 0;
                                    part.frames.add(frame);
                                }
                            }
                        } else {
                            SLOGE("bootanimation.zip is compressed; must be only stored");
                        }
                    }
                }
            }

ok loadAnimation方法执行完毕,这时候zip的文件资源的信息已经全部知道了,继续分析movie方法,下面就是playAnimation 播放开机动画:

for (size_t i=0 ; i<pcount ; i++) {//part0 --- partN
        const Animation::Part& part(animation.parts[i]);// 获取Part实例
        const size_t fcount = part.frames.size();//获取图片的帧数
        .....
           //desc.txt COUNT如果配置为0 表示一直循环
           for (int r=0 ; !part.count || r<part.count ; r++) {
            ......
            //desc.txt RGBHEX配置,背景颜色
            glClearColor(
                    part.backgroundColor[0],
                    part.backgroundColor[1],
                    part.backgroundColor[2],
                    1.0f);

            for (size_t j=0 ; j<fcount && (!exitPending() || part.playUntilComplete) ; j++) {
                const Animation::Frame& frame(part.frames[j]);//获取每个图片帧开始绘制
                nsecs_t lastFrame = systemTime();
                 //使用opengl 进行绘制
                  if (r > 0) {
                      glBindTexture(GL_TEXTURE_2D, frame.tid);
                  } else {
                      if (part.count != 1) {
                          glGenTextures(1, &frame.tid);
                          glBindTexture(GL_TEXTURE_2D, frame.tid);
                          glTexParameterx(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
                          glTexParameterx(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
                      }
                      int w, h;
                      initTexture(frame.map, &w, &h);
                  }
                  .......
                  checkExit();//检查开机动画结束标记
                }
                usleep(part.pause * ns2us(frameDuration));//执行完一个part 停留的帧数
                ...
           }      
           .....
}

3 实战自定义的zip开机动画

准备开机动画资源,编写desc.txt 文件:

1080 360 60                # 宽x高=1080x192, fps=60帧
c 1 0 part0 #ffee00 c c    # part0 只播放1次,但需全部图片都播放,不能打断 最后两个c c 是坐标在中间的位置
c 0 0 part1 #ffee00 c c    # part1  COUNT:0 只要开机动画结束属性不为1 这个part1就会一直循环播放
c 1 0 part2 #ffee00 c c    # 背景颜色 #ffee00
c 1 1 part3 #ffee00 c c    # 只播放1次,但需全部图片都播放,不能打断,在播放结束时延迟1帧,即1/fps=1/60秒
c 1 0 part4 #ffee00 c c

注意打包zip要使用存储方式命令如下:

zip -r -X -Z store bootanimation part*/* desc.txt

接着我们把 bootanimation.zip 动画文件预制到 /system/media/ 目录下(你需要添加自己的品牌设备添加Product):

把 bootanimation.zip 动画文件移动到 device/sufulu/su_os 文件中。

在我们的自定义 Product 配置文件 device/sufulu/su_os/su_os.mk 中添加如下内容

PRODUCT_ARTIFACT_PATH_REQUIREMENT_WHITELIST += \
    root/init.zygote32_64.rc \
    root/init.zygote64_32.rc \
    /system/media/bootanimation.zip \

# Copy different zygote settings for vendor.img to select by setting property
# ro.zygote=zygote64_32 or ro.zygote=zygote32_64:
#   1. 64-bit primary, 32-bit secondary OR
#   2. 32-bit primary, 64-bit secondary
# init.zygote64_32.rc is in the core_64_bit.mk below
PRODUCT_COPY_FILES += \
    system/core/rootdir/init.zygote32_64.rc:root/init.zygote32_64.rc \
    $(LOCAL_PATH)/bootanimation.zip:/system/media/bootanimation.zip \

如果你使用的AOSP 8及以下的,只需要修改bootanimation/Android.mk

$(shell cp $(LOCAL_PATH)/bootanimation.zip $(ANDROID_PRODUCT_OUT)/system/media/bootanimation.zip)

回顾整个开机动画的流程如下:从系统启动→ init进程启动(属性系统监听) → sf进程启动 → bootanimation启动 → 播放那个开机动画 -> 开机动画绘制 → 何时结束开机动画的完整流程