Update project version to 2.2.2, Noto fonts and emoji support. (#1720)

This commit is contained in:
Xiaoxia
2026-02-01 01:04:24 +08:00
committed by GitHub
parent f7284a57df
commit b6c61fe390
11 changed files with 168 additions and 57 deletions

View File

@ -45,6 +45,10 @@ void Display::SetChatMessage(const char* role, const char* content) {
ESP_LOGW(TAG, " %s", content);
}
void Display::ClearChatMessages() {
// Default empty implementation, override in subclasses if needed
}
void Display::SetTheme(Theme* theme) {
current_theme_ = theme;
Settings settings("display", true);

View File

@ -35,6 +35,7 @@ public:
virtual void ShowNotification(const std::string &notification, int duration_ms = 3000);
virtual void SetEmotion(const char* emotion);
virtual void SetChatMessage(const char* role, const char* content);
virtual void ClearChatMessages();
virtual void SetTheme(Theme* theme);
virtual Theme* GetTheme() { return current_theme_; }
virtual void UpdateStatusBar(bool update_all = false);

View File

@ -556,7 +556,6 @@ void LcdDisplay::SetChatMessage(const char* role, const char* content) {
}
auto lvgl_theme = static_cast<LvglTheme*>(current_theme_);
auto text_font = lvgl_theme->text_font()->font();
// Create a message bubble
lv_obj_t* msg_bubble = lv_obj_create(content_);
@ -774,6 +773,26 @@ void LcdDisplay::SetPreviewImage(std::unique_ptr<LvglImage> image) {
// Auto-scroll to the image bubble
lv_obj_scroll_to_view_recursive(img_bubble, LV_ANIM_ON);
}
void LcdDisplay::ClearChatMessages() {
DisplayLockGuard lock(this);
if (content_ == nullptr) {
return;
}
// Use lv_obj_clean to delete all children of content_ (chat message bubbles)
lv_obj_clean(content_);
// Reset chat_message_label_ as it has been deleted
chat_message_label_ = nullptr;
// Show the centered AI logo (emoji_label_) again
if (emoji_label_ != nullptr) {
lv_obj_remove_flag(emoji_label_, LV_OBJ_FLAG_HIDDEN);
}
ESP_LOGI(TAG, "Chat messages cleared");
}
#else
void LcdDisplay::SetupUI() {
DisplayLockGuard lock(this);
@ -891,29 +910,35 @@ void LcdDisplay::SetupUI() {
lv_label_set_text(status_label_, Lang::Strings::INITIALIZING);
lv_obj_align(status_label_, LV_ALIGN_CENTER, 0, 0);
/* Top layer: Bottom bar - fixed at bottom, minimum height 48, height can be adaptive */
/* Top layer: Bottom bar - fixed height at bottom */
bottom_bar_ = lv_obj_create(screen);
lv_obj_set_width(bottom_bar_, LV_HOR_RES);
lv_obj_set_height(bottom_bar_, LV_SIZE_CONTENT);
lv_obj_set_style_min_height(bottom_bar_, 48, 0); // Set minimum height 48
lv_obj_set_size(bottom_bar_, LV_HOR_RES, text_font->line_height + lvgl_theme->spacing(12));
lv_obj_set_style_radius(bottom_bar_, 0, 0);
lv_obj_set_style_bg_color(bottom_bar_, lvgl_theme->background_color(), 0);
lv_obj_set_style_text_color(bottom_bar_, lvgl_theme->text_color(), 0);
lv_obj_set_style_pad_top(bottom_bar_, lvgl_theme->spacing(2), 0);
lv_obj_set_style_pad_bottom(bottom_bar_, lvgl_theme->spacing(2), 0);
lv_obj_set_style_pad_all(bottom_bar_, 0, 0);
lv_obj_set_style_pad_left(bottom_bar_, lvgl_theme->spacing(4), 0);
lv_obj_set_style_pad_right(bottom_bar_, lvgl_theme->spacing(4), 0);
lv_obj_set_style_border_width(bottom_bar_, 0, 0);
lv_obj_set_scrollbar_mode(bottom_bar_, LV_SCROLLBAR_MODE_OFF);
lv_obj_align(bottom_bar_, LV_ALIGN_BOTTOM_MID, 0, 0);
/* chat_message_label_ placed in bottom_bar_ and vertically centered */
/* chat_message_label_ placed in bottom_bar_, single-line horizontal scroll */
chat_message_label_ = lv_label_create(bottom_bar_);
lv_label_set_text(chat_message_label_, "");
lv_obj_set_width(chat_message_label_, LV_HOR_RES - lvgl_theme->spacing(8)); // Subtract left and right padding
lv_label_set_long_mode(chat_message_label_, LV_LABEL_LONG_WRAP); // Auto wrap mode
lv_obj_set_style_text_align(chat_message_label_, LV_TEXT_ALIGN_CENTER, 0); // Center text alignment
lv_obj_set_width(chat_message_label_, LV_HOR_RES - lvgl_theme->spacing(8));
lv_label_set_long_mode(chat_message_label_, LV_LABEL_LONG_SCROLL_CIRCULAR);
lv_obj_set_style_text_align(chat_message_label_, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_style_text_color(chat_message_label_, lvgl_theme->text_color(), 0);
lv_obj_align(chat_message_label_, LV_ALIGN_CENTER, 0, 0); // Vertically and horizontally centered in bottom_bar_
lv_obj_align(chat_message_label_, LV_ALIGN_CENTER, 0, 0);
// Start scrolling after a delay (short text won't scroll)
static lv_anim_t a;
lv_anim_init(&a);
lv_anim_set_delay(&a, 1000);
lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE);
lv_obj_set_style_anim(chat_message_label_, &a, LV_PART_MAIN);
lv_obj_set_style_anim_duration(chat_message_label_, lv_anim_speed_clamped(60, 300, 60000), LV_PART_MAIN);
low_battery_popup_ = lv_obj_create(screen);
lv_obj_set_scrollbar_mode(low_battery_popup_, LV_SCROLLBAR_MODE_OFF);
@ -972,6 +997,14 @@ void LcdDisplay::SetChatMessage(const char* role, const char* content) {
}
lv_label_set_text(chat_message_label_, content);
}
void LcdDisplay::ClearChatMessages() {
DisplayLockGuard lock(this);
// In non-wechat mode, just clear the chat message label
if (chat_message_label_ != nullptr) {
lv_label_set_text(chat_message_label_, "");
}
}
#endif
void LcdDisplay::SetEmotion(const char* emotion) {
@ -1005,6 +1038,8 @@ void LcdDisplay::SetEmotion(const char* emotion) {
gif_controller_ = std::make_unique<LvglGif>(image->image_dsc());
if (gif_controller_->IsLoaded()) {
// Set loop delay to 1000ms
gif_controller_->SetLoopDelay(3000);
// Set up frame update callback
gif_controller_->SetFrameCallback([this]() {
lv_image_set_src(emoji_image_, gif_controller_->image_dsc());

View File

@ -48,7 +48,8 @@ protected:
public:
~LcdDisplay();
virtual void SetEmotion(const char* emotion) override;
virtual void SetChatMessage(const char* role, const char* content) override;
virtual void SetChatMessage(const char* role, const char* content) override;
virtual void ClearChatMessages() override;
virtual void SetPreviewImage(std::unique_ptr<LvglImage> image) override;
// Add theme switching function

View File

@ -5,7 +5,8 @@
#define TAG "LvglGif"
LvglGif::LvglGif(const lv_img_dsc_t* img_dsc)
: gif_(nullptr), timer_(nullptr), last_call_(0), playing_(false), loaded_(false) {
: gif_(nullptr), timer_(nullptr), last_call_(0), playing_(false), loaded_(false),
loop_delay_ms_(0), loop_waiting_(false), loop_wait_start_(0) {
if (!img_dsc || !img_dsc->data) {
ESP_LOGE(TAG, "Invalid image descriptor");
return;
@ -66,6 +67,7 @@ void LvglGif::Start() {
if (timer_) {
playing_ = true;
loop_waiting_ = false; // Reset loop waiting state
last_call_ = lv_tick_get();
lv_timer_resume(timer_);
lv_timer_reset(timer_);
@ -104,9 +106,15 @@ void LvglGif::Stop() {
lv_timer_pause(timer_);
}
// Reset loop waiting state
loop_waiting_ = false;
if (gif_) {
gd_rewind(gif_);
NextFrame();
// Render first frame without advancing
if (gif_->canvas) {
gd_render_frame(gif_, gif_->canvas);
}
ESP_LOGD(TAG, "GIF animation stopped and rewound");
}
}
@ -134,6 +142,15 @@ void LvglGif::SetLoopCount(int32_t count) {
gif_->loop_count = count;
}
uint32_t LvglGif::GetLoopDelay() const {
return loop_delay_ms_;
}
void LvglGif::SetLoopDelay(uint32_t delay_ms) {
loop_delay_ms_ = delay_ms;
ESP_LOGD(TAG, "Loop delay set to %lu ms", delay_ms);
}
uint16_t LvglGif::width() const {
if (!loaded_ || !gif_) {
return 0;
@ -157,6 +174,18 @@ void LvglGif::NextFrame() {
return;
}
// Check if we're in loop wait state (only for infinite loop GIFs with delay)
if (loop_waiting_) {
uint32_t wait_elapsed = lv_tick_elaps(loop_wait_start_);
if (wait_elapsed < loop_delay_ms_) {
// Still waiting for loop delay
return;
}
// Loop delay completed, continue playing
loop_waiting_ = false;
ESP_LOGD(TAG, "Loop delay completed, continuing GIF");
}
// Check if enough time has passed for the next frame
uint32_t elapsed = lv_tick_elaps(last_call_);
if (elapsed < gif_->gce.delay * 10) {
@ -165,15 +194,30 @@ void LvglGif::NextFrame() {
last_call_ = lv_tick_get();
// Save file position before getting next frame to detect loop
uint32_t pos_before = gif_->f_rw_p;
// Get next frame
int has_next = gd_get_frame(gif_);
if (has_next == 0) {
// Animation finished, pause timer
// Animation truly finished (non-infinite loop)
playing_ = false;
if (timer_) {
lv_timer_pause(timer_);
}
ESP_LOGD(TAG, "GIF animation completed");
return;
}
// Detect loop by checking if file position jumped back (rewound to start)
// This works for looping GIFs regardless of when loop_count is set
if (loop_delay_ms_ > 0 && gif_->f_rw_p < pos_before) {
// File position decreased, meaning GIF looped back to beginning
// Start waiting before rendering this frame
loop_waiting_ = true;
loop_wait_start_ = lv_tick_get();
ESP_LOGD(TAG, "GIF completed one cycle, waiting %lu ms before next loop", loop_delay_ms_);
return;
}
// Render current frame

View File

@ -58,6 +58,17 @@ public:
*/
void SetLoopCount(int32_t count);
/**
* Get loop delay in milliseconds (delay between loops)
*/
uint32_t GetLoopDelay() const;
/**
* Set loop delay in milliseconds (delay between loops)
* @param delay_ms Delay in milliseconds before starting next loop. 0 means no delay.
*/
void SetLoopDelay(uint32_t delay_ms);
/**
* Get GIF dimensions
*/
@ -86,6 +97,11 @@ private:
bool playing_;
bool loaded_;
// Loop delay configuration
uint32_t loop_delay_ms_; // Delay between loops in milliseconds
bool loop_waiting_; // Whether we're waiting for the next loop
uint32_t loop_wait_start_; // Timestamp when loop wait started
// Frame update callback
std::function<void()> frame_callback_;