Files
esp32s3_player/main/APP/home/ui_home.c
T

350 lines
12 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
****************************************************************************************************
* @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 <stdio.h>
#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;
}
}