HiFi player project, first commit.

This commit is contained in:
francis.wang committed 2026-10-26 20:07:12 +08:00
commit 4126ea13a1
49 files changed
+6333

No files matched your search

+349
View File
@@ -0,0 +1,349 @@
/**
****************************************************************************************************
* @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;
}
}
+41
View File
@@ -0,0 +1,41 @@
/**
****************************************************************************************************
* @file ui_home.h
* @author HiFi Player Project
* @version V1.2
* @date 2026-01-01
* @brief 主界面(iPhone 风格图标网格)界面接口
*
* 按键:K1 上一个图标 / K2 下一个图标 / K3 打开 / K0 切换应用(全局)
****************************************************************************************************
*/
#ifndef __UI_HOME_H
#define __UI_HOME_H
#include "lvgl.h"
#ifdef __cplusplus
extern "C" {
#endif
/** 创建主界面(只创建,不显示) */
void ui_home_create(void);
/** 显示主界面并复位内部状态 */
void ui_home_show(void);
/** 周期刷新显示 */
void ui_home_tick(void);
/** 处理本界面的功能键(K1/K2/K3);K0 由 ui.c 统一处理 */
void ui_home_handle_key(uint8_t key);
/** 仅触发按键高亮(不做动作),供 K0 使用 */
void ui_home_flash(int key);
#ifdef __cplusplus
}
#endif
#endif /* __UI_HOME_H */