1. 花了几天时间基于lvgl8.3封装了lvgl_cpp,写了一些基本需要的控件,支持链式调用

还存在一点点bug,不难fix
2. 增加了中文字库,支持中文显示
3. 修复和优化了一些地方
This commit is contained in:
Misaki
2025-09-27 05:43:43 +08:00
parent a47e20cb64
commit 801138631e
18 changed files with 67398 additions and 36 deletions
@@ -9,6 +9,7 @@
#include "LVGL_Driver.h"
#include <esp_log.h>
#include <ToolsClass.h>
LVGLRender* LVGLRender::LVGLRenderInstance = nullptr;
std::mutex LVGLRender::instance_mutex;
@@ -30,12 +31,14 @@ LVGLRender* LVGLRender::getInstance() {
void LVGLRender::LVGL_Update() {
while (true) {
vTaskDelay(LVGL_DELAY_FROM_FPS(LVGLRender::fps));
lv_timer_handler();
{
const std::lock_guard<std::mutex> lock(mtx); // lvgl并非线程安全,需要加锁
lv_timer_handler();
}
std::this_thread::sleep_for(std::chrono::milliseconds(fps));
}
}
// 构造函数
LVGLRender::LVGLRender() {
ESP_LOGI("LVGL_Render", "LVGL_Render构造函数...初始化媒体驱动...");
@@ -63,14 +66,24 @@ LVGLRender::~LVGLRender() {
}
void LVGLRender::log() {
ESP_LOGI("LVGL_Render", "LVGL_Render log...");
void LVGLRender::setFps(uint16_t fps_) {
fps = fps_;
}
// 静态:拼出完整路径
std::string LVGLRender::makeFullPath(const std::string& filename)
{
return "/sdcard/" + filename;
uint16_t LVGLRender::getFps() {
return fps;
}
void LVGLRender::tryToInitRenderGif() {
ESP_LOGI("LVGL_Render", "尝试初始化 LVGL 底层驱动...");
}
void LVGLRender::log() {
ESP_LOGI("LVGL_Render", "LVGL_Render log...");
}
#include "SDFileManager.h"
@@ -118,15 +131,15 @@ void LVGLRender::renderGifInternal(const std::vector<uint8_t>& data,
gif_desc.header.cf = LV_IMG_CF_RAW_CHROMA_KEYED;
gif_desc.header.always_zero = 0;
gif_desc.header.reserved = 0;
gif_desc.header.w = (lv_coord_t)w;
gif_desc.header.h = (lv_coord_t)h;
gif_desc.header.w = static_cast<lv_coord_t>(w);
gif_desc.header.h = static_cast<lv_coord_t>(h);
gif_desc.data_size = current_gif_data.size();
gif_desc.data = current_gif_data.data();
// 创建新的 GIF 对象
current_gif_obj = lv_gif_create(lv_scr_act());
lv_gif_set_src(current_gif_obj, &gif_desc);
lv_obj_center(current_gif_obj);
current_gif_obj = lv_gif_create(lv_scr_act()); // copy到当前gif对象
lv_gif_set_src(current_gif_obj, &gif_desc); // 设置源
lv_obj_center(current_gif_obj); // 居中
ESP_LOGI("LVGLRender", "GIF 已渲染并循环播放");
}
@@ -141,7 +154,7 @@ void LVGLRender::RenderGif(const std::string &filename) {
lv_obj_set_style_bg_color(lv_scr_act(), lv_color_black(), 0); // 背景黑色
lv_obj_set_style_bg_opa(lv_scr_act(), LV_OPA_COVER, 0); // 透明度
std::string fullPath = makeFullPath(filename);
std::string fullPath = ToolsClass::makeFullPath(filename);
std::vector<uint8_t> gifBin = readWholeFile(fullPath);
if (gifBin.empty()) return;
@@ -19,8 +19,6 @@ class LVGLRender {
public:
LVGLRender(LVGLRender const&) = delete; // 禁止拷贝构造函数
LVGLRender& operator=(LVGLRender const&) = delete;// 禁止赋值运算符
public:
static std::string makeFullPath(const std::string& filename);
public:
static LVGLRender* getInstance();
@@ -32,6 +30,12 @@ public:
*/
void RenderGif(const std::string &filename);
static void setFps(uint16_t fps_);
static uint16_t getFps();
void tryToInitRenderGif();
void log();
// gif渲染
@@ -59,6 +63,8 @@ private:
lv_obj_t* current_gif_obj = nullptr; /// 当前GIF对象
std::vector<uint8_t> current_gif_data; /// 当前GIF数据
std::string last_gif_filename; /// 最后一次渲染的GIF文件名
std::mutex mtx;
};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,5 @@
//
// Created by misaki on 2025/9/26.
//
#include "lvpp.h"
@@ -0,0 +1,849 @@
//
// Created by misaki on 2025/9/26.
//
#pragma once
#include <esp_heap_caps.h>
#include <esp_log.h>
#include <lvgl.h>
#include <font/lv_font.h>
#include <functional>
#include <optional>
#include <utility>
#include "SDFileManager.h"
#include "ToolsClass.h"
LV_FONT_DECLARE(SiYuanHeiTiGoogleBan); // 思源黑体 Google版
namespace lvgl_cpp {
// 前向声明
class Obj;
class Screen;
class Label;
class Button;
class Arc;
class Slider;
class Switch;
class Roller;
class Dropdown;
class List;
class TextArea;
class Image;
class IconButton;
// 基础封装
class Obj {
public:
Obj() = default;
explicit Obj(lv_obj_t* raw) : ptr_(raw) {}
virtual ~Obj() { if (ptr_) lv_obj_del(ptr_); }
Obj(Obj&& o) noexcept : ptr_(std::exchange(o.ptr_, nullptr)) {}
Obj& operator=(Obj&& o) noexcept {
if (this != &o) { reset(); ptr_ = std::exchange(o.ptr_, nullptr); }
return *this;
}
Obj(const Obj&) = delete;
Obj& operator=(const Obj&) = delete;
[[nodiscard]] lv_obj_t* raw() const { return ptr_; }
void reset() { if (ptr_) { lv_obj_del(ptr_); ptr_ = nullptr; } }
// 通用链式
Obj& size(const lv_coord_t w, const lv_coord_t h) { lv_obj_set_size(ptr_, w, h); return *this; }
Obj& pos(const lv_coord_t x, const lv_coord_t y) { lv_obj_set_pos(ptr_, x, y); return *this; }
Obj& center() { lv_obj_center(ptr_); return *this; }
Obj& align(const lv_align_t align, const lv_obj_t* base = nullptr, const lv_coord_t x = 0, const lv_coord_t y = 0) {
if (base) { // 如果有 base
lv_obj_align_to(ptr_, base, align, x, y);
}else {
lv_obj_align(ptr_, align, x, y);
}
return *this;
}
Obj& add_flag(const lv_obj_flag_t f) { lv_obj_add_flag(ptr_, f); return *this; }
Obj& clear_flag(const lv_obj_flag_t f) { lv_obj_clear_flag(ptr_, f); return *this; }
Obj& add_style(lv_style_t* style, const lv_style_selector_t sel = LV_PART_MAIN) {
lv_obj_add_style(ptr_, style, sel); return *this;
}
// 事件转发
using EventCb = std::function<void(lv_event_t*)>;
Obj& on(const lv_event_code_t code, EventCb cb) {
auto* ctx = new EventCb(std::move(cb));
lv_obj_add_event_cb(ptr_, event_trampoline, code, ctx);
return *this;
}
protected: // 改为 protected 以允许派生类访问
lv_obj_t* ptr_ = nullptr;
private:
static void event_trampoline(lv_event_t* e) {
const auto* ctx = static_cast<EventCb*>(lv_event_get_user_data(e));
(*ctx)(e);
}
};
// 样式构造器
struct Style {
lv_style_t s{};
Style() { lv_style_init(&s); }
~Style() { lv_style_reset(&s); }
Style& bg(lv_color_t c, lv_opa_t o = LV_OPA_COVER) {
lv_style_set_bg_color(&s, c);
lv_style_set_bg_opa(&s, o); return *this;
}
Style& radius(const lv_coord_t r) { lv_style_set_radius(&s, r); return *this; }
Style& pad_all(const lv_coord_t v) { lv_style_set_pad_all(&s, v); return *this; }
Style& border(const lv_color_t c, const lv_coord_t w = 1) {
lv_style_set_border_color(&s, c);
lv_style_set_border_width(&s, w); return *this;
}
Style& text_color(const lv_color_t c) { lv_style_set_text_color(&s, c); return *this; }
Style& shadow(const lv_coord_t w, const lv_color_t c) {
lv_style_set_shadow_width(&s, w);
lv_style_set_shadow_color(&s, c); return *this;
}
explicit operator lv_style_t*() { return &s; }
};
// 动画助手
template<typename T>
struct Anim {
lv_anim_t a{};
Anim(T* var, const int32_t start, const int32_t end, const uint32_t time = 300, const lv_anim_path_cb_t path = lv_anim_path_ease_out) {
lv_anim_init(&a);
lv_anim_set_var(&a, var);
lv_anim_set_values(&a, start, end);
lv_anim_set_time(&a, time);
lv_anim_set_path_cb(&a, path);
}
Anim& exec_cb(const lv_anim_exec_xcb_t cb) { lv_anim_set_exec_cb(&a, cb); return *this; }
Anim& ready_cb(const lv_anim_ready_cb_t cb) { lv_anim_set_ready_cb(&a, cb); return *this; }
void start() const { lv_anim_start(&a); }
};
// 具体控件
class Screen : public Obj {
public:
Screen() : Obj(lv_obj_create(nullptr)) {}
};
class Label : public Obj {
public:
explicit Label(const Obj& parent) : Obj(lv_label_create(parent.raw())) {}
Label& text(const char* txt) { lv_label_set_text(raw(), txt); return *this; }
};
class Button : public Obj {
public:
explicit Button(const Obj& parent) : Obj(lv_btn_create(parent.raw())) {}
};
class Arc : public Obj {
public:
explicit Arc(const Obj& parent) : Obj(lv_arc_create(parent.raw())) {}
Arc& value(const int16_t v) { lv_arc_set_value(raw(), v); return *this; }
Arc& range(const int16_t min, const int16_t max) { lv_arc_set_range(raw(), min, max); return *this; }
};
class Slider : public Obj {
public:
explicit Slider(const Obj& parent) : Obj(lv_slider_create(parent.raw())) {}
Slider& value(const int32_t v) { lv_slider_set_value(raw(), v, LV_ANIM_OFF); return *this; }
Slider& range(const int32_t min, const int32_t max) { lv_slider_set_range(raw(), min, max); return *this; }
};
class Switch : public Obj {
public:
explicit Switch(const Obj& parent) : Obj(lv_switch_create(parent.raw())) {}
};
class Roller : public Obj {
public:
explicit Roller(const Obj& parent) : Obj(lv_roller_create(parent.raw())) {}
Roller& options(const char* opts, const lv_roller_mode_t m = LV_ROLLER_MODE_NORMAL) {
lv_roller_set_options(raw(), opts, m); return *this;
}
};
class Dropdown : public Obj {
public:
explicit Dropdown(const Obj& parent) : Obj(lv_dropdown_create(parent.raw())) {}
Dropdown& options(const char* opts) { lv_dropdown_set_options(raw(), opts); return *this; }
};
class List : public Obj {
public:
explicit List(const Obj& parent) : Obj(lv_list_create(parent.raw())) {}
lv_obj_t* add_btn(const void* img, const char* txt) const { return lv_list_add_btn(raw(), img, txt); }
};
class TextArea : public Obj {
public:
explicit TextArea(const Obj& parent) : Obj(lv_textarea_create(parent.raw())) {}
TextArea& text(const char* txt) { lv_textarea_set_text(raw(), txt); return *this; }
TextArea& placeholder(const char* txt) { lv_textarea_set_placeholder_text(raw(), txt); return *this; }
};
class Image : public Obj {
public:
explicit Image(const Obj& parent) : Obj(lv_img_create(parent.raw())) {}
// 读 bin → PSRAM → 一次性 dsc → 显示
Image& bin(const char* fileName, const uint32_t w, const uint32_t h)
{
// 读文件
const std::string data = SDFileManager::getInstance()->readFileSync(ToolsClass::makeFullPath(fileName));
if (data.empty()) {
ESP_LOGE("Image", "bin read fail: %s", fileName);
return *this;
}
size_t sz = data.size();
if (sz != w * h * 2) { // RGB565 检查
ESP_LOGW("Image", "size mismatch, expect %lu, got %zu", w * h * 2, sz);
}
// 拷到 PSRAMLVGL 长期持有)
void* buf = heap_caps_malloc(sz, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (!buf) {
ESP_LOGE("Image", "PSRAM malloc failed");
return *this;
}
memcpy(buf, data.data(), sz);
// 构造静态描述符(生命周期跟随 Image 对象)
dsc_ = std::unique_ptr<lv_img_dsc_t, DscDeleter>(new lv_img_dsc_t{});
dsc_->data = static_cast<const uint8_t*>(buf);
dsc_->header.cf = LV_IMG_CF_TRUE_COLOR;
dsc_->header.w = w;
dsc_->header.h = h;
dsc_->data_size = sz;
// 交给 LVGL
lv_img_set_src(raw(), dsc_.get());
return *this;
}
private:
// 用 unique_ptr 托管,Image 析构时自动释放 PSRAM 与 dsc
struct DscDeleter {
void operator()(const lv_img_dsc_t* p) const {
if (p && p->data) heap_caps_free(const_cast<uint8_t*>(p->data));
delete p;
}
};
std::unique_ptr<lv_img_dsc_t, DscDeleter> dsc_;
};
class IconButton : public Button {
public:
explicit IconButton(const Obj& parent) : Button(parent) {}
IconButton& icon(const char* fileName, const uint32_t w, const uint32_t h)
{
// 图标作为独立子 Image 创建
img_ = std::make_unique<Image>(*this);
img_->bin(fileName, w, h);
lv_obj_center(img_->raw());
return *this;
}
private:
std::unique_ptr<Image> img_; // 跟随 IconButton 生命周期
};
// 消息框 MsgBox
class MsgBox : public Obj {
public:
// 静态创建:模态、居中、自动大小
static MsgBox create(const char* title,
const char* txt,
const char* btns[] = nullptr) // nullptr -> 默认 "OK"
{
lv_obj_t* mbox = lv_msgbox_create(nullptr, title, txt, btns, true);
lv_obj_center(mbox);
return MsgBox{mbox};
}
// 关闭并删除自身
void close() { lv_msgbox_close(raw()); ptr_ = nullptr; }
private:
explicit MsgBox(lv_obj_t* p) : Obj(p) {}
};
// 进度条 Bar
class Bar : public Obj {
public:
explicit Bar(const Obj& parent) : Obj(lv_bar_create(parent.raw())) {}
Bar& range(const int32_t min, const int32_t max) { lv_bar_set_range(raw(), min, max); return *this; }
Bar& value(const int32_t v, const lv_anim_enable_t anim = LV_ANIM_OFF) {
lv_bar_set_value(raw(), v, anim); return *this;
}
// 双向模式
Bar& start_value(const int32_t v, const lv_anim_enable_t anim = LV_ANIM_OFF) {
lv_bar_set_start_value(raw(), v, anim); return *this;
}
Bar& anim_time(const uint32_t ms) { lv_obj_set_style_anim_time(raw(), ms, LV_PART_MAIN); return *this; }
};
// 折线 Line
class Line : public Obj {
public:
explicit Line(const Obj& parent) : Obj(lv_line_create(parent.raw())) {}
/* 直接传 std::vector<lv_point_t> 更安全 */
Line& points(const std::vector<lv_point_t>& v) {
lv_line_set_points(raw(), v.data(), v.size()); return *this;
}
Line& y_invert(const bool en) { lv_line_set_y_invert(raw(), en); return *this; }
};
// 颜色选择器 ColorPicker
class ColorPicker : public Obj {
public:
explicit ColorPicker(const Obj& parent) : Obj(lv_colorwheel_create(parent.raw(), true)) {}
// 获取/设置 HSV
[[nodiscard]] lv_color_t color() const { return lv_colorwheel_get_rgb(raw()); }
ColorPicker& color(const lv_color_t c) { lv_colorwheel_set_rgb(raw(), c); return *this; }
// 事件回调:LV_EVENT_VALUE_CHANGED 即可拿到新颜色
};
// Timer 封装(C++ lambda 版)
class Timer {
public:
using Cb = std::function<void()>;
// 构造即创建,默认循环模式
explicit Timer(Cb cb, const uint32_t period_ms, const bool once = false)
: cb_(new Cb(std::move(cb)))
{
t_ = lv_timer_create(timer_trampoline, period_ms, cb_.get());
if (once) lv_timer_set_repeat_count(t_, 1);
}
~Timer() { // RAII:自动反注册、释放
if (t_) lv_timer_del(t_);
}
// 手动停止/重启
void pause() const { lv_timer_pause(t_); }
void resume() const { lv_timer_resume(t_); }
void reset() const { lv_timer_reset(t_); }
// 修改周期
Timer& period(const uint32_t ms) { lv_timer_set_period(t_, ms); return *this; }
private:
lv_timer_t* t_ = nullptr;
std::unique_ptr<Cb> cb_; // 托管 lambda 生命周期
static void timer_trampoline(lv_timer_t* tm) {
const auto* f = static_cast<Cb*>(tm->user_data);
(*f)();
}
};
// Toast 消息泡泡
class Toast {
public:
enum class Type : uint8_t { INFO, WARN, ERROR };
// 一键弹出,默认底部居中 2 s
static void show(const char* txt,
const Type tp = Type::INFO,
const uint32_t duration_ms = 2000,
const lv_align_t align = LV_ALIGN_BOTTOM_MID,
const lv_coord_t offset_y = -60)
{
// 确保在 LVGL 线程里执行
if (!lv_is_initialized()) return;
lv_async_call(
[](void* p) {
auto* ctx = static_cast<Context*>(p);
create_internal(ctx);
delete ctx;
},
new Context{txt, tp, duration_ms, align, offset_y});
}
private:
struct Context {
const char* txt;
Type tp;
uint32_t dur;
lv_align_t align;
lv_coord_t off_y;
};
static void create_internal(const Context* ctx)
{
// 根容器(透明全屏)
lv_obj_t* parent = lv_layer_top(); // 顶层 overlay
lv_obj_t* toast = lv_obj_create(parent);
lv_obj_set_size(toast, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_set_scrollbar_mode(toast, LV_SCROLLBAR_MODE_OFF);
lv_obj_clear_flag(toast, LV_OBJ_FLAG_SCROLLABLE);
// 去掉边框 / 背景 / 阴影
lv_obj_set_style_border_width(toast, 0, 0);
lv_obj_set_style_bg_opa(toast, LV_OPA_TRANSP, 0);
lv_obj_set_style_shadow_width(toast, 0, 0);
// 消息标签
lv_obj_t* label = lv_label_create(toast);
lv_label_set_text(label, ctx->txt);
lv_obj_set_style_pad_hor(label, 12, 0);
lv_obj_set_style_pad_ver(label, 8, 0);
lv_obj_set_style_radius(label, 8, 0);
lv_obj_set_style_text_color(label, lv_color_white(), 0);
lv_obj_set_style_text_font(label, &SiYuanHeiTiGoogleBan, LV_PART_MAIN);
// 根据类型配色
switch (ctx->tp) {
case Type::INFO:
lv_obj_set_style_bg_color(label, lv_color_hex(0x2196F3), 0);
break;
case Type::WARN:
lv_obj_set_style_bg_color(label, lv_color_hex(0xFF9800), 0);
break;
case Type::ERROR:
lv_obj_set_style_bg_color(label, lv_color_hex(0xF44336), 0);
break;
}
lv_obj_set_style_bg_opa(label, LV_OPA_COVER, 0);
// 定位
lv_obj_align(toast, ctx->align, 0, ctx->off_y);
// 进场动画:向上淡入
lv_obj_set_style_translate_y(toast, 30, 0);
lv_obj_set_style_opa(toast, LV_OPA_0, 0);
lv_anim_t a;
lv_anim_init(&a);
lv_anim_set_var(&a, toast);
lv_anim_set_time(&a, 250);
lv_anim_set_values(&a, LV_OPA_0, LV_OPA_COVER);
lv_anim_set_exec_cb(&a, [](void* obj, int32_t v) {
lv_obj_set_style_opa(static_cast<lv_obj_t *>(obj), v, 0);
lv_obj_set_style_translate_y(static_cast<lv_obj_t *>(obj), 30 - (v * 30) / 255, 0);
});
lv_anim_start(&a);
// 定时器:到达时间后淡出并删除
lv_timer_create(
[](lv_timer_t* t) {
auto* toast = static_cast<lv_obj_t *>(t->user_data);
/* 淡出 */
lv_anim_t a2;
lv_anim_init(&a2);
lv_anim_set_var(&a2, toast);
lv_anim_set_time(&a2, 200);
lv_anim_set_values(&a2, LV_OPA_COVER, LV_OPA_0);
lv_anim_set_exec_cb(&a2, [](void* obj, int32_t v) {
lv_obj_set_style_opa(static_cast<lv_obj_t *>(obj), v, 0);
});
lv_anim_set_ready_cb(&a2, [](lv_anim_t* a) {
lv_obj_del(static_cast<lv_obj_t *>(a->var));
});
lv_anim_start(&a2);
lv_timer_del(t); // 自毁
},
ctx->dur, toast);
}
};
// PhoneStyle-Battery
class Battery : public Obj {
public:
explicit Battery(const Obj& parent) : Obj(lv_obj_create(parent.raw())) {
// 主容器:圆角 + 边框 + 阴影
lv_obj_set_style_radius(raw(), 6, 0);
lv_obj_set_style_bg_color(raw(), lv_color_hex(0x9E9E9E), 0);
lv_obj_set_style_bg_opa(raw(), LV_OPA_COVER, 0);
lv_obj_set_style_border_width(raw(), 2, 0);
lv_obj_set_style_border_color(raw(), lv_color_hex(0x616161), 0);
lv_obj_set_style_shadow_width(raw(), 8, 0);
lv_obj_set_style_shadow_ofs_y(raw(), 4, 0);
lv_obj_set_style_shadow_color(raw(), lv_color_hex(0x9E9E9E), 0);
lv_obj_set_style_pad_all(raw(), 2, 0); // 内容区留白 2 px
lv_obj_set_size(raw(), LV_SIZE_CONTENT, LV_SIZE_CONTENT);
// 电量柱(动态宽/高)
fill_ = lv_obj_create(raw());
lv_obj_set_style_radius(fill_, 5, 0);
lv_obj_set_style_bg_opa(fill_, LV_OPA_COVER, 0);
lv_obj_set_size(fill_, 0, LV_PCT(100)); // 初始 0 %
applyGradient(); // 渐变刷一次
// 百分比文字(默认显示)
label_ = lv_label_create(raw());
lv_label_set_text(label_, "0%");
lv_obj_set_style_text_color(label_, lv_color_white(), 0);
lv_obj_set_style_text_font(label_, &lv_font_montserrat_14, 0);
lv_obj_center(label_);
}
// 设置整体尺寸(w×h)→ 自动计算内容区
Battery& size(const lv_coord_t w, const lv_coord_t h) {
lv_obj_set_size(raw(), w, h);
return *this;
}
// 显示/隐藏百分比文字
Battery& percent(const bool show) {
if (show)
lv_obj_clear_flag(label_, LV_OBJ_FLAG_HIDDEN);
else
lv_obj_add_flag(label_, LV_OBJ_FLAG_HIDDEN);
return *this;
}
// 注册电量回调 0~100,并启动 5 s 定时器
Battery& onRead(std::function<uint8_t()> provider) {
provider_ = std::move(provider);
update(); // 立即一次
if (!timer_) {
timer_ = lv_timer_create([](lv_timer_t* t){
auto* self = static_cast<Battery *>(t->user_data);
self->update();
}, 5000, this);
}
return *this;
}
// 手动刷新一次(公开)
Battery& update() {
if (!provider_) return *this;
pct_ = provider_();
pct_ = pct_ > 100 ? 100 : pct_;
// 更新电量条长度(保留 2 px 内边距)
const lv_coord_t content = lv_obj_get_content_width(raw()); // 去掉边框+pad
const auto fill_w = static_cast<lv_coord_t>((static_cast<int>(content) * pct_) / 100);
lv_obj_set_width(fill_, fill_w);
// 变色 + 渐变重刷
if (pct_ > 60) mainColor_ = lv_color_hex(0x4CAF50); // 绿
else if (pct_ > 20) mainColor_ = lv_color_hex(0xFFEB3B); // 黄
else mainColor_ = lv_color_hex(0xF44336); // 红
applyGradient();
// 文字
lv_label_set_text_fmt(label_, "%" LV_PRId32 "%%", static_cast<int32_t>(pct_));
return *this;
}
// 链式对齐
Battery& align(const lv_align_t align_type, const lv_coord_t x_ofs = 0, const lv_coord_t y_ofs = 0) {
lv_obj_align(raw(), align_type, x_ofs, y_ofs);
return *this;
}
private:
void applyGradient() const {
// 构造一次性渐变描述符(浅→主→深)
static lv_grad_dsc_t grad;
grad.dir = LV_GRAD_DIR_HOR;
grad.stops_count = 3;
grad.stops[0].color = lv_color_lighten(mainColor_, LV_OPA_30);
grad.stops[0].frac = 0;
grad.stops[1].color = mainColor_;
grad.stops[1].frac = 128;
grad.stops[2].color = lv_color_darken(mainColor_, LV_OPA_30);
grad.stops[2].frac = 255;
lv_obj_set_style_bg_grad(fill_, &grad, 0);
}
uint8_t pct_ = 50;
lv_color_t mainColor_ = lv_color_hex(0x4CAF50);
lv_obj_t* fill_;
lv_obj_t* label_;
lv_timer_t* timer_ = nullptr;
std::function<uint8_t()> provider_;
};
// DateTime 日期时间
class DateTime : public Obj {
public:
explicit DateTime(const Obj& parent) : Obj(lv_label_create(parent.raw())) {
lv_label_set_text(raw(), "--/--/-- --:--:--");
}
// 设置格式串,LVGL 语法,如 " %m/%d/%y %H:%M:%S"
DateTime& format(const char* fmt) {
fmt_ = fmt; refresh(); return *this;
}
// 注册数据源回调:外部把格式化好的字符串写进 buf
DateTime& onRead(std::function<void(char* buf, size_t)> cb) {
readCb_ = std::move(cb);
refresh();
// 1 s 定时刷新
timer_ = lv_timer_create([](lv_timer_t* t){
auto* self = static_cast<DateTime *>(t->user_data);
self->refresh();
}, 1000, this);
return *this;
}
// 立即刷新一次
DateTime& refresh() {
if (!readCb_) return *this;
readCb_(buf_, sizeof(buf_));
lv_label_set_text(raw(), buf_);
return *this;
}
private:
lv_timer_t* timer_ = nullptr;
std::function<void(char*, size_t)> readCb_;
std::string fmt_ = "%Y/%m/%d %H:%M:%S";
char buf_[64] = {0};
};
// GIF 显示控件
class Gif final : public Obj {
public:
explicit Gif(const Obj& parent) : Obj(lv_gif_create(parent.raw())) {}
bool getGifWH(const uint8_t* raw, uint32_t& w, uint32_t& h)
{
if (!raw || memcmp(raw, "GIF", 3) != 0) {
ESP_LOGE("LVGLRender", "不是合法 GIF 文件头");
return false;
}
// GIF87a/89a 宽高偏移 6~9 字节,小端
w = raw[6] | (raw[7] << 8);
h = raw[8] | (raw[9] << 8);
ESP_LOGI("LVGLRender", "GIF 尺寸: %lu x %lu", w, h);
return true;
}
// 从 SD 卡加载 GIF 文件(完整路径)
Gif& src(const char* full_path) {
// 读整个文件到 PSRAM
std::string content = SDFileManager::getInstance()->readFileSync(ToolsClass::makeFullPath(full_path));
if (content.empty()) {
ESP_LOGE("Gif", "read %s fail", full_path);
return *this;
}
// 解析宽高
uint32_t w = 0, h = 0;
if (!getGifWH(reinterpret_cast<const uint8_t*>(content.data()), w, h)) {
ESP_LOGE("Gif", "bad GIF header");
return *this;
}
// 把数据拷到 PSRAMLVGL 长期持有)
void* buf = heap_caps_malloc(content.size(), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (!buf) {
ESP_LOGE("Gif", "PSRAM malloc fail");
return *this;
}
memcpy(buf, content.data(), content.size());
// 构造一次性描述符(跟随 Gif 对象生命周期)
dsc_ = std::unique_ptr<lv_img_dsc_t, DscDeleter>(new lv_img_dsc_t{});
dsc_->header.cf = LV_IMG_CF_RAW_CHROMA_KEYED;
dsc_->header.always_zero = 0;
dsc_->header.reserved = 0;
dsc_->header.w = static_cast<lv_coord_t>(w);
dsc_->header.h = static_cast<lv_coord_t>(h);
dsc_->data_size = content.size();
dsc_->data = static_cast<const uint8_t*>(buf);
// 交给 LVGL
lv_gif_set_src(raw(), dsc_.get());
return *this;
}
// 控制播放/暂停
Gif& play() { lv_gif_restart(raw()); return *this; }
Gif& pause() { /*lv_gif_pause(raw());*/ return *this; }
private:
// 描述符 + PSRAM 一并释放
struct DscDeleter {
void operator()(lv_img_dsc_t* p) const {
if (p && p->data) heap_caps_free(const_cast<uint8_t*>(p->data));
delete p;
}
};
std::unique_ptr<lv_img_dsc_t, DscDeleter> dsc_;
};
// 子应用抽象
class BaseApp : public Obj {
public:
explicit BaseApp(const Obj& parent) : Obj(lv_obj_create(parent.raw())), btn_exit_(parent) {
lv_obj_set_size(raw(), 360, 360);
// 退出按钮
btn_exit_.size(40, 40)
.align(LV_ALIGN_CENTER, nullptr, 0, -130) // 居中
.on(LV_EVENT_CLICKED, [this](lv_event_t *) { this->exit(); });
label_.align(LV_ALIGN_CENTER, nullptr, 0, 0);
label_.text(LV_SYMBOL_CLOSE);
}
// 子类重写:界面内容
virtual void onShow() {} // 进入时调用
virtual void onHide() {} // 退出时调用
// 主动退出
void exit() {
onHide();
// 禁止自己再被点击
lv_obj_clear_flag(raw(), LV_OBJ_FLAG_CLICKABLE);
// 异步自毁:让 LVGL 在下一轮循环再删我们
lv_async_call([](void* self) {
lv_obj_del(static_cast<lv_obj_t*>(self));
}, raw());
// 通知工厂回到主页 → 由工厂/菜单负责
if (exit_cb_) exit_cb_();
}
// 注册退出通知回调(由 AppFactory 设置)
void onExit(std::function<void()> cb) { exit_cb_ = std::move(cb); }
protected:
Button btn_exit_{*this}; // 退出按钮
Label label_{btn_exit_}; // 退出图标
private:
std::function<void()> exit_cb_;
};
// 子应用工厂
class AppFactory {
public:
using Creator = std::function<std::unique_ptr<BaseApp>(Obj& parent)>;
// 注册新应用:名字 + lambda 创建器
static void registerApp(const char* name, Creator&& creator) {
registry()[name] = std::move(creator);
}
// 创建实例:parent → 传入当前屏幕
static std::unique_ptr<BaseApp> create(const char* name, Obj& parent) {
auto it = registry().find(name);
if (it == registry().end()) return nullptr;
return it->second(parent);
}
private:
static std::unordered_map<std::string, Creator>& registry() {
static std::unordered_map<std::string, Creator> inst;
return inst;
}
};
// 应用菜单(网格)
class AppMenu : public Obj {
public:
explicit AppMenu(const Obj& parent) : Obj(lv_obj_create(parent.raw())) {
lv_obj_set_size(raw(), 360, 360);
lv_obj_set_style_bg_opa(raw(), LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(raw(), 0, 0);
lv_obj_clear_flag(raw(), LV_OBJ_FLAG_SCROLLABLE); // 圆形屏建议不要滚动
lv_obj_set_style_pad_top(raw(), 60, 0); // ← 新增
lv_obj_set_style_pad_bottom(raw(),60, 0); // ← 新增
lv_obj_set_style_pad_left(raw(), 20, 0);
lv_obj_set_style_pad_right(raw(), 20, 0);
// 横向 2 列,居中排列
lv_obj_set_flex_flow(raw(), LV_FLEX_FLOW_ROW_WRAP);
lv_obj_set_flex_align(raw(), LV_FLEX_ALIGN_SPACE_EVENLY,
LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_START);
}
// 添加一个应用图标:图标 bin + 名字
AppMenu& addItem(const char* name, // 应用名
lv_coord_t w = 140, // 按钮宽
lv_coord_t h = 50) // 按钮高
{
// 每项:一个文字按钮
auto* btn = lv_btn_create(raw());
lv_obj_set_size(btn, w, h);
lv_obj_set_style_radius(btn, 8, 0);
lv_obj_t* label = lv_label_create(btn);
lv_label_set_text(label, name);
lv_obj_set_style_text_font(label, &SiYuanHeiTiGoogleBan, LV_PART_MAIN);
lv_obj_center(label);
// 点击 -> 回调
lv_obj_add_event_cb(btn, [](lv_event_t* e){
const auto* self = static_cast<AppMenu *>(lv_event_get_user_data(e));
if (self && self->click_cb_) {
const char* txt = lv_label_get_text(
lv_obj_get_child(lv_event_get_target(e), 0));
self->click_cb_(txt);
}
}, LV_EVENT_CLICKED, this);
return *this;
}
// 注册点击回调:返回应用名
AppMenu& onClick(std::function<void(const char*)> cb) {
click_cb_ = std::move(cb);
return *this;
}
private:
std::function<void(const char*)> click_cb_;
};
// 主页
class HomePage : public Obj {
public:
explicit HomePage(const Obj& parent) : Obj(lv_obj_create(parent.raw())) {
lv_obj_set_size(raw(), 360, 360);
lv_obj_set_style_radius(raw(), LV_RADIUS_CIRCLE, 0); // 圆形裁切
lv_obj_set_style_clip_corner(raw(), true, 0);
lv_obj_set_style_border_width(raw(), 0, 0);
// 彻底禁止滚动
lv_obj_clear_flag(raw(), LV_OBJ_FLAG_SCROLLABLE); // 禁止滚动
// lv_obj_set_scroll_dir(raw(), LV_DIR_NONE);
// 背景图片(默认空,可热切换)
bg_img_ = std::make_unique<Image>(*this);
// 顶栏电池
battery_ = std::make_unique<Battery>(*this);
battery_->size(60, 30)
.percent(true)
.align(LV_ALIGN_CENTER, -10, -150);
// 底栏时间
datetime_ = std::make_unique<DateTime>(*this);
datetime_->format("%m/%d %a %H:%M")
.align(LV_ALIGN_BOTTOM_MID, nullptr, 0, -10);
// 长按识别:按住 500 ms 即进入应用菜单
lv_obj_add_flag(raw(), LV_OBJ_FLAG_CLICKABLE); // 必须置位,否则收不到输入事件
on(LV_EVENT_LONG_PRESSED, [this](lv_event_t*) {
if (menu_open_cb_) menu_open_cb_();
});
}
// 热切换背景
HomePage& bg(const char* bin_path, const uint32_t w, const uint32_t h) {
bg_img_->bin(bin_path, w, h);
bg_img_->center();
return *this;
}
// 注册电量回调
HomePage& onBattery(std::function<uint8_t()> cb) {
battery_->onRead(std::move(cb));
return *this;
}
// 注册时间回调
HomePage& onDateTime(std::function<void(char*, size_t)> cb) {
datetime_->onRead(std::move(cb));
return *this;
}
// 注册右滑 → 打开菜单的回调
HomePage& onOpenMenu(std::function<void()> cb) {
menu_open_cb_ = std::move(cb);
return *this;
}
private:
std::unique_ptr<Image> bg_img_;
std::unique_ptr<Battery> battery_;
std::unique_ptr<DateTime> datetime_;
std::function<void()> menu_open_cb_;
};
} // namespace lvgl