/* ui/minibar.c — 底部 mini 播放栏(跨 Activity 常驻;非播放页 + 有活动播放会话时显示)。 * * 需求(用户定案 2026-09-03):ncm 首页/歌单/搜索/设置等**非播放页**底部,当存在活动播放会话 * (player_manager 有当前曲目,state != NCM_PC_STOPPED)时显示一条紧凑 mini 播放栏(与主流 * 音乐 App 一致):封面缩略(40×40,复用 cover 缩略链)+ 歌名/歌手(截断)+ 播放/暂停钮 * (点击 toggle)+ 细进度带;**点击栏体 → 跳转 ncm 播放页(ACT_PLAYER)**;播放页内不显示此栏。 * 替代原先「顶栏进播放页图标」方案(本栏常驻非播放页,进播放页更直接,与主流 App 交互一致)。 * * 实现(零侵入页面 UI,跨页面常驻,最小侵): * - 绘制 = SDK 绘制物注册表 CUSTOM 项(z=MINI_Z,bbox=底部 MINI_H 带)。CUSTOM 项不按 owner * Activity 过滤(见 meow_ui_draw.c paint():仅 WIDGET 按 owner 过滤、VIEW 按 entered 过滤, * CUSTOM 恒画)→ 跨 Activity 常驻;且只在它所在区域被失效/整帧重画时被调(区域收敛,不整屏)。 * - 命中 = app.c app_on_tap **顶层先调 mini_bar_hit()**(返回 1 即消费,不再落页面 on_tap)—— * 点播放/暂停钮 toggle、点栏体 push(ACT_PLAYER)。不走 meow_ui_global:宿主 test_host_stub * 只桩了 global_render/hit 而未桩 add/remove,为双端 0 warning 可链,改走 app_on_tap 拦截。 * - 刷新 = mini_bar_tick()(app_on_tick 每帧调):可见性/歌曲/暂停态/进度 1% 桶变化 → * 失效整条栏带并重绘;切歌 → 复用 cover_request 拉新封面(非播放页也随切歌更新)。 * - 绘制安全:封面 cover_get() 持锁返回,绘完 cover_put()(与播放/歌单页同口径,§18 修 UAF)。 * * 性能:栏带仅 ~480×64;只在该带失效时重画(区域收敛),不整屏;进度按 1% 桶驱动 * (约 1 次/2s,不逐帧刷)。播放/进度/元数据读取全部走 player_manager 与 cover(锁内快照), * 不新开线程、不碰页面布局。 */ #include "../ncm_app_internal.h" #include #include #include "ncm_icons.h" /* ---- 几何 ---- */ #define MINI_H 64 /* 栏带高(含底部进度带/下缘) */ #define MINI_PAD 12 /* 左内边距 */ #define MINI_COV 40 /* 封面缩略边长 */ #define MINI_GAP 12 /* 封面↔文本↔按钮 间距 */ #define MINI_BTN 32 /* 播放/暂停图标绘制边长 */ #define MINI_HITR 22 /* 播放/暂停钮命中半径(略大于图标,好点) */ #define MINI_Z 400 /* 栏带 z:高于页面内容(≤60)/页脚(1),不与 topbar(999,顶部) 相交 */ #define MINI_TITLE 18 /* 歌名字号 */ #define MINI_ART 14 /* 歌手字号 */ /* ---- 状态(UI 线程串行,无需锁)---- */ static int g_mini_id; /* CUSTOM 绘制物 id(0=未注册) */ static ncm_rect_t g_bar_r; /* 栏带矩形(绘制 bbox + 命中区) */ static int g_last_vis, g_last_state, g_last_song; static long g_last_bucket; /* 进度桶(0..100,1% 粒度) */ static char g_last_id[24]; /* 上次绘制的歌曲 id(切歌判据) */ /* 是否显示 mini 栏:有活动播放会话(state≠STOPPED)且当前非播放页。 */ static int mini_visible(void) { if (player_manager_state() == NCM_PC_STOPPED) return 0; if (meow_ui_activity_active() == ACT_PLAYER) return 0; return 1; } /* 当前播放曲元数据:优先播放快照(窗口无关);快照未就绪退窗口拷贝——与 player.c cur_song_meta 同口径。 */ static int mini_cur_song_meta(ncm_song_t *out) { if (player_manager_current_song(out) == 0) return 0; int sc = player_manager_playing_song(); if (sc >= 0 && appstate_song_copy(sc, out) == 0) return 0; return -1; } /* 圆角图四角裁掉(盖回栏底色):局部复制 player.c round_corners_bg(mini 栏独立绘制, * 不复用跨文件 static)。 */ static void mini_round_corners(int x, int y, int w, int h, int r, uint32_t bg) { if (r > w / 2) r = w / 2; if (r > h / 2) r = h / 2; for (int dy = 0; dy < r; dy++) { int inset = r - (int)sqrt((double)(r * r - (r - dy) * (r - dy))); if (inset < 0) inset = 0; if (inset > w / 2) inset = w / 2; meow_ui_fill_rect(x, y + dy, inset, 1, bg); meow_ui_fill_rect(x + w - inset, y + dy, inset, 1, bg); meow_ui_fill_rect(x, y + h - 1 - dy, inset, 1, bg); meow_ui_fill_rect(x + w - inset, y + h - 1 - dy, inset, 1, bg); } } /* MD3 图标 blit(播放/暂停):ncm_icons.h 32×32 单通道 alpha 最近邻放大到 size×size,居中 (cx,cy)。 */ static void mini_draw_icon(int idx, int cx, int cy, int size, uint32_t color) { const unsigned char *src = ncm_icons[idx].data; if (size <= 0) return; int x0 = cx - size / 2, y0 = cy - size / 2; for (int y = 0; y < size; y++) { int sy = (y * NCM_ICON_SIZE) / size; if (sy >= NCM_ICON_SIZE) sy = NCM_ICON_SIZE - 1; for (int x = 0; x < size; x++) { int sx = (x * NCM_ICON_SIZE) / size; if (sx >= NCM_ICON_SIZE) sx = NCM_ICON_SIZE - 1; int a = src[sy * NCM_ICON_SIZE + sx]; if (a > 4) meow_ui_fill_rect_alpha(x0 + x, y0 + y, 1, 1, color, a); } } } /* 绘制整条 mini 栏。draw_cb 由绘制物注册表在「区域/整帧重画」时调用;仅 mini_visible() 才画。 */ static void mini_draw(void *ctx) { (void)ctx; if (!mini_visible()) return; const meow_theme *th = meow_theme_get(); const ncm_pal_t *pa = ncm_palette(g_dark); ncm_rect_t r = g_bar_r; int W = r.w, H = r.h; /* 底(surfaceContainer,与列表卡片同色)+ 顶部 1px 分隔线 */ meow_ui_fill_rect(r.x, r.y, W, H, th->surfaceContainer); meow_ui_fill_rect(r.x, r.y, W, 1, th->outlineVariant); /* 当前曲元数据(快照优先) */ ncm_song_t sm; int have = (mini_cur_song_meta(&sm) == 0); const char *nm = (have && sm.name[0]) ? sm.name : "暂无播放"; const char *ar = (have && sm.art_name[0]) ? sm.art_name : ""; /* 封面缩略(复用 cover 链;cover_get 持锁返回,cover_put 配对) */ int cx = r.x + MINI_PAD, cy = r.y + (H - MINI_COV) / 2; ncm_fill_round(cx, cy, MINI_COV, MINI_COV, 8, pa->red_ctr); meow_img *cov = cover_get(); if (cov) { meow_ui_img_cover(cx, cy, MINI_COV, cov); mini_round_corners(cx, cy, MINI_COV, MINI_COV, 8, th->surfaceContainer); } else { meow_ui_text_center(cx + MINI_COV / 2, cy + (MINI_COV - meow_ui_text_height(th->font_body)) / 2, "\u266a", th->font_body, pa->red); } cover_put(); /* 歌名/歌手(截断到播放钮左) */ int tx = cx + MINI_COV + MINI_GAP; int maxw = r.x + W - MINI_PAD - tx - (MINI_BTN + MINI_GAP * 2); if (maxw < 40) maxw = 40; int ty = r.y + (H - (MINI_TITLE + MINI_ART + 4)) / 2; meow_ui_text(tx, ty, ncm_text_fit(nm, maxw, MINI_TITLE), MINI_TITLE, th->onSurface); meow_ui_text(tx, ty + MINI_TITLE + 2, ncm_text_fit(ar, maxw, MINI_ART), MINI_ART, th->onSurfaceVariant); /* 播放/暂停钮(右侧红 icon;PAUSED/STOPPED → 播放,否则暂停) */ int st = player_manager_state(); int bx = r.x + W - MINI_PAD - MINI_BTN / 2 - MINI_GAP / 2; int bcy = r.y + H / 2; mini_draw_icon((st == NCM_PC_PAUSED || st == NCM_PC_STOPPED) ? NCM_I_PLAY : NCM_I_PAUSE, bx, bcy, MINI_BTN, pa->red); /* 细进度带(底缘 3px:track surfaceVariant + fill 品牌红) */ int64_t pos = player_manager_pos_ms(); int64_t dur = player_manager_dur_ms(); if (dur <= 0 && have) dur = sm.duration_ms; int frac = dur > 0 ? (int)(pos * 1000 / dur) : 0; if (frac < 0) frac = 0; if (frac > 1000) frac = 1000; meow_ui_fill_rect(r.x, r.y + H - 3, W, 3, th->surfaceVariant); meow_ui_fill_rect(r.x, r.y + H - 3, (int)((int64_t)W * frac / 1000), 3, pa->red); } /* 命中:app.c app_on_tap 顶层先调。非显示态/栏带外 → 0(不消费)。点播放/暂停钮 → toggle; * 点栏体 → push(ACT_PLAYER)(进播放页)。返回 1=消费(拦截,不再落页面 on_tap)。 */ int mini_bar_hit(int x, int y) { if (!mini_visible()) return 0; if (y < g_bar_r.y || y >= g_bar_r.y + g_bar_r.h || x < g_bar_r.x || x >= g_bar_r.x + g_bar_r.w) return 0; int bx = g_bar_r.x + g_bar_r.w - MINI_PAD - MINI_BTN / 2 - MINI_GAP / 2; int bcy = g_bar_r.y + g_bar_r.h / 2; int dx = x - bx, dy = y - bcy; if (dx * dx + dy * dy <= MINI_HITR * MINI_HITR) { int st = player_manager_state(); if (st == NCM_PC_PAUSED) player_manager_pause(0); /* 恢复 */ else if (st != NCM_PC_STOPPED) player_manager_pause(1); /* 暂停(含 BUFFER 保守停) */ return 1; } meow_view_page_push(ACT_PLAYER); return 1; } /* 每帧(app_on_tick):可见性/歌曲/暂停态/进度 1% 桶 变化 → 失效整条栏带并重绘; * 切歌 → 复用 cover_request 拉新封面(非播放页也随切歌更新;cover_request 幂等去重,宿主 mock 内跳过网络)。 */ void mini_bar_tick(uint64_t now) { (void)now; int vis = mini_visible(); int st = player_manager_state(); int sc = player_manager_playing_song(); ncm_song_t sm; int have = (mini_cur_song_meta(&sm) == 0); const char *id = have ? sm.id : ""; int64_t pos = player_manager_pos_ms(), dur = player_manager_dur_ms(); long bucket = dur > 0 ? (long)(pos * 100 / dur) : 0; if (bucket < 0) bucket = 0; if (bucket > 100) bucket = 100; int id_changed = (id[0] && strcmp(id, g_last_id) != 0); if (vis == g_last_vis && st == g_last_state && sc == g_last_song && bucket == g_last_bucket && !id_changed) return; g_last_vis = vis; g_last_state = st; g_last_song = sc; g_last_bucket = bucket; ncm_strlcpy(g_last_id, id, sizeof g_last_id); /* 切歌 → 请求当前曲封面(专辑图优先,无则歌单封面兜底) */ if (vis && id_changed) { if (have && sm.al_pic[0]) cover_request(sm.al_pic, sm.id); else if (have && g_st.cover_url[0]) cover_request(g_st.cover_url, sm.id); } if (g_mini_id) meow_ui_invalidate(g_bar_r.x, g_bar_r.y, g_bar_r.w, g_bar_r.h); } /* 登记(main 建页后调一次,host/device 通用):注册 CUSTOM 绘制物(跨 Activity 常驻)。 */ void mini_bar_init(void) { if (g_mini_id) return; int W = meow_ui_width(), H = meow_ui_height(); g_bar_r = NR(0, H - MINI_H, W, MINI_H); g_last_vis = -1; g_last_state = -1; g_last_song = -1; g_last_bucket = -1; g_last_id[0] = 0; g_mini_id = meow_draw_custom_register(MINI_Z, g_bar_r.x, g_bar_r.y, g_bar_r.w, g_bar_r.h, mini_draw, NULL); } /* 注销(app 退出前)。 */ void mini_bar_shutdown(void) { if (g_mini_id) { meow_draw_unregister(g_mini_id); g_mini_id = 0; } }