/** **************************************************************************************************** * @file ui_home.c * @author HiFi Player Project * @version V1.3 * @date 2026-01-01 * @brief 主界面 —— iPhone 风格图标网格(应用启动器) * * ┌──────────────────────────────────┐ * │ APP LAUNCHER 1/4 │ 状态栏 * ├──────────────────────────────────┤ * │ ╭────╮ ╭────╮ ╭────╮ ╭────╮│ * │ │ ♪ │ │ 📁 │ │ ⚙ │ │ ☰ ││ 图标(圆角渐变 + 字形) * │ ╰────╯ ╰────╯ ╰────╯ ╰────╯│ * │ MUSIC FILES SETTINGS ABOUT │ 名称 * │ WAV files in SD /MUSIC │ 选中项说明 * │ ● ○ ○ │ 页码圆点 * ├──────────────────────────────────┤ * │ [◀K1] [▶K2] [✔K3] [→K0] │ 按键提示(Dock) * └──────────────────────────────────┘ * * 按键: * K1 上一个图标 K2 下一个图标 K3 打开 K0 切换应用(全局) * * 图标不使用图片文件,而是「圆角渐变底 + LVGL 内置字形」,因此无需额外资源, * 新增应用时只要在 app.c 的表项里填 icon / color_from / color_to 即可。 **************************************************************************************************** */ #include "ui_home.h" #include #include "ui_common.h" #include "app.h" /* ---------------- 网格布局参数 ---------------- */ #define HOME_COLS 4 /* 每页图标列数(iPhone 风格一排) */ #define HOME_MAX_PAGES 4 /* 最多支持多少页 */ #define CELL_W 76 #define CELL_H 88 #define TILE_SIZE 68 /* 图标方块尺寸 */ #define TILE_RADIUS 16 /* 图标圆角(iOS 超级圆角观感) */ #define TILE_Y 0 #define NAME_DY 72 /* 名称相对单元格顶部的偏移 */ #define GRID_Y 36 /* 网格顶部 */ #define GRID_X 8 /* 网格左侧起始 x */ #define DOTS_Y 150 /* 页码圆点 y */ #define INFO_Y 132 /* 选中项说明 y */ /* ---------------- 控件 ---------------- */ static lv_obj_t *s_scr = NULL; static lv_obj_t *s_lbl_index = NULL; static lv_obj_t *s_lbl_info = NULL; static lv_obj_t *s_dot[HOME_MAX_PAGES]; /* 每页的图标控件 */ static lv_obj_t *s_tile[HOME_COLS]; static lv_obj_t *s_glyph[HOME_COLS]; static lv_obj_t *s_name[HOME_COLS]; static ui_key_bar_t s_bar; static ui_flash_t s_flash; /* ---------------- 缓存(避免无谓重绘) ---------------- */ static int s_page = -1; static int s_sel = -2; /* ---------------- 按键提示(Dock) ---------------- */ static const char *const s_symbols[APP_KEY_NUM] = { LV_SYMBOL_RIGHT, /* K0:切换应用 */ LV_SYMBOL_LEFT, /* K1:上一个图标 */ LV_SYMBOL_RIGHT, /* K2:下一个图标 */ LV_SYMBOL_OK, /* K3:打开 */ }; static const char *const s_captions[APP_KEY_NUM] = { "K0 SWITCH", "K1 PREV", "K2 NEXT", "K3 OPEN", }; static const bool s_enabled[APP_KEY_NUM] = { true, true, true, true }; /* ---------------- 内部实现 ---------------- */ /** * @brief 设置图标方块的外观(渐变 / 选中环 / 未实现时变暗) */ static void style_tile(int slot, const app_entry_t *app, bool selected) { lv_obj_t *tile = s_tile[slot]; lv_obj_set_style_bg_color(tile, lv_color_hex(app->color_from), LV_PART_MAIN); lv_obj_set_style_bg_grad_color(tile, lv_color_hex(app->color_to), LV_PART_MAIN); lv_obj_set_style_bg_grad_dir(tile, LV_GRAD_DIR_VER, LV_PART_MAIN); lv_obj_set_style_bg_opa(tile, app->enabled ? LV_OPA_COVER : LV_OPA_50, LV_PART_MAIN); /* 选中:白色描边环 + 更强的投影(iOS 图标的“抬起”观感) */ lv_obj_set_style_border_width(tile, selected ? 3 : 0, LV_PART_MAIN); lv_obj_set_style_border_color(tile, lv_color_hex(UI_COL_TEXT), LV_PART_MAIN); lv_obj_set_style_border_opa(tile, LV_OPA_COVER, LV_PART_MAIN); lv_obj_set_style_shadow_width(tile, selected ? 22 : 10, LV_PART_MAIN); lv_obj_set_style_shadow_color(tile, lv_color_hex(app->color_to), LV_PART_MAIN); lv_obj_set_style_shadow_opa(tile, selected ? LV_OPA_50 : LV_OPA_30, LV_PART_MAIN); lv_obj_set_style_shadow_ofs_y(tile, selected ? 6 : 3, LV_PART_MAIN); /* 字形 */ lv_obj_set_style_text_color(s_glyph[slot], lv_color_hex(app->enabled ? 0xFFFFFF : UI_COL_DIM), LV_PART_MAIN); /* 名称 */ lv_obj_set_style_text_color(s_name[slot], lv_color_hex(app->enabled ? (selected ? UI_COL_TEXT : UI_COL_SUB) : UI_COL_DIM), LV_PART_MAIN); } /** * @brief 刷新当前页的图标、页码与说明文字 */ static void refresh(bool force) { int count = 0; const app_entry_t *apps = app_get_entries(&count); int sel = app_get_selection(); if (count <= 0) { sel = -1; } else if (sel >= count) { sel = count - 1; } int page = (sel < 0) ? 0 : (sel / HOME_COLS); if (!force && (page == s_page) && (sel == s_sel)) { return; /* 无变化 */ } s_page = page; s_sel = sel; /* ---- 图标 ---- */ for (int i = 0; i < HOME_COLS; i++) { int idx = page * HOME_COLS + i; if (idx < count) { const app_entry_t *app = &apps[idx]; ui_label_set_if_changed(s_glyph[i], app->icon); ui_label_set_if_changed(s_name[i], app->title); style_tile(i, app, (idx == sel)); lv_obj_clear_flag(s_tile[i], LV_OBJ_FLAG_HIDDEN); lv_obj_clear_flag(s_name[i], LV_OBJ_FLAG_HIDDEN); } else { lv_obj_add_flag(s_tile[i], LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(s_name[i], LV_OBJ_FLAG_HIDDEN); } } /* ---- 状态栏计数 ---- */ char buffer[24]; if ((count > 0) && (sel >= 0)) { snprintf(buffer, sizeof(buffer), "%d/%d", sel + 1, count); } else { snprintf(buffer, sizeof(buffer), "--/--"); } ui_label_set_if_changed(s_lbl_index, buffer); /* ---- 选中项说明 ---- */ const char *info = ""; if ((sel >= 0) && (apps[sel].subtitle != NULL)) { info = apps[sel].subtitle; } ui_label_set_if_changed(s_lbl_info, info); /* ---- 页码圆点 ---- */ int pages = (count + HOME_COLS - 1) / HOME_COLS; if (pages > HOME_MAX_PAGES) { pages = HOME_MAX_PAGES; } for (int i = 0; i < HOME_MAX_PAGES; i++) { if (i < pages) { lv_obj_clear_flag(s_dot[i], LV_OBJ_FLAG_HIDDEN); lv_obj_set_style_bg_opa(s_dot[i], (i == page) ? LV_OPA_COVER : LV_OPA_30, LV_PART_MAIN); } else { lv_obj_add_flag(s_dot[i], LV_OBJ_FLAG_HIDDEN); } } } /** * @brief 打开当前选中的应用 */ static void open_selected(void) { int count = 0; const app_entry_t *apps = app_get_entries(&count); int sel = app_get_selection(); if ((sel < 0) || (sel >= count)) { return; } if (!apps[sel].enabled || (apps[sel].mode >= APP_MODE_NUM)) { return; /* 尚未实现的应用,忽略 */ } app_set_mode(apps[sel].mode); } /* ---------------- 对外接口 ---------------- */ void ui_home_create(void) { s_scr = lv_obj_create(NULL); ui_style_screen(s_scr); /* ---------------- 状态栏 ---------------- */ lv_obj_t *header = ui_card(s_scr, 0, 0, UI_HOR_RES, 28, UI_COL_PANEL, 0); lv_obj_t *title = ui_label(header, LV_SYMBOL_LIST " APP LAUNCHER", &lv_font_montserrat_14, UI_COL_SUB); lv_obj_align(title, LV_ALIGN_LEFT_MID, 10, 0); s_lbl_index = ui_label(header, "--/--", &lv_font_montserrat_12, UI_COL_SUB); lv_obj_align(s_lbl_index, LV_ALIGN_RIGHT_MID, -10, 0); /* ---------------- 图标网格(一排,iOS 风格) ---------------- */ for (int i = 0; i < HOME_COLS; i++) { lv_coord_t cell_x = (lv_coord_t)(GRID_X + i * CELL_W); /* 图标方块(直接挂在屏幕上,避免投影被容器裁掉) */ s_tile[i] = ui_card(s_scr, (lv_coord_t)(cell_x + (CELL_W - TILE_SIZE) / 2), GRID_Y, TILE_SIZE, TILE_SIZE, UI_COL_CARD, TILE_RADIUS); /* 图标字形 */ s_glyph[i] = ui_label(s_tile[i], "", &lv_font_montserrat_24, 0xFFFFFF); lv_obj_center(s_glyph[i]); /* 名称 */ s_name[i] = ui_label(s_scr, "", &lv_font_montserrat_12, UI_COL_SUB); lv_obj_set_pos(s_name[i], cell_x, (lv_coord_t)(GRID_Y + NAME_DY)); lv_obj_set_size(s_name[i], CELL_W, 14); lv_obj_set_style_text_align(s_name[i], LV_TEXT_ALIGN_CENTER, LV_PART_MAIN); lv_label_set_long_mode(s_name[i], LV_LABEL_LONG_DOT); } /* ---------------- 选中项说明 ---------------- */ s_lbl_info = ui_label(s_scr, "", &lv_font_montserrat_12, UI_COL_DIM); lv_obj_set_pos(s_lbl_info, 0, INFO_Y); lv_obj_set_size(s_lbl_info, UI_HOR_RES, 14); lv_obj_set_style_text_align(s_lbl_info, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN); lv_label_set_long_mode(s_lbl_info, LV_LABEL_LONG_DOT); /* ---------------- 页码圆点 ---------------- */ for (int i = 0; i < HOME_MAX_PAGES; i++) { lv_obj_t *dot = lv_obj_create(s_scr); lv_obj_set_size(dot, 6, 6); lv_obj_clear_flag(dot, LV_OBJ_FLAG_SCROLLABLE | LV_OBJ_FLAG_CLICKABLE); lv_obj_set_style_radius(dot, LV_RADIUS_CIRCLE, LV_PART_MAIN); lv_obj_set_style_bg_color(dot, lv_color_hex(UI_COL_TEXT), LV_PART_MAIN); lv_obj_set_style_bg_opa(dot, LV_OPA_30, LV_PART_MAIN); lv_obj_set_style_border_width(dot, 0, LV_PART_MAIN); int total_w = HOME_MAX_PAGES * 6 + (HOME_MAX_PAGES - 1) * 8; lv_obj_set_pos(dot, (lv_coord_t)((UI_HOR_RES - total_w) / 2 + i * 14), DOTS_Y); lv_obj_add_flag(dot, LV_OBJ_FLAG_HIDDEN); s_dot[i] = dot; } /* ---------------- 底部按键提示(Dock) ---------------- */ ui_key_bar_create(s_scr, &s_bar, s_symbols, s_captions, s_enabled); refresh(true); } void ui_home_show(void) { s_sel = -2; /* 强制刷新一次 */ s_page = -1; refresh(true); ui_key_bar_set_highlight(&s_bar, -1); lv_scr_load(s_scr); } void ui_home_tick(void) { ui_key_bar_set_highlight(&s_bar, ui_flash_active(&s_flash)); refresh(false); } void ui_home_flash(int key) { ui_flash_trigger(&s_flash, key); } void ui_home_handle_key(uint8_t key) { ui_flash_trigger(&s_flash, key); switch (key) { case APP_KEY_1: /* K1:上一个图标 */ app_select_move(-1); break; case APP_KEY_2: /* K2:下一个图标 */ app_select_move(+1); break; case APP_KEY_3: /* K3:打开选中的应用 */ open_selected(); break; default: break; } }