fix(sdk): listview 默认行高按字体行高派生 — 双行列表首行标题不再被裁剪

根因(M12 finding / SDK 侧):meow_listview 双行 renderer 把「标题+副标题」
文本块 block = 2*meow_ui_text_height(font_body) + MEOW_LISTV_SUB_GAP(2) 在行内
垂直居中(ty = 行顶 + (item-block)/2),且文本只在 `ty >= 视口顶` 时才画。
旧默认行高固定 56 假定行高 ≤ 27;真机 source_han_serif 20px(ascent 24 +
descent 6)行高 30 → block 62 > 56 → ty = 行顶 - 3,首行(行顶 == 视口顶)
标题被裁剪守卫整条跳过、副标题仍在 → 所有用默认行高的 App 两行列表首行
标题消失。

修复(patch_v2/sdk/meow_ui_widget.c):
- item_h=0 改为「派生」哨兵:listv_item_h() 在未显式定行高时按当前主题正文
  字体行高算 —— lines=2: max(56, 2*line_h+2)(行高 30 → 62)、lines=1:
  max(48, line_h)。draw/hit/max_scroll 共用同一函数,行几何恒一致;
- meow_listview_create 不再写死 56;set_item_renderer 不再写死 48/56;
  set_item_tier 恢复派生默认(仍是 1→≥48 / 2→≥56 的口径,字体大时自动长高);
- 显式 meow_listview_set_item_h(>0) 语义不变、优先且固定(ncm 传 70、files
  传 56 等调用方不受影响);set_item_h(<=0) 仍忽略,返回值改为「当前生效行高」;
- 新增 meow_listview_item_h() 读当前生效行高(App 自绘行装饰对齐用,避免再
  硬编码 56);
- src2 分支的双行判断补 `w->lines == 2`,与 pool 分支同口径:tier/renderer 设
  单行档位时明确不画副标题(此前单行档位 48px 里仍塞 62px 文本块 → 同样丢标题)。

同类路径评估(不改):
- LazyColumn/LazyRow(W_LIST):单行 label,item_h 由 create 显式给(默认 44),
  且 item_h 兼作横向列宽,按行高派生意味不明;默认 44 > 真机行高 30,当前不触发;
- lazygrid:格子几何由 App 显式给、文本底部对齐并整体 clip_push 裁剪,不受影响。

影响面:所有用默认行高的两行列表(hello/reader/files 及其它 App)在 20px 字库
下默认行高 56 → 62;显式 set_item_h 的调用方(ncm 70/launcher/files 等)零变化。
meow_ui.a 变更 → 下游 App 需重链(追加导出符号 meow_listview_item_h,ABI 兼容,
老 App 不重编也能跑)。

验证:见下一条 test 提交(宿主 --wrap 行高 30 + 像素断言 35 断言)。
This commit is contained in:
AskaEth committed 2026-11-05 20:38:02 +08:00
1 parent 805cfe11cc
commit 4d9ba46a7b
2 files changed
+44 -13

No files matched your search

+9 -3
View File
@@ -316,11 +316,16 @@ typedef struct meow_list_item {
/* 行数据源回调:idx 有效时填充 out 并返回 1;越界返回 0。每行轻量填充(不做 IO,§5.5)。 */
typedef int (*meow_list2_source)(int idx, meow_list_item *out, void *ctx);
/* 创建:视口(x,y,w,h) + 行数 count + 行数据源 + 选中模式。行高默认 56(可用 set_item_h 改)。 */
/* 创建:视口(x,y,w,h) + 行数 count + 行数据源 + 选中模式。
* 行高默认按当前主题字体行高派生(双行 max(56, 2*行高+2) / 单行 max(48, 行高)),
* 20px 字库(行高 30)下双行=62;显式 set_item_h 可覆盖(App 自管卡高时用)。 */
meow_widget *meow_listview_create(int x, int y, int w, int h, int count,
meow_list2_source src, void *ctx, meow_list_sel mode);
/* 行高(固定;越界/<=0 忽略)。 */
/* 行高:显式 px(>0)优先并固定;越界/<=0 忽略(保持字体派生)。返回当前生效行高。 */
int meow_listview_set_item_h(meow_widget *lv, int item);
/* 当前生效行高(显式值,或未显式时按主题字体行高派生的值)。无效控件返回 0。
* 行几何(draw/hit/max_scroll)全部按此值,App 自绘行装饰用它对齐,勿再硬编码 56。 */
int meow_listview_item_h(meow_widget *lv);
/* 行数(搜索过滤/懒加载/目录切换时动态更新):截断 sel 到新范围 + clamp scroll。 */
void meow_listview_set_count(meow_widget *lv, int count);
/* 滚动:set 越界 clamp 到 [0, max_scroll];by 相对滚动(App on_swipe/touch_move 用)。 */
@@ -394,7 +399,8 @@ int meow_listview_set_vsource(meow_widget *lv, meow_listview_count_fn count,
* show_icon=1 画左图标/缩略图;show_right=1 画右尾件。返回 0=成功。 */
int meow_listview_set_item_renderer(meow_widget *lv, int lines, int show_icon,
int show_right);
/* 行档位默认行高:lines=1→48(单行)、2→56(双行);与 set_item_h(精确 px)互补。 */
/* 行档位(默认行高口径):lines=1→max(48, 行高)、2→max(56, 2*行高+2),由当前字体行高派生
* (与 set_item_h 互补:tier 恢复派生默认,set_item_h 设精确 px)。 */
int meow_listview_set_item_tier(meow_widget *lv, int lines);
/* 完全自绘:设 draw_item 回调(覆盖默认 renderer);NULL=用默认 renderer。返回 0=成功。 */
int meow_listview_set_item_draw(meow_widget *lv, meow_listview_draw_fn draw,
+35 -10
View File
@@ -79,7 +79,7 @@ struct meow_widget {
int maxlen; /* W_TEXTFIELD 上限(含 NUL) */
meow_ime_mode ime_mode; /* W_TEXTFIELD IME 模式 */
int value; /* W_PROGRESS:0-100 / -1=不定; W_BADGE:计数 */
int item_h; /* W_LIST:行高/列宽(lazy),默认 44 */
int item_h; /* W_LIST:行高/列宽(lazy),默认 44;W_LISTVIEW:显式行高,0=按字体行高派生 */
int scroll; /* W_LIST:滚动偏移(lazy) */
int horizontal; /* W_LIST:1=横向(LazyRow) */
int back_on; /* W_TOPAPPBAR:显示返回箭头 */
@@ -1051,7 +1051,24 @@ int meow_list_set_container(meow_widget *w, int on)
* 宿主/设备需图片图标的 App 经 meow_ui_img_load_* 强引用 meow_ui_img.o → 此处自动解析。 */
extern void meow_ui_img_scaled(int x, int y, int w, int h, meow_img *img) __attribute__((weak));
static int listv_item_h(const meow_widget *w) { return (w->item_h > 0) ? w->item_h : 56; }
/* 行高:显式 meow_listview_set_item_h 优先(App 自管行几何时按自己算出的值传,如 ncm 70);
* 未显式(item_h<=0)→ 按当前主题正文字体行高派生,保证「文本块 ≤ 行高」:
* lines=2:max(56, 2*line_h + MEOW_LISTV_SUB_GAP) (20px 字库行高 30 → 62)
* lines=1:max(48, line_h)
* 旧默认固定 56 在真机 source_han_serif 20px(ascent 24 + descent 6 = 30)下小于双行
* 文本块 62 → 居中后 ty = 行顶 - 3,首行(行顶==视口顶)标题被 `ty >= vy0` 裁剪守卫整条
* 跳过,只剩副标题(M12 finding / M15 本源修复)。派生值由主题字库实时算出,draw/hit/
* max_scroll 三路共用同一函数 → 行几何恒一致;改字库档位/主题后自动跟随。 */
static int listv_item_h(const meow_widget *w)
{
if (w->item_h > 0) return w->item_h;
const meow_theme *th = meow_theme_get();
int line_h = th ? meow_ui_text_height(th->font_body) : 0;
if (w->lines == 1)
return (line_h > 48) ? line_h : 48;
int h = 2 * line_h + MEOW_LISTV_SUB_GAP;
return (h > 56) ? h : 56;
}
/* §5.5 虚拟化辅助:当前行数(pool 模式实时查 count_fn;src2 模式用缓存的 w->count)。 */
static int listv_count(const meow_widget *w)
@@ -1203,7 +1220,7 @@ meow_widget *meow_listview_create(int x, int y, int w, int h, int count,
t->owner = meow_ui_activity_active();
t->state = MEOW_WS_ENABLED;
t->count = count > 0 ? count : 0; t->src2 = src; t->ctx = ctx;
t->sel = -1; t->item_h = 56; t->scroll = 0; t->swipe_idx = -1;
t->sel = -1; t->item_h = 0; t->scroll = 0; t->swipe_idx = -1; /* item_h=0 → 按字体行高派生(见 listv_item_h) */
t->sel_mode = (mode >= MEOW_LIST_SEL_NONE && mode <= MEOW_LIST_SEL_MULTI) ? mode : MEOW_LIST_SEL_NONE;
t->lines = 2; t->show_icon = 1; t->show_right = 1; /* 默认双行富 item + 图标 + 右尾件(src2 布局不变) */
t->container = 1; /* M6.37 P1:SDK 默认列表走 MD3 容器模式——行=surfaceContainer 圆角卡 + 空隙 + 去分隔线 + 卡内 tonal 选中,
@@ -1225,7 +1242,12 @@ int meow_listview_set_item_h(meow_widget *w, int item)
listv_ensure_pool(w); /* 行高变小 → 可见行增多 → 池容量随之调整 */
meow_gfx_mark_dirty(w->x, w->y, w->w, w->h);
}
return w->item_h;
return listv_item_h(w); /* 返回当前生效行高(显式值或字体派生值) */
}
int meow_listview_item_h(meow_widget *w)
{
if (!w || w->type != W_LISTVIEW) return 0;
return listv_item_h(w);
}
void meow_listview_set_count(meow_widget *w, int count)
{
@@ -1398,7 +1420,7 @@ int meow_listview_set_item_renderer(meow_widget *w, int lines, int show_icon,
if (lines < 1) lines = 1;
if (lines > 2) lines = 2;
w->lines = lines; w->show_icon = show_icon ? 1 : 0; w->show_right = show_right ? 1 : 0;
if (w->item_h <= 0) w->item_h = (lines == 1) ? 48 : 56; /* 未显式定行高 → 档位默认 */
/* 未显式定行高(item_h<=0)→ 保持派生模式(按 lines 档位从字体行高算),不写死像素 */
listv_ensure_pool(w); /* 行档位可能变化 → 确保池容量 */
meow_gfx_mark_dirty(w->x, w->y, w->w, w->h);
return 0;
@@ -1410,7 +1432,7 @@ int meow_listview_set_item_tier(meow_widget *w, int lines)
if (lines < 1) lines = 1;
if (lines > 2) lines = 2;
w->lines = lines;
w->item_h = (lines == 1) ? 48 : 56; /* 档位默认高(可随后 set_item_h 覆盖) */
w->item_h = 0; /* 档位=字体派生默认(单行 max(48,行高) / 双行 max(56,2*行高+2));set_item_h 可覆盖 */
listv_ensure_pool(w);
meow_gfx_mark_dirty(w->x, w->y, w->w, w->h);
return 0;
@@ -2007,8 +2029,9 @@ void meow_widget_draw(meow_widget *w)
}
}
} else {
/* ① src2 模式(既有):每帧按需取行(meow_list2_source),行为不变。
* 行 = 图标(左) + 标题 + 副标题(次要色) + 右侧操作区;选中行 = primaryContainer。 */
/* ① src2 模式(既有):每帧按需取行(meow_list2_source),虚拟化行为不变。
* 行 = 图标(左) + 标题 + 副标题(次要色) + 右侧操作区;选中行 = primaryContainer。
* lines=1(set_item_renderer/tier 单行档位)时不画副标题,与 pool 分支同口径。 */
for (int i = first; i < last; i++) {
meow_list_item it;
memset(&it, 0, sizeof it);
@@ -2055,9 +2078,11 @@ void meow_widget_draw(meow_widget *w)
if (icy + isz >= vy0 && icy < vy1) /* 图标不越视口边界才画(防泄漏) */
listv_draw_icon(th, rlx + pad, icy, it.icon_kind, it.icon, iccol);
/* 标题 + 副标题(两行居中;无副标题=单行居中;越界不画防泄漏) */
/* 标题 + 副标题(lines=2 有副标题→两行居中;lines=1 或无副标题→单行居中;越界不画防泄漏)。
* lines 档位与 pool 分支同口径:tier/renderer 设单行时明确不画副标题,
* 否则 48px 单行档位里塞 62px 文本块 → 首行标题被裁剪守卫跳过。 */
int ty;
if (it.subtitle && it.subtitle[0]) {
if (w->lines == 2 && it.subtitle && it.subtitle[0]) {
ty = ry + (item - (2 * thh + MEOW_LISTV_SUB_GAP)) / 2;
if (it.title && ty >= vy0 && ty + thh <= vy1)
listv_text_trunc(rtx, ty, it.title, text_maxw, th->font_body, textcol);