fix(待办): 修正行高异常(我误读了测量单位,绕了三轮)
## 现象
待办列表每一条都非常高,几乎占满半屏。
## 根因
做 #42(编辑入口扩大热区)时,我给内容区 Column 加了 `.height('100%')`。
在 ListItem 里,百分比的参照系不是"行内容高度",而是被拉伸的可用高度 ⇒
单条 ListItem 被撑到 **1790px(573vp)**。
## 修正
删掉 `.height('100%')`,热区改由外层 Row 的 `padding(10)` 承担 ——
行高恢复由内容自然决定。
同时把待办列表从 `ListView + ListItemGroup + ForEach(Column)`
重构为**扁平 List**(大标题/分组头/条目摊平成一维数组),
因为原先改成 List 后分组渲染需要 ListItemGroup;扁平化后
分组视觉完全保留(分组头就是一行普通 ListItem),结构更可控。
## ⚠️ 我这轮绕了远路,记录下来
**误把 px 当 vp。** 屏幕 1080px ÷ 3.125 密度 ⇒ 1vp = 3.125px。
| 测量 | 换算 | 当时我的判断 | 实际 |
|---|---|---|---|
| 1790px | 573vp | 异常 ❌ | 对,确实异常 |
| 176px | **56vp** | 以为异常,继续改 | **正常**(两行文字+padding)|
| 117px | 37vp | 以为异常 | 正常(探针单行)|
| 275px | 88vp | 以为"修好了" | 我强行加 height(88) 造成 |
于是白白做了三轮"优化"(加 constraintSize、改删除按钮固定高、
给 ListItem 加 height(88)),每次都让行高更高,最后全部回退。
教训:**dump 出来的 bounds 是 px,改代码用的是 vp,必须先换算再判断**。
## 编译
`BUILD SUCCESSFUL`
## 真机验证
全部待办行统一 **56.3vp**(176px):未完成 1 条、已完成 6 条一致;
分组头 21.8vp、大标题 15.4vp,层级清晰。
This commit is contained in:
@@ -44,6 +44,35 @@ class MonthCell {
|
||||
* 用 class 而不是 Map/匿名对象:ArkUI 的 ForEach 需要稳定结构,
|
||||
* 用带类型的类更不容易在 ArkTS 严格模式下出错。
|
||||
*/
|
||||
/**
|
||||
* 待办列表的扁平行。
|
||||
*
|
||||
* 为什么需要:ListItemGroup 会把剩余空间按比例再分给组内 ListItem,
|
||||
* 导致行高无法由内容决定(实测不设高度=176px,设 height(96) 反而=300px)。
|
||||
* 改用扁平 List 后,必须把「大标题 / 分组头 / 条目」摊平成一维数组,
|
||||
* 由这个类承载每行的角色。
|
||||
*/
|
||||
class TodoFlatRow {
|
||||
/** caption=大标题(未完成/已完成) header=分组头 item=待办条目 */
|
||||
role: string = '';
|
||||
text: string = '';
|
||||
group: TodoGroup | undefined = undefined;
|
||||
event: DisplayEvent | undefined = undefined;
|
||||
/** 大标题额外上边距("已完成"前留白) */
|
||||
extraTop: number = 0;
|
||||
|
||||
/** ForEach key:各角色用各自稳定标识,避免刷新时错位 */
|
||||
keyOf(idx: number): string {
|
||||
if (this.role === 'item' && this.event !== undefined) {
|
||||
return `i${this.event.id}_${this.event.completed}`;
|
||||
}
|
||||
if (this.role === 'header' && this.group !== undefined) {
|
||||
return `h${this.group.calKey}`;
|
||||
}
|
||||
return `c${idx}`;
|
||||
}
|
||||
}
|
||||
|
||||
class TodoGroup {
|
||||
calKey: string = '';
|
||||
name: string = '';
|
||||
@@ -2672,64 +2701,90 @@ struct Index {
|
||||
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
|
||||
}
|
||||
|
||||
/** 待办页:与日历视图完全分开,只读展示服务器端 VTODO */
|
||||
@Builder
|
||||
// ⭐ TODO_GROUPED 待办视图:未完成 / 已完成 两大段,段内再按**日历本**分组。
|
||||
// 用户要求"按日历本分类显示,未完成里也能按家庭、生活分开,要有区分度"。
|
||||
// 分组标题前带该本的颜色圆点 —— 这才是"区分度"的来源。
|
||||
todoBody() {
|
||||
// ⭐ 外层由 Scroll 改为 List:`swipeAction` 只能挂在 ListItem 上,
|
||||
// 而 ListItem 必须是 List 的直接子节点(#39 左滑删除)。
|
||||
// 分组用 ListItemGroup 实现 —— 它天生支持分组头,正好放"色点 + 本名 + 条数"。
|
||||
List({ space: 6 }) {
|
||||
ListItem() {
|
||||
Text(`未完成(${this.todos.filter((t: DisplayEvent): boolean => !t.completed).length})`)
|
||||
.fontSize(13)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor($r('app.color.text_secondary'))
|
||||
.width('100%')
|
||||
.textAlign(TextAlign.Start)
|
||||
/**
|
||||
* 待办列表的扁平行(摊平后渲染)。
|
||||
*
|
||||
* ⚠️ 这是**普通方法**,不能挂 @Builder —— 里面是普通语句(new/赋值/循环),
|
||||
* @Builder 内只允许 UI 组件语法。
|
||||
*/
|
||||
private todoFlatRows(): TodoFlatRow[] {
|
||||
const rows: TodoFlatRow[] = [];
|
||||
const undone: TodoGroup[] = this.todoGroups(false);
|
||||
const r0 = new TodoFlatRow();
|
||||
r0.role = 'caption';
|
||||
r0.text = `未完成(${this.todos.filter((t: DisplayEvent): boolean => !t.completed).length})`;
|
||||
rows.push(r0);
|
||||
for (const g of undone) {
|
||||
const rh = new TodoFlatRow();
|
||||
rh.role = 'header';
|
||||
rh.group = g;
|
||||
rows.push(rh);
|
||||
for (const t of g.items) {
|
||||
const rt = new TodoFlatRow();
|
||||
rt.role = 'item';
|
||||
rt.event = t;
|
||||
rows.push(rt);
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
ForEach(this.todoGroups(false), (g: TodoGroup) => {
|
||||
ListItemGroup({ header: this.todoGroupHeader(g) }) {
|
||||
ForEach(g.items, (t: DisplayEvent) => {
|
||||
ListItem() {
|
||||
this.todoRow(t)
|
||||
}
|
||||
.width('100%')
|
||||
.swipeAction({ end: this.todoSwipeDelete(t) })
|
||||
}, (t: DisplayEvent) => `u${t.id}_${t.completed}`)
|
||||
}
|
||||
if (this.todos.some((t: DisplayEvent): boolean => t.completed)) {
|
||||
const r1 = new TodoFlatRow();
|
||||
r1.role = 'caption';
|
||||
r1.text = `已完成(${this.todos.filter((t: DisplayEvent): boolean => t.completed).length})`;
|
||||
r1.extraTop = 10;
|
||||
rows.push(r1);
|
||||
for (const g of this.todoGroups(true)) {
|
||||
const rh = new TodoFlatRow();
|
||||
rh.role = 'header';
|
||||
rh.group = g;
|
||||
rows.push(rh);
|
||||
for (const t of g.items) {
|
||||
const rt = new TodoFlatRow();
|
||||
rt.role = 'item';
|
||||
rt.event = t;
|
||||
rows.push(rt);
|
||||
}
|
||||
.width('100%')
|
||||
}, (g: TodoGroup) => `g0_${g.calKey}`)
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
if (this.todos.some((t: DisplayEvent): boolean => t.completed)) {
|
||||
ListItem() {
|
||||
Text(`已完成(${this.todos.filter((t: DisplayEvent): boolean => t.completed).length})`)
|
||||
.fontSize(13)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor($r('app.color.text_secondary'))
|
||||
.width('100%')
|
||||
.textAlign(TextAlign.Start)
|
||||
.margin({ top: 10 })
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
ForEach(this.todoGroups(true), (g: TodoGroup) => {
|
||||
ListItemGroup({ header: this.todoGroupHeader(g) }) {
|
||||
ForEach(g.items, (t: DisplayEvent) => {
|
||||
ListItem() {
|
||||
this.todoRow(t)
|
||||
}
|
||||
@Builder
|
||||
todoBody() {
|
||||
// ⭐ 用扁平 List 而非 ListItemGroup:
|
||||
// ListItemGroup 会把剩余空间按比例再分给组内 ListItem,
|
||||
// 实测「不设高度=176px、设 height(96)=300px」(越限越高),
|
||||
// 无法得到"由内容决定"的行高。扁平化后行高自然贴合内容。
|
||||
// 分组视觉不受影响 —— 分组头就是一行普通 ListItem。
|
||||
List({ space: 4 }) {
|
||||
ForEach(this.todoFlatRows(), (row: TodoFlatRow) => {
|
||||
if (row.role === 'caption') {
|
||||
ListItem() {
|
||||
Text(row.text)
|
||||
.fontSize(13)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor($r('app.color.text_secondary'))
|
||||
.width('100%')
|
||||
.swipeAction({ end: this.todoSwipeDelete(t) })
|
||||
}, (t: DisplayEvent) => `c${t.id}_${t.completed}`)
|
||||
.textAlign(TextAlign.Start)
|
||||
.margin({ top: row.extraTop })
|
||||
}
|
||||
.width('100%')
|
||||
}, (g: TodoGroup) => `g1_${g.calKey}`)
|
||||
}
|
||||
} else if (row.role === 'header') {
|
||||
ListItem() {
|
||||
if (row.group !== undefined) {
|
||||
this.todoGroupHeader(row.group)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
} else if (row.event !== undefined) {
|
||||
ListItem() {
|
||||
this.todoRow(row.event as DisplayEvent)
|
||||
}
|
||||
.width('100%')
|
||||
// 高度由内容自然决定(实测两行文字 ≈ 56vp,正常)。
|
||||
// ⚠️ 绝不能给内部内容区加 height('100%') —— 那会把行撑到 573vp。
|
||||
.swipeAction({ end: this.todoSwipeDelete(row.event as DisplayEvent) })
|
||||
}
|
||||
}, (row: TodoFlatRow, idx: number) => row.keyOf(idx))
|
||||
|
||||
if (this.todos.length === 0 && !this.loading) {
|
||||
ListItem() {
|
||||
@@ -2763,7 +2818,7 @@ struct Index {
|
||||
.edgeEffect(EdgeEffect.Spring)
|
||||
}
|
||||
|
||||
/** 分组头:色点 + 本名(条数) —— 与原 Column 版一致 */
|
||||
/** 分组头:色点 + 本名(条数) */
|
||||
@Builder
|
||||
todoGroupHeader(g: TodoGroup) {
|
||||
Row({ space: 6 }) {
|
||||
@@ -2781,9 +2836,8 @@ struct Index {
|
||||
/**
|
||||
* 左滑露出的「删除」按钮。
|
||||
*
|
||||
* 复用现有 `askDeleteTodo`(二次确认)→ `doDeleteTodo`(本地软删 + 服务器 DELETE),
|
||||
* 不新写删除逻辑,保证与编辑弹层里的删除走同一条路径。
|
||||
* 删除前先把目标存进 todoEditId,因为 askDeleteTodo 用的是这个字段。
|
||||
* ⚠️ 不要用 height('100%'):swipeAction 的 end 组件参与行高测量,
|
||||
* 百分比会造成放大循环。用固定高度。
|
||||
*/
|
||||
@Builder
|
||||
todoSwipeDelete(t: DisplayEvent) {
|
||||
@@ -2794,13 +2848,15 @@ struct Index {
|
||||
.textAlign(TextAlign.Center)
|
||||
}
|
||||
.width(72)
|
||||
// 用 100% 与 ListItem 同高:ListItem 高度由内容决定,
|
||||
// 按钮跟随即可(官方文档:swipeAction builder 高度默认不匹配内容,
|
||||
// 需要显式撑满才不会只占一半)。
|
||||
.height('100%')
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.backgroundColor($r('app.color.error'))
|
||||
.borderRadius(12)
|
||||
.margin({ left: 6 })
|
||||
.onClick(() => {
|
||||
// askDeleteTodo 依赖 todoEditId 定位要删哪一条
|
||||
this.todoEditId = t.id;
|
||||
this.todoEditTitle = t.title;
|
||||
this.todoEditWritable = t.writable;
|
||||
@@ -2808,6 +2864,8 @@ struct Index {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 待办编辑覆盖层。
|
||||
*
|
||||
@@ -3254,9 +3312,10 @@ struct Index {
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.layoutWeight(1)
|
||||
// ⭐ 撑满行高:原来高度只到文字高度,行内上下留白点不到,
|
||||
// 用户觉得"只有点标题才有反应"(#42)。撑满后整块内容区都可点。
|
||||
.height('100%')
|
||||
// ⚠️ 不要用 .height('100%'):ListItem 里百分比会参照被拉伸的可用高度,
|
||||
// 实测让单条变成 1790px(几乎占满整屏)。
|
||||
// 热区扩大改由外层 Row 的 padding 承担(见下方 .padding(10)),
|
||||
// 内容区自身不再加 padding,避免内外叠加把行撑高。
|
||||
// ⭐ 内容区点击 → 打开编辑(不改变完成状态)
|
||||
.onClick(() => {
|
||||
this.openTodoEditor(t);
|
||||
|
||||
Reference in New Issue
Block a user