Jimmy Chen

A Programmer

(原创)Android充电动画分析

  今天来分析一下Android的关机充电动画是如何实现的。至于充电启动的流程,之后有时间再做分析,这里主要分析跟Android关机充电相关的部分。不多说直接开始分析代码吧。

healthd_mode_charger_init

  首先是healthd_mode_charger.cpp文件的healthd_mode_charger_init函数,这个函数是充电模式animation的初始化函数

void healthd_mode_charger_init(struct healthd_config* config) {
    int ret;
    // charger的状态是charger_state
    charger* charger = &charger_state;
    int i;
    int epollfd;

    // 初始化kernel log
    dump_last_kmsg();

    LOGW("--------------- STARTING CHARGER MODE ---------------\n");

    ret = ev_init(std::bind(&input_callback, charger, std::placeholders::_1, std::placeholders::_2));
    if (!ret) {
        epollfd = ev_get_epollfd();
        healthd_register_event(epollfd, charger_event_handler, EVENT_WAKEUP_FD);
    }

    // 解析animation.txt文件的内容,初始化animation结构体,下面会详细分析
    // (大家可以先阅读这个函数的内容是什么,然后再返回这里往下看)
    animation* anim = init_animation();
    charger->batt_anim = anim;

    // 看函数名及参数可知,用于创建电池状态出错的界面
    ret = res_create_display_surface(anim->fail_file.c_str(), &charger->surf_unknown);
    if (ret < 0) {
        LOGE("Cannot load custom battery_fail image. Reverting to built in.\n");
        ret = res_create_display_surface("charger/battery_fail", &charger->surf_unknown);
        if (ret < 0) {
            LOGE("Cannot load built in battery_fail image\n");
            charger->surf_unknown = NULL;
        }
    }

    GRSurface** scale_frames;
    int scale_count;
    int scale_fps;  
    // 从函数名可知,从png文件中创建多帧动画界面
    // 这个函数会根据png图片头部信息,将一张png图切成多个frame
    // 因为之前UI部门给的图片都是动画切出来的单张图片,所以这个函数很少用到
    ret = res_create_multi_display_surface(anim->animation_file.c_str(), &scale_count, &scale_fps,
                                           &scale_frames);
    // 判断出错信息
    if (ret < 0) {
        LOGE("Cannot load battery_scale image\n");
        anim->num_frames = 0;
        anim->num_cycles = 1;
    } else if (scale_count != anim->num_frames) {
        LOGE("battery_scale image has unexpected frame count (%d, expected %d)\n", scale_count,
             anim->num_frames);
        anim->num_frames = 0;
        anim->num_cycles = 1;
    } else {
        for (i = 0; i < anim->num_frames; i++) {
            anim->frames[i].surface = scale_frames[i];
        }
    }
    ev_sync_key_state(
        std::bind(&set_key_callback, charger, std::placeholders::_1, std::placeholders::_2));

    charger->next_screen_transition = -1;
    charger->next_key_check = -1;
    charger->next_pwr_check = -1;
    healthd_config = config;
    charger->boot_min_cap = config->boot_min_cap;
}

init_animation

  init_animation函数同样在healthd_mode_charger.cpp文件中。

animation* init_animation() {
    bool parse_success;

    std::string content;
    // 读取animation.txt文件信息,animation_desc_path定义如下
    // static constexpr const char* animation_desc_path="/res/values/charger/animation.txt";
    if (base::ReadFileToString(animation_desc_path, &content)) {
        // 解析读取到的animation文件的内容,该函数在下面分析
        // 大家也可以往下拉看怎么分析的,再回来这里往下看
        parse_success = parse_animation_desc(content, &battery_animation);
    } else {
        LOGW("Could not open animation description at %s\n", animation_desc_path);
        parse_success = false;
    }

    if (!parse_success) {
        // 解析失败,使用默认的animation动画
        LOGW("Could not parse animation description. Using default animation.\n");
        battery_animation = BASE_ANIMATION;
        battery_animation.animation_file.assign("charger/battery_scale");
        battery_animation.frames = default_animation_frames;
        battery_animation.num_frames = ARRAY_SIZE(default_animation_frames);
    }
    if (battery_animation.fail_file.empty()) {
        // 未定义电池信息未知动画,就采用默认的电池信息未知动画
        battery_animation.fail_file.assign("charger/battery_fail");
    }

    // 输出解析到的内容
    LOGV("Animation Description:\n");
    LOGV("  animation: %d %d '%s' (%d)\n", battery_animation.num_cycles,
         battery_animation.first_frame_repeats, battery_animation.animation_file.c_str(),
         battery_animation.num_frames);
    LOGV("  fail_file: '%s'\n", battery_animation.fail_file.c_str());
    LOGV("  clock: %d %d %d %d %d %d '%s'\n", battery_animation.text_clock.pos_x,
         battery_animation.text_clock.pos_y, battery_animation.text_clock.color_r,
         battery_animation.text_clock.color_g, battery_animation.text_clock.color_b,
         battery_animation.text_clock.color_a, battery_animation.text_clock.font_file.c_str());
    LOGV("  percent: %d %d %d %d %d %d '%s'\n", battery_animation.text_percent.pos_x,
         battery_animation.text_percent.pos_y, battery_animation.text_percent.color_r,
         battery_animation.text_percent.color_g, battery_animation.text_percent.color_b,
         battery_animation.text_percent.color_a, battery_animation.text_percent.font_file.c_str());

    // 输出每帧动画的信息,显示时间,最低显示电量和最高显示电量
    for (int i = 0; i < battery_animation.num_frames; i++) {
        LOGV("  frame %.2d: %d %d %d\n", i, battery_animation.frames[i].disp_time,
             battery_animation.frames[i].min_level, battery_animation.frames[i].max_level);
    }

    return &battery_animation;
}

parse_animation_desc

  parse_animation_desc函数在AnimationParser.cpp文件中

bool parse_animation_desc(const std::string& content, animation* anim) {
    // 定义animation.txt文件可以解析的字段
    static constexpr const char* animation_prefix = "animation: ";
    static constexpr const char* fail_prefix = "fail: ";
    static constexpr const char* clock_prefix = "clock_display: ";
    static constexpr const char* percent_prefix = "percent_display: ";
    static constexpr const char* frame_prefix = "frame: ";

    // 帧动画vector
    std::vector frames;

    // for循环逐行解析
    // 以 animation: 3 2 charger/boot_charger_02 为例
    for (const auto& line : base::Split(content, "\n")) {
        animation::frame frame;
        const char* rest;

        // can_ignore_line用于跳过空行和'#'开头的行
        if (can_ignore_line(line.c_str())) {
            continue;
        // remove_prefix会根据第二个参数是否符合,上面的例子刚好符合这一项
        // 符合对应xxx_prefix后,会将"animation: "之后的内容,即"3 2 charger/boot_charger_02"
        // 返回给rest,remove_prefix函数下面也会分析
        } else if (remove_prefix(line, animation_prefix, &rest)) {
            int start = 0, end = 0;
            // sscanf将3赋给anim->num_cycles,其余类似
            // 需要说明的是%n用于获取sscanf到%n未知读取的字符数,然后赋给start或者end
            // 这里"%n%*s%n"就是将"charger/boot_charger_02"字符串在line中的
            // 起始结束位置赋给start、end
            if (sscanf(rest, "%d %d %n%*s%n", &anim->num_cycles, &anim->first_frame_repeats,
                    &start, &end) != 2 ||
                end == 0) {
                LOGE("Bad animation format: %s\n", line.c_str());
                return false;
            } else {
                // 如果上面解析成功,就将"charger/boot_charger_02"赋给animation_file
                anim->animation_file.assign(&rest[start], end - start);
            }
        // 下面的内容类似
        } else if (remove_prefix(line, fail_prefix, &rest)) {
            anim->fail_file.assign(rest);
        } else if (remove_prefix(line, clock_prefix, &rest)) {
            if (!parse_text_field(rest, &anim->text_clock)) {
                LOGE("Bad clock_display format: %s\n", line.c_str());
                return false;
            }
        } else if (remove_prefix(line, percent_prefix, &rest)) {
            if (!parse_text_field(rest, &anim->text_percent)) {
                LOGE("Bad percent_display format: %s\n", line.c_str());
                return false;
            }
        } else if (sscanf(line.c_str(), " frame: %d %d %d",
                &frame.disp_time, &frame.min_level, &frame.max_level) == 3) {
            frames.push_back(std::move(frame));
        } else {
            LOGE("Malformed animation description line: %s\n", line.c_str());
            return false;
        }
    }

    if (anim->animation_file.empty() || frames.empty()) {
        LOGE("Bad animation description. Provide the 'animation: ' line and at least one 'frame: ' "
             "line.\n");
        return false;
    }

    anim->num_frames = frames.size();
    anim->frames = new animation::frame[frames.size()];
    std::copy(frames.begin(), frames.end(), anim->frames);

    return true;
}

这里因为公司使用的是将gif动画切出来的一张张独立的图,不像默认动画battery_scale.png图那样,一张图包含所有的充电动画surface信息,所以这里我们可以修改这个函数:

  1. 在animation结构体里面的frame结构体中添加一个string类型的字段frame_file,用来记录对应的frame文件名
  2. 修改frame: 这个prefix的解析内容,添加frame文件的文件名字段解析,例如:frame: 50 16 20 charger/charging_animation_09,将最后的charger/charging_animation_09内容记录到新添加的frame_file字段中
  3. 根据frame_file指定的文件名,通过res_create_display_surface函数来为每一帧动画图创建对应的frame,因为animation的frames指针就是用来记录动画内容的,所以将创建的frame记录到frames中即可。
  4. 需要注意注释掉healthd_mode_charger_init函数中通过charger/scale创建muti frame的代码

remove_prefix

  remove_prefix函数同样在AnimationParser.cpp文件中

bool remove_prefix(const std::string& line, const char* prefix, const char** rest) {
    const char* str = line.c_str();
    int start;
    char c;

    // 经过解析后format为" animation: %n%c"
    std::string format = base::StringPrintf(" %s%%n%%c", prefix);
    // sscanf解析后,start为"animation: 3 2 charger/boot_charger_02"字符串中3的所在位置
    if (sscanf(str, format.c_str(), &start, &c) != 1) {
        return false;
    }

    // rest为"3 2 charger/boot_charger_02"字符串
    *rest = &str[start];
    return true;
}

  到这里,动画的初始化过程就基本讲完了,不过还有一个需要注意的地方就是,在调用res_create_display_surface的时候会调用open_png函数来打开png图片,在这个函数里面会判断png图片是否符合要求

...............

    if (bit_depth == 8 && *channels == 3 && color_type == PNG_COLOR_TYPE_RGB) {
        // 8-bit RGB images: great, nothing to do.
    } else if (bit_depth <= 8 && *channels == 1 && color_type == PNG_COLOR_TYPE_GRAY) {
        // 1-, 2-, 4-, or 8-bit gray images: expand to 8-bit gray.
        png_set_expand_gray_1_2_4_to_8(*png_ptr);
    } else if (bit_depth <= 8 && *channels == 1 && color_type == PNG_COLOR_TYPE_PALETTE) {
        // paletted images: expand to 8-bit RGB.  Note that we DON'T
        // currently expand the tRNS chunk (if any) to an alpha
        // channel, because minui doesn't support alpha channels in
        // general.
        png_set_palette_to_rgb(*png_ptr);
        *channels = 3;
    } else {
        fprintf(stderr, "minui doesn't support PNG depth %d channels %d color_type %d\n",
                bit_depth, *channels, color_type);
        result = -7;
        goto exit;
    }

.................

从上面一段代码可以看出,animation动画图支持3通道的RGB图,4通道的RGBA类型的图是支持不了的,这里的A指的是Alpha通道;另外就是单通道灰阶图也是支持的。下面分析一下动画显示过程

update_screen_state

static void update_screen_state(charger* charger, int64_t now) {
    animation* batt_anim = charger->batt_anim;
    int disp_time;

    if (!batt_anim->run || now < charger->next_screen_transition) return;

    // 如下一段代码主要是为了点亮屏幕,如果无法点亮屏幕就退出
    if (healthd_draw == nullptr) {
        if (healthd_config && healthd_config->screen_on) {
            if (!healthd_config->screen_on(batt_prop)) {
                LOGV("[%" PRId64 "] leave screen off\n", now);
                batt_anim->run = false;
                charger->next_screen_transition = -1;
                if (charger->charger_connected) request_suspend(true);
                return;
            }
        }

        healthd_draw.reset(new HealthdDraw(batt_anim));

#ifndef CHARGER_DISABLE_INIT_BLANK
        healthd_draw->blank_screen(true);
#endif
    }

    // 下面判断是否动画已经结束,如果结束清除中间变量然后返回
    if (batt_anim->num_cycles > 0 && batt_anim->cur_cycle == batt_anim->num_cycles) {
        reset_animation(batt_anim);
        charger->next_screen_transition = -1;
        healthd_draw->blank_screen(true);
        LOGV("[%" PRId64 "] animation done\n", now);
        if (charger->charger_connected) request_suspend(true);
        return;
    }

    // 获取该帧显示的时间
    disp_time = batt_anim->frames[batt_anim->cur_frame].disp_time;

    // 动画开始
    if (batt_anim->cur_frame == 0) {
        LOGV("[%" PRId64 "] animation starting\n", now);
        if (batt_prop) {
            // 从battery service中获取当前点亮和电池状态
            batt_anim->cur_level = batt_prop->batteryLevel;
            batt_anim->cur_status = batt_prop->batteryStatus;
            if (batt_prop->batteryLevel >= 0 && batt_anim->num_frames != 0) {
                // 找到第一张符合对应电量要求的帧
                for (int i = 0; i < batt_anim->num_frames; i++) {
                    if (batt_anim->cur_level >= batt_anim->frames[i].min_level &&
                        batt_anim->cur_level <= batt_anim->frames[i].max_level) {
                        batt_anim->cur_frame = i;
                        break;
                    }
                }

                // 动画第一帧可以设置重复显示次数,所以需要更新第一帧显示的时间
                disp_time = batt_anim->frames[batt_anim->cur_frame].disp_time *
                            batt_anim->first_frame_repeats;
            }
        }
    }

    // 如果动画显示循环次数设置为0就,就直接灭屏
    if (batt_anim->cur_cycle == 0) healthd_draw->blank_screen(false);

    // 调用redraw_screen刷新屏幕
    healthd_draw->redraw_screen(charger->batt_anim, charger->surf_unknown);

    if (batt_anim->num_frames == 0 || batt_anim->cur_level < 0) {
        LOGW("[%" PRId64 "] animation missing or unknown battery status\n", now);
        charger->next_screen_transition = now + BATTERY_UNKNOWN_TIME;
        batt_anim->cur_cycle++;
        return;
    }

    // 计算下一张动画的切换时间
    charger->next_screen_transition = now + disp_time;

    // 如果还插着充电器
    if (charger->charger_connected) {
        // 记录下一帧动画的索引
        batt_anim->cur_frame++;

        while (batt_anim->cur_frame < batt_anim->num_frames &&
               (batt_anim->cur_level < batt_anim->frames[batt_anim->cur_frame].min_level ||
                batt_anim->cur_level > batt_anim->frames[batt_anim->cur_frame].max_level)) {
            batt_anim->cur_frame++;
        }
        if (batt_anim->cur_frame >= batt_anim->num_frames) {
            batt_anim->cur_cycle++;
            batt_anim->cur_frame = 0;

        }
    } else {
        // 拔出了充电器,重置变量
        batt_anim->cur_frame = 0;
        batt_anim->cur_cycle++;
    }
}

  动画显示无非就是判断当前电池的状态,然后选择到对应的frame,再通过healthd_draw显示到屏幕上面。下面我们再简单看看healthd_draw的redraw_screen方法

void HealthdDraw::redraw_screen(const animation* batt_anim, GRSurface* surf_unknown) {
  clear_screen();

  // 首先判断电池状态和动画内容是否有问题
  if (batt_anim->cur_level < 0 || batt_anim->num_frames == 0)
    // 有问题显示battery_fail动画
    draw_unknown(surf_unknown);
  else
    // 否者显示电池充电动画
    draw_battery(batt_anim);
  // 刷新屏幕
  gr_flip();
}


void HealthdDraw::draw_battery(const animation* anim) {
  const animation::frame& frame = anim->frames[anim->cur_frame];

  if (anim->num_frames != 0) {
    // 如函数名,将动画显示到屏幕中间
    draw_surface_centered(frame.surface);
    LOGV("drawing frame #%d min_cap=%d time=%d\n", anim->cur_frame,
         frame.min_level, frame.disp_time);
  }
  // 如果有设置显示时钟和电池百分百,则同时描绘
  draw_clock(anim);
  draw_percent(anim);
}


int HealthdDraw::draw_surface_centered(GRSurface* surface) {
  int w = gr_get_width(surface);
  int h = gr_get_height(surface);
  int x = (screen_width_ - w) / 2 + kSplitOffset;
  int y = (screen_height_ - h) / 2;

  LOGV("drawing surface %dx%d+%d+%d\n", w, h, x, y);
  gr_blit(surface, 0, 0, w, h, x, y);
  if (kSplitScreen) {
    x += screen_width_ - 2 * kSplitOffset;
    LOGV("drawing surface %dx%d+%d+%d\n", w, h, x, y);
    gr_blit(surface, 0, 0, w, h, x, y);
  }

  return y + h;
}

所以,动画的连续显示也显得比较简单。如果想要做修改,理清显示流程即可。

另外,看上面的流程可以知道,如果需要显示电量百分比,可以提供对应的font字体文件,当然更简单的就是提供一到十和百分号的png图,然后按照上面解析动画的流程一样,将百分比对应的图片也加载进来,最后显示即可。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d 博主赞过: