Bootanimatioin | OpenGL绘制分析与实战

169 阅读7分钟

在上一篇文章中开机动画的整体流程有了深入的了解,默认的开机动画在Bootanimation.cpp 中的android() 方法中实现了绘制

实现效果图如下:本次要在默认的开机动画基础上使用opengl 绘制当前的时间 展示

绘制详解

在android()方法中,首先看到的是initTexture 初始化纹理:

    //初始化纹理 加载图片 frameworks/base/core/res/assets/images/....
    initTexture(&mAndroid[0], mAssets, "images/android-logo-mask.png");
    initTexture(&mAndroid[1], mAssets, "images/android-logo-shine.png");

将两个图片加载了进来,那么这两个图片是什么呢?

aosp/frameworks/base/core/res/assets/images/android-logo-mask.png 可以看到 文字是透明的镂空的

aosp/frameworks/base/core/res/assets/images/android-logo-shine.png

其实看到这两个图片就是到要干什么了, android-logo-shine.png 是作为一个扫光的效果 不断的移动,android-logo-mask.png 遮盖到android-logo-shine.png 图片的上面,就是默认的开机动画效果

继续看下面的代码:主要做了清理屏幕的工作

 // clear screen 清理下屏幕
    glShadeModel(GL_FLAT);
    glDisable(GL_DITHER);
    glDisable(GL_SCISSOR_TEST);
    glClearColor(0,0,0,1);
    glClear(GL_COLOR_BUFFER_BIT);
    eglSwapBuffers(mDisplay, mSurface);

    glEnable(GL_TEXTURE_2D);
    glTexEnvx(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);

继续看下面的代码:设置了一块裁剪区域,用来绘制android 动画,同时设置了允许两个图片的融合,才能实现闪光的效果

    const GLint xc = (mWidth  - mAndroid[0].w) / 2;
    const GLint yc = (mHeight - mAndroid[0].h) / 2;
    //修改裁剪区域 否则 只会绘制android
    const Rect updateRect(xc, yc, xc + mAndroid[0].w, yc + mAndroid[0].h);
    //裁剪一块区域
    glScissor(updateRect.left, mHeight - updateRect.bottom, updateRect.width(),
            updateRect.height());
    // Blend state 允许两个图片的融合
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    glTexEnvx(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);

继续看下面的代码,开始了绘制:扫光的图片位于底部并且不断的移动,遮罩图片位于扫光图片的上方,这样就绘制出了开机动画

 const nsecs_t startTime = systemTime();
    do {
        nsecs_t now = systemTime();
        double time = now - startTime;
        float t = 4.0f * float(time / us2ns(16667)) / mAndroid[1].w;
        GLint offset = (1 - (t - floorf(t))) * mAndroid[1].w;
        GLint x = xc - offset;//扫光图片不断的移动

        glDisable(GL_SCISSOR_TEST);
        glClear(GL_COLOR_BUFFER_BIT);

        glEnable(GL_SCISSOR_TEST);
        glDisable(GL_BLEND);
        glBindTexture(GL_TEXTURE_2D, mAndroid[1].name);
        glDrawTexiOES(x,                 yc, 0, mAndroid[1].w, mAndroid[1].h);//对扫光的图进行绘制
        glDrawTexiOES(x + mAndroid[1].w, yc, 0, mAndroid[1].w, mAndroid[1].h);

        glEnable(GL_BLEND);//开启融合
        glBindTexture(GL_TEXTURE_2D, mAndroid[0].name);//绘制遮罩的图 带有Android的图
        glDrawTexiOES(xc, yc, 0, mAndroid[0].w, mAndroid[0].h);

        EGLBoolean res = eglSwapBuffers(mDisplay, mSurface);//显示到屏幕上
        if (res == EGL_FALSE)
            break;

        // 12fps: don't animate too fast to preserve CPU 为了不让CPU过重,1S 中绘制12张图片
        const nsecs_t sleepTime = 83333 - ns2us(systemTime() - now);
        if (sleepTime > 0)
            usleep(sleepTime);

        checkExit();
    } while (!exitPending());

当动画退出时候的扫尾工作,释放纹理:

  glDeleteTextures(1, &mAndroid[0].name);
  glDeleteTextures(1, &mAndroid[1].name);

绘制实战

了解了默认的动画绘制实现原理,如何在原有的基础上绘制一个时钟呢?

首先BootAnimation中默认有实现drawClock方法:此方法就是绘制时钟,这个方法中主要是获取当前的时间转换,通过Font 进行绘制

void BootAnimation::drawClock(const Font& font, const int xPos, const int yPos) {
    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);

    if (length != TIME_LENGTH - 1) {
        SLOGE("Couldn't format time; abandoning boot animation clock");
        mClockEnabled = false;
        return;
    }

    char* out = timeBuff[0] == ' ' ? &timeBuff[1] : &timeBuff[0];
    int x = xPos;
    int y = yPos;
    drawText(out, font, false, &x, &y);
}

这个Font 是什么呢?Font是一个结构体内部有Texture就是用来绘制的

    struct Font {
        FileMap* map;
        Texture texture;
        int char_width;
        int char_height;
    };

那么如何初始化Font呢?drawClock 需要Font的实例 initFont 对font进行了初始化:

status_t BootAnimation::initFont(Font* font, const char* fallback) {
    status_t status = NO_ERROR;

    if (font->map != nullptr) {
        glGenTextures(1, &font->texture.name);
        glBindTexture(GL_TEXTURE_2D, font->texture.name);

        status = initTexture(font->map, &font->texture.w, &font->texture.h);

        glTexParameterx(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
        glTexParameterx(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
        glTexParameterx(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
        glTexParameterx(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    } else if (fallback != nullptr) {
        status = initTexture(&font->texture, mAssets, fallback);
    } else {
        return NO_INIT;
    }

    if (status == NO_ERROR) {
        font->char_width = font->texture.w / FONT_NUM_COLS;
        font->char_height = font->texture.h / FONT_NUM_ROWS / 2;  // There are bold and regular rows
    }

    return status;
}

fallback 不为空时就会初始化纹理,那么这个fallback 是什么呢?其实就是图片 ,opengl要想渲染时钟上的数据,不能直接设置字符,只能通过图片来进行绘制文件,可以在aosp/frameworks/base/core/res/assets/images/clock_font.png 找到时钟的一个图片,正是导入这个图片opengl才可以去绘制时钟上的文字

可以来看下initTexture 方法如下:传递参数会对texture进行初始化,AssetManager 资源的管理类,name就是资源的名称,initTexture方法主要是将资源文件转换为bitmap 获取了资源文件的信息,并且生成纹理进行绑定

status_t BootAnimation::initTexture(Texture* texture, AssetManager& assets,
        const char* name) {
    Asset* asset = assets.open(name, Asset::ACCESS_BUFFER);
    if (asset == nullptr)
        return NO_INIT;
    SkBitmap bitmap;//将文件转换为bitmap
    sk_sp<SkData> data = SkData::MakeWithoutCopy(asset->getBuffer(false),
            asset->getLength());
    sk_sp<SkImage> image = SkImage::MakeFromEncoded(data);
    image->asLegacyBitmap(&bitmap, SkImage::kRO_LegacyBitmapMode);
    asset->close();
    delete asset;
    //获取图片的宽高数据
    const int w = bitmap.width();
    const int h = bitmap.height();
    const void* p = bitmap.getPixels();

    GLint crop[4] = { 0, h, w, -h };
    texture->w = w;
    texture->h = h;
    //opengl生成纹理
    glGenTextures(1, &texture->name);
    glBindTexture(GL_TEXTURE_2D, texture->name);//绑定这个纹理 后面的操作都是针对这个纹理的
    ........
    return NO_ERROR;
}

从上述可以知道drawClock方法,首先会initFont 通过资源文件,初始化纹理,然后获取当前的时间转换为固定格式的字符,通过drawText方法绘制时钟的文字,drawText 方法如下:

主要是将需要绘制的字符串,for循环字符串中的字符,对资源文件转换的纹理进行裁减,比如1 会找到图片的中1进行裁减

void BootAnimation::drawText(const char* str, const Font& font, bool bold, int* x, int* y) {
    glEnable(GL_BLEND);  // Allow us to draw on top of the animation
    glBindTexture(GL_TEXTURE_2D, font.texture.name);
    const int len = strlen(str);
    const int strWidth = font.char_width * len;
    if (*x == TEXT_CENTER_VALUE) {
        *x = (mWidth - strWidth) / 2;
    } else if (*x < 0) {
        *x = mWidth + *x - strWidth;
    }
    if (*y == TEXT_CENTER_VALUE) {
        *y = (mHeight - font.char_height) / 2;
    } else if (*y < 0) {
        *y = mHeight + *y - font.char_height;
    }
    int cropRect[4] = { 0, 0, font.char_width, -font.char_height };
    for (int i = 0; i < len; i++) {//对纹理的裁减
        char c = str[i];
        if (c < FONT_BEGIN_CHAR || c > FONT_END_CHAR) {
            c = '?';
        }
        // Crop the texture to only the pixels in the current glyph
        const int charPos = (c - FONT_BEGIN_CHAR);  // Position in the list of valid characters
        const int row = charPos / FONT_NUM_COLS;
        const int col = charPos % FONT_NUM_COLS;
        cropRect[0] = col * font.char_width;  // Left of column
        cropRect[1] = row * font.char_height * 2; // Top of row
        // Move down to bottom of regular (one char_heigh) or bold (two char_heigh) line
        cropRect[1] += bold ? 2 * font.char_height : font.char_height;
        glTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_CROP_RECT_OES, cropRect);
        glDrawTexiOES(*x, *y, 0, font.char_width, font.char_height);
        *x += font.char_width;
    }
    glDisable(GL_BLEND);  // Return to the animation's default behaviour
    glBindTexture(GL_TEXTURE_2D, 0);
}

ok,上述一系列分析中,已经知道了如何绘制时钟了,那么开始写代码:

  1. 首先需要一个Font的变量,用来保存绘制时钟的纹理
--- a/cmds/bootanimation/BootAnimation.h
+++ b/cmds/bootanimation/BootAnimation.h
@@ -166,6 +166,7 @@ private:
     sp<SurfaceComposerClient>       mSession;
     AssetManager mAssets;
     Texture     mAndroid[2];
+    Font        mClockFont;
     int         mWidth;
     int         mHeight;
     int         mCurrentInset;

2. 在android()方法中初始化Font 以及绘制时钟,CLOCK_FONT_ASSET 就是资源文件的位置

@@ -398,12 +402,20 @@ bool BootAnimation::android()
 {
     SLOGD("%sAnimationShownTiming start time: %" PRId64 "ms", mShuttingDown ? "Shutdown" : "Boot",
             elapsedRealtime());
+    //初始化纹理 加载图片 frameworks/base/core/res/assets/images/....
     initTexture(&mAndroid[0], mAssets, "images/android-logo-mask.png");
     initTexture(&mAndroid[1], mAssets, "images/android-logo-shine.png");
 
     mCallbacks->init({});
+    //初始化initFont
+    bool hasInitFont = false;
+    //CLOCK_FONT_ASSET 是一个图片 opengl不支持直接字符显示, opengl需要根据图片的纹理来进行绘制 将图片加载了近来 bitmap
+    if (initFont(&mClockFont,CLOCK_FONT_ASSET) == NO_ERROR){
+        hasInitFont = true;
+        ALOGD("android init Font ok,font name = %u",mClockFont.texture.name);
+    }

修改裁减区域,默认的裁减区域只有android区域,需要扩大区域,如果不扩大就看不到绘制的时钟:

@@ -416,12 +428,13 @@ bool BootAnimation::android()
 
     const GLint xc = (mWidth  - mAndroid[0].w) / 2;
     const GLint yc = (mHeight - mAndroid[0].h) / 2;
-    const Rect updateRect(xc, yc, xc + mAndroid[0].w, yc + mAndroid[0].h);
-
+    //修改裁剪区域 否则 只会绘制android区域,时钟区域就显示不出来
+    const Rect updateRect(xc, yc, xc + mAndroid[0].w, yc + mAndroid[0].h * 2);
+    //裁剪一块区域
     glScissor(updateRect.left, mHeight - updateRect.bottom, updateRect.width(),
-            updateRect.height());
+            updateRect.height() * 2);

然后在do-while循环中添加时钟绘制方法:

+        drawClock(mClockFont,TEXT_CENTER_VALUE,yc + mAndroid[0].h);//绘制时钟,位置在水平居中,在Android的上面
+        EGLBoolean res = eglSwapBuffers(mDisplay, mSurface);//显示到屏幕上
         if (res == EGL_FALSE)
             break;
 
-        // 12fps: don't animate too fast to preserve CPU
+        // 12fps: don't animate too fast to preserve CPU 为了不让CPU过重,1S 中绘制12张图片
         const nsecs_t sleepTime = 83333 - ns2us(systemTime() - now);
         if (sleepTime > 0)
             usleep(sleepTime);

do-while循环结束,退出开机动画的时候需要释放纹理:

@@ -460,13 +474,18 @@ bool BootAnimation::android()
 
     glDeleteTextures(1, &mAndroid[0].name);
     glDeleteTextures(1, &mAndroid[1].name);
+    //释放时钟的纹理
+    if (hasInitFont){
+        ALOGD("clear clock gl delete texture");
+        glDeleteTextures(1,&mClockFont.texture.name);
+    }
     return false;
 }
 

这里就完成了添加时钟的绘制,编译启动模拟器查看是否成功:

make
emulator