/** * dual_gif_demo.c — 两个 GIF 循环交替播放实现 * * 实现策略: * 1. 启动时加载两个 GIF 整个文件到 PSRAM * 2. 构造两个 lv_img_dsc_t(CF_RAW),data 指向 PSRAM * 3. 创建一个 lv_gif 控件,set_src(gif1) * 4. 用 lv_timer 在 GIF1 时长到达后切到 GIF2,再切回 GIF1,循环 * 5. 控件只创建一次,背景不重绘 */ #include "dual_gif_demo.h" #include "esp_log.h" #include "esp_heap_caps.h" #include "esp_lvgl_port.h" #include "esp_spiffs.h" #include "lvgl.h" #include #include #include static const char *TAG = "DUAL_GIF"; // 运行时状态 static uint8_t *g_gif_data[2] = {NULL, NULL}; // PSRAM 中的 GIF 二进制 static size_t g_gif_size[2] = {0, 0}; static uint32_t g_gif_duration[2] = {0, 0}; // 每个 GIF 完整时长(ms) static lv_img_dsc_t g_gif_dsc[2]; // LVGL 图像描述符 static lv_obj_t *g_gif_obj = NULL; // 单一 lv_gif 控件 static lv_timer_t *g_switch_timer = NULL; static int g_current = 0; static bool g_playing = false; // 切换定时器:到达时长后切到另一个 GIF static void switch_timer_cb(lv_timer_t *timer) { if (!g_playing || !g_gif_obj) return; g_current = 1 - g_current; lv_gif_set_src(g_gif_obj, &g_gif_dsc[g_current]); // 下次切换时间 = 当前 GIF 的完整时长 lv_timer_set_period(timer, g_gif_duration[g_current]); } // 加载单个 GIF 文件到 PSRAM static esp_err_t load_gif_file(const char *path, uint8_t **out_data, size_t *out_size) { FILE *f = fopen(path, "rb"); if (!f) { ESP_LOGE(TAG, "打开失败: %s", path); return ESP_FAIL; } fseek(f, 0, SEEK_END); size_t sz = ftell(f); fseek(f, 0, SEEK_SET); uint8_t *buf = heap_caps_malloc(sz, MALLOC_CAP_SPIRAM); if (!buf) { ESP_LOGE(TAG, "PSRAM 分配失败: %d bytes", (int)sz); fclose(f); return ESP_ERR_NO_MEM; } size_t r = fread(buf, 1, sz, f); fclose(f); if (r != sz) { heap_caps_free(buf); return ESP_FAIL; } *out_data = buf; *out_size = sz; ESP_LOGI(TAG, "已加载 %s (%.1f KB)", path, sz / 1024.0); return ESP_OK; } esp_err_t dual_gif_demo_start(const char *path1, uint32_t dur1_ms, const char *path2, uint32_t dur2_ms) { if (g_playing) { ESP_LOGW(TAG, "已在播放,先 stop"); return ESP_ERR_INVALID_STATE; } // 检查 SPIFFS 是否已挂载,未挂载则自动挂载 size_t total = 0, used = 0; if (esp_spiffs_info("storage", &total, &used) != ESP_OK) { ESP_LOGI(TAG, "SPIFFS 未挂载,自动挂载..."); esp_vfs_spiffs_conf_t conf = { .base_path = "/spiflash", .partition_label = "storage", .max_files = 5, .format_if_mount_failed = false, }; esp_err_t err = esp_vfs_spiffs_register(&conf); if (err != ESP_OK) { ESP_LOGE(TAG, "SPIFFS 挂载失败: %s", esp_err_to_name(err)); return err; } } // 加载两个 GIF 到 PSRAM if (load_gif_file(path1, &g_gif_data[0], &g_gif_size[0]) != ESP_OK) { return ESP_FAIL; } if (load_gif_file(path2, &g_gif_data[1], &g_gif_size[1]) != ESP_OK) { heap_caps_free(g_gif_data[0]); g_gif_data[0] = NULL; return ESP_FAIL; } g_gif_duration[0] = dur1_ms; g_gif_duration[1] = dur2_ms; // 构造 lv_img_dsc_t(CF_RAW,LVGL 内部会用 gifdec 解码) for (int i = 0; i < 2; i++) { memset(&g_gif_dsc[i], 0, sizeof(lv_img_dsc_t)); g_gif_dsc[i].header.cf = LV_IMG_CF_RAW; g_gif_dsc[i].header.always_zero = 0; g_gif_dsc[i].header.reserved = 0; g_gif_dsc[i].header.w = 0; // CF_RAW 时由 gifdec 解析 g_gif_dsc[i].header.h = 0; g_gif_dsc[i].data_size = g_gif_size[i]; g_gif_dsc[i].data = g_gif_data[i]; } // 在 LVGL 任务中创建 lv_gif 控件 + 启动切换定时器 if (!lvgl_port_lock(200)) { ESP_LOGE(TAG, "lvgl_port_lock 失败"); heap_caps_free(g_gif_data[0]); heap_caps_free(g_gif_data[1]); g_gif_data[0] = g_gif_data[1] = NULL; return ESP_FAIL; } g_gif_obj = lv_gif_create(lv_scr_act()); lv_gif_set_src(g_gif_obj, &g_gif_dsc[0]); lv_obj_align(g_gif_obj, LV_ALIGN_CENTER, 0, 0); g_current = 0; g_playing = true; // 第一次切换在 GIF1 时长结束后触发 g_switch_timer = lv_timer_create(switch_timer_cb, dur1_ms, NULL); lvgl_port_unlock(); ESP_LOGI(TAG, "✓ 开始循环播放: GIF1(%lums) ↔ GIF2(%lums)", (unsigned long)dur1_ms, (unsigned long)dur2_ms); return ESP_OK; } void dual_gif_demo_stop(void) { if (!g_playing) return; g_playing = false; if (lvgl_port_lock(200)) { if (g_switch_timer) { lv_timer_del(g_switch_timer); g_switch_timer = NULL; } if (g_gif_obj) { lv_obj_del(g_gif_obj); g_gif_obj = NULL; } lvgl_port_unlock(); } for (int i = 0; i < 2; i++) { if (g_gif_data[i]) { heap_caps_free(g_gif_data[i]); g_gif_data[i] = NULL; } } ESP_LOGI(TAG, "停止播放并释放 PSRAM"); } bool dual_gif_demo_is_playing(void) { return g_playing; }