当前位置: 首页 > news >正文

android11关机安卓充电的UI定制化

引言

首先上一张安卓充电的图片:
安卓关机状态下有两种充电模式: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);
    else
        draw_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 on
                   all devices. Check the property and continue booting or reboot
                   accordingly. */
                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 = 0
    max_h = 0
    for c in text:
        w, h = font_bold.getsize(c)
        if w > max_w:
            max_w = w
            
        if h > max_h:
            max_h = h
        
    print max_w, max_h

    image = Image.new(mode='L', size=(max_w*96, max_h*2))
    draw_table = ImageDraw.Draw(im=image)
    i = 0
    for c in text:
        text_width, text_height = font_bold.getsize(c)
        print c , text_width, text_height
        draw_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 + 1

    image.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

相关文章:

  • 练习题:110
  • Mybatis逆向工程
  • 【商城实战(94)】构建高并发的负载均衡与集群架构
  • RedHatLinux(2025.3.22)
  • 解决 macOS (M1 Pro) 上使用 Vite 进行 Build 打包时 Node 进程内存溢出的问题
  • 复现GitHub上`https://github.com/tobiasfshr/map4d`这个项目
  • Android学习总结之ContentProvider跨应用数据共享
  • 无需docker三步安装deepseek可视化操作软件-Open-WebUI
  • RabbitMQ消息相关
  • #C8# UVM中的factory机制 #S8.5# 对factory机制的重载进一步思考(二)
  • Hyperlane:Rust Web开发的未来,释放极致性能与简洁之美
  • 2025-3-29算法打卡
  • epoll 和ractor模型学习
  • Docker 的实质作用是什么
  • Blender多摄像机怎么指定相机渲染图像
  • 《数据结构:单链表》
  • 最常使用的现代C++新特性介绍
  • 复古半色调褶皱照片效果ps特效滤镜样机 Halftone Crumpled Paper Effect
  • 通过本地部署 DeepSeek 来协助感光材料研发(配方设计和有机合成等方面)的一般步骤和思路
  • docker(2) -- 启动后修改目录和网络
  • 北大深圳研究生院成立科学智能学院:培养交叉复合型人才
  • 上海74岁老人宜春旅游时救起落水儿童,“小孩在挣扎容不得多想”
  • 挤占学生伙食费、公务考察到景区旅游……青岛通报5起违规典型问题
  • 餐饮店直播顾客用餐,律师:公共场所并非无隐私,需对方同意
  • 柴德赓、纪庸与叫歇碑
  • 加拿大今日大选:房价、印度移民和特朗普,年轻人在焦虑什么?