引言
首先上一张安卓充电的图片:
安卓关机状态下有两种充电模式:uboot-charge和android-charge,可通过dts配置使用哪一种充电模式。
dts配置中uboot-charge和android-charge是互斥的,如下配置的是开启android-charge:
kernel/arch/arm64/boot/dts/rockchip/rk3566_xxproject.dts
本片主要讲解安卓充电的定制化。
安卓充电流程讲解
InitAnimation解析animation.txt配置文件
实现安卓充电的是一个名为charger的文件,源码位置:system/core/healthd
。
程序启动时首先会通过InitAnimation函数根据animation.txt解析得到图片和字体文件的路径。animation.txt内容如下:
#动画循环次数 首帧显示次数 动画压缩文件名(charge_scale是多张图片合成的一张图片)
animation: 3 1 charge_scale
#fail文件名
fail: fail_scale
#c c r g b a 字体文件名
clock_display: c c 255 255 255 255 font
percent_display: c c 255 255 255 255 font
#电量20以下显示的图片
frame: 500 0 19
#电量40以下显示的图片
frame: 600 0 39
frame: 700 0 59
frame: 750 0 79
frame: 750 0 89
frame: 750 0 100
Charger实现在system/core/healthd/healthd_mode_charger.cpp
static constexpr const char* product_animation_desc_path = "/product/etc/res/values/charger/animation.txt";
static constexpr const char* product_animation_root = "/product/etc/res/images/";void Charger::InitAnimation() {bool parse_success;std::string content;if (base::ReadFileToString(product_animation_desc_path, &content)) {parse_success = parse_animation_desc(content, &batt_anim_);batt_anim_.set_resource_root(product_animation_root);} else if (base::ReadFileToString(animation_desc_path, &content)) {parse_success = parse_animation_desc(content, &batt_anim_);} else {LOGW("Could not open animation description at %s\n", animation_desc_path);parse_success = false;}
//parse_animation_desc实现在system/core/healthd/AnimationParser.cpp
bool parse_animation_desc(const std::string& content, animation* anim) {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: ";std::vector<animation::frame> frames;for (const auto& line : base::Split(content, "\n")) {animation::frame frame;const char* rest;if (can_ignore_line(line.c_str())) {continue;} else if (remove_prefix(line, animation_prefix, &rest)) {int start = 0, end = 0;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 {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;
}
//parse_text_field实现在system/core/healthd/AnimationParser.cpp
bool parse_text_field(const char* in, animation::text_field* field) {int* x = &field->pos_x;int* y = &field->pos_y;int* r = &field->color_r;int* g = &field->color_g;int* b = &field->color_b;int* a = &field->color_a;int start = 0, end = 0;if (sscanf(in, "c c %d %d %d %d %n%*s%n", r, g, b, a, &start, &end) == 4) {*x = CENTER_VAL;*y = CENTER_VAL;} else if (sscanf(in, "c %d %d %d %d %d %n%*s%n", y, r, g, b, a, &start, &end) == 5) {*x = CENTER_VAL;} else if (sscanf(in, "%d c %d %d %d %d %n%*s%n", x, r, g, b, a, &start, &end) == 5) {*y = CENTER_VAL;} else if (sscanf(in, "%d %d %d %d %d %d %n%*s%n", x, y, r, g, b, a, &start, &end) != 6) {return false;}if (end == 0) return false;field->font_file.assign(&in[start], end - start);return true;
}
初始化GRSurface
根据上一步解析得到的图片的路径转化成绘图表面
void Charger::Init(struct healthd_config* config) {...InitAnimation();ret = res_create_display_surface(batt_anim_.fail_file.c_str(), &surf_unknown_);if (ret < 0) {LOGE("Cannot load custom battery_fail image. Reverting to built in: %d\n", ret);ret = res_create_display_surface("charger/battery_fail", &surf_unknown_);if (ret < 0) {LOGE("Cannot load built in battery_fail image\n");surf_unknown_ = NULL;}}GRSurface** scale_frames;int scale_count;int scale_fps; // Not in use (charger/battery_scale doesn't have FPS text// chunk). We are using hard-coded frame.disp_time instead.ret = res_create_multi_display_surface(batt_anim_.animation_file.c_str(), &scale_count,&scale_fps, &scale_frames);if (ret < 0) {LOGE("Cannot load battery_scale image\n");batt_anim_.num_frames = 0;batt_anim_.num_cycles = 1;} else if (scale_count != batt_anim_.num_frames) {LOGE("battery_scale image has unexpected frame count (%d, expected %d)\n", scale_count,batt_anim_.num_frames);batt_anim_.num_frames = 0;batt_anim_.num_cycles = 1;} else {for (i = 0; i < batt_anim_.num_frames; i++) {batt_anim_.frames[i].surface = scale_frames[i];}}
绘制电量图片、电量百分比和时间文字,用的minui框架
void Charger::UpdateScreenState(int64_t now) {...if (healthd_draw_ == nullptr) {...//初始化healthd_draw_healthd_draw_.reset(new HealthdDraw(&batt_anim_));if (android::sysprop::ChargerProperties::disable_init_blank().value_or(false)) {healthd_draw_->blank_screen(true);screen_blanked_ = true;}}//执行具体的绘制healthd_draw_->redraw_screen(&batt_anim_, surf_unknown_);...
}HealthdDraw::HealthdDraw(animation* anim): kSplitScreen(get_split_screen()), kSplitOffset(get_split_offset()) {int ret = gr_init();if (ret < 0) {LOGE("gr_init failed\n");graphics_available = false;return;}graphics_available = true;sys_font = gr_sys_font();if (sys_font == nullptr) {LOGW("No system font, screen fallback text not available\n");} else {gr_font_size(sys_font, &char_width_, &char_height_);}screen_width_ = gr_fb_width() / (kSplitScreen ? 2 : 1);screen_height_ = gr_fb_height();int res;if (!anim->text_clock.font_file.empty() &&(res = gr_init_font(anim->text_clock.font_file.c_str(), &anim->text_clock.font)) < 0) {LOGE("Could not load time font (%d)\n", res);}if (!anim->text_percent.font_file.empty() &&(res = gr_init_font(anim->text_percent.font_file.c_str(), &anim->text_percent.font)) < 0) {LOGE("Could not load percent font (%d)\n", res);}
}void HealthdDraw::redraw_screen(const animation* batt_anim, GRSurface* surf_unknown) {if (!graphics_available) return;clear_screen();/* try to display *something* */if (batt_anim->cur_status == BATTERY_STATUS_UNKNOWN || batt_anim->cur_level < 0 ||batt_anim->num_frames == 0)draw_unknown(surf_unknown);elsedraw_battery(batt_anim);gr_flip();
}void HealthdDraw::draw_battery(const animation* anim) {if (!graphics_available) return;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);
}void HealthdDraw::draw_percent(const animation* anim) {if (!graphics_available) return;int cur_level = anim->cur_level;if (anim->cur_status == BATTERY_STATUS_FULL) {cur_level = 100;}if (cur_level < 0) return;const animation::text_field& field = anim->text_percent;if (field.font == nullptr || field.font->char_width == 0 || field.font->char_height == 0) {return;}std::string str = base::StringPrintf("%d%%", cur_level);int x, y;determine_xy(field, str.size(), &x, &y);LOGV("drawing percent %s %d %d\n", str.c_str(), x, y);gr_color(field.color_r, field.color_g, field.color_b, field.color_a);draw_text(field.font, x, y, str.c_str());
}
HealthdDraw实现在system/core/healthd/healthd_draw.cpp
电量刷新和事件响应
充电状体下会监听power按键、充电器插拔事件和电量更新事件。
监听power按键实现在HandleInputState函数,按下power键会触发重新显示充电动画。
void Charger::HandleInputState(int64_t now) {//监听power按键ProcessKey(KEY_POWER, now);if (next_key_check_ != -1 && now > next_key_check_) next_key_check_ = -1;
}void Charger::ProcessKey(int code, int64_t now) {key_state* key = &keys_[code];if (code == KEY_POWER) {if (key->down) {int64_t reboot_timeout = key->timestamp + POWER_ON_KEY_TIME;if (now >= reboot_timeout) {/* We do not currently support booting from charger mode onall devices. Check the property and continue booting or rebootaccordingly. */if (property_get_bool("ro.enable_boot_charger_mode", false)) {LOGW("[%" PRId64 "] booting from charger mode\n", now);property_set("sys.boot_from_charger_mode", "1");} else {if (batt_anim_.cur_level >= boot_min_cap_) {LOGW("[%" PRId64 "] rebooting\n", now);reboot(RB_AUTOBOOT);} else {LOGV("[%" PRId64"] ignore power-button press, battery level ""less than minimum\n",now);}}} else {/* if the key is pressed but timeout hasn't expired,* make sure we wake up at the right-ish time to check*/SetNextKeyCheck(key, POWER_ON_KEY_TIME);/* Turn on the display and kick animation on power-key press* rather than on key release*/kick_animation(&batt_anim_);request_suspend(false);}} else {/* if the power key got released, force screen state cycle */if (key->pending) {kick_animation(&batt_anim_);request_suspend(false);}}}key->pending = false;
}
如果想要监听更多的按键事件,只需要在HandleInputState函数中新增ProcessKey(KEY_xxx, now)
,然后在ProcessKey实现对应键值的逻辑即可。
充电器插拔回调到HandlePowerSupplyState函数
void Charger::HandlePowerSupplyState(int64_t now) {int timer_shutdown = UNPLUGGED_SHUTDOWN_TIME;if (!have_battery_state_) return;if (!charger_online()) {//断开充电器...} else {//插入充电器...}
}
电量刷新会回调到OnHealthInfoChanged函数
void Charger::OnHealthInfoChanged(const HealthInfo_2_1& health_info) {set_charger_online(health_info);if (!have_battery_state_) {have_battery_state_ = true;next_screen_transition_ = curr_time_ms() - 1;request_suspend(false);reset_animation(&batt_anim_);kick_animation(&batt_anim_);}health_info_ = health_info.legacy.legacy;AdjustWakealarmPeriods(charger_online());
}
在rk3566 android11中动画执行完后,如果电量刷新了不会触发界面的刷新。如要实现电量实时更新到界面,在此方法中新增逻辑即可,下面贴下我实现电量实时刷新的patch
diff --git a/healthd/healthd_mode_charger.cpp b/healthd/healthd_mode_charger.cpp
--- a/healthd/healthd_mode_charger.cpp (revision 6ae575fc403d2504435366ac34ff233e537e78bd)
+++ b/healthd/healthd_mode_charger.cpp (revision 1122ab003e599072fa194f23b593fbd4ad84205e)
@@ -617,6 +617,15 @@reset_animation(&batt_anim_);kick_animation(&batt_anim_);}
+ //huanghp add: refresh screen when batteryLevel changed
+ if (health_info_.batteryLevel != health_info.legacy.legacy.batteryLevel){
+ LOGV("batteryLevel changed : %d\n",health_info.legacy.legacy.batteryLevel);
+ request_suspend(false);
+ reset_animation(&batt_anim_);
+ kick_animation(&batt_anim_);
+ }
+ //huanghp end;health_info_ = health_info.legacy.legacy;AdjustWakealarmPeriods(charger_online());
源码更新了后可以单编charger,ado root && adb remount后替换charger文件重启机器就能看到效果,不需要刷机。对于下面的充电图标和字体也是找到对应目录直接替换后重启就可以看效果。
充电图标替换
修改默认关机充电图标实际上要替换battery_scale.png,charge_scale.png实际是由多张图片合成的一张图片。
对应c源码配置
void Charger::InitDefaultAnimationFrames() {owned_frames_ = {{.disp_time = 750,.min_level = 0,.max_level = 19,.surface = NULL,},{.disp_time = 750,.min_level = 0,.max_level = 39,.surface = NULL,},{.disp_time = 750,.min_level = 0,.max_level = 59,.surface = NULL,},{.disp_time = 750,.min_level = 0,.max_level = 79,.surface = NULL,},{.disp_time = 750,.min_level = 80,.max_level = 95,.surface = NULL,},{.disp_time = 750,.min_level = 0,.max_level = 100,.surface = NULL,},};
}
合成和拆分charge_scale用到的脚本:bootable/recovery/interlace-frames.py
#合成命令
python interlace-frames.py -o battery_scale.png oem/battery00.png oem/battery01.png oem/battery02.png oem/battery03.png oem/battery04.png oem/battery05.png
#拆分命令
python interlace-frames.py -d battery_scale.png -o battery.png
font.png字体文件替换
bootable/recovery/fonts
目录下默认有些不同大小的字体文件,官方的说法是字体都是用font
Inconsolata自动生成的。
The images in this directory were generated using the font
Inconsolata, which is released under the OFL license and was obtained
from:
https://code.google.com/p/googlefontdirectory/source/browse/ofl/inconsolata/
打开链接发现内容不在了,没有找到制作字体的工具。
因此如果要使用更大字号的字体,就需要自己想办法制作字体,这里我从stackoverflow找到个
可以自动生成的python脚本,试了生成的字体可以使用。
'auto generate font png'
from PIL import Image, ImageDraw, ImageFont
import os
def draw_png(name, font_size = 40):font_reg = ImageFont.truetype(name + '-Regular' + '.ttf', font_size)font_bold = ImageFont.truetype(name + '-Bold' + '.ttf', font_size)text=r''' !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~'''text_width, text_height = font_bold.getsize(text)max_w = 0max_h = 0for c in text:w, h = font_bold.getsize(c)if w > max_w:max_w = wif h > max_h:max_h = hprint max_w, max_himage = Image.new(mode='L', size=(max_w*96, max_h*2))draw_table = ImageDraw.Draw(im=image)i = 0for c in text:text_width, text_height = font_bold.getsize(c)print c , text_width, text_heightdraw_table.text(xy=(max_w*i, 0), text=c, fill='#ffffff', font=font_reg, anchor="mm", align="center")draw_table.text(xy=(max_w*i, max_h), text=c, fill='#ffffff', font=font_bold, anchor="mm",align="center")i = i + 1image.show()image.save( name + '.png', 'PNG')image.close()if __name__ == "__main__":print('running:')try:draw_png('Roboto',100)except Exception as e:print( ' ERR: ', e)
字体文件直接在aosp源码目录查找find ./ -name *.ttf |grep Roboto
参考:
- https://blog.csdn.net/lmpt90/article/details/103390395
- https://stackoverflow.com/questions/65180151/how-to-generate-a-font-image-used-in-android-power-off-charging-animation