

实例:思维导图(Mind Map)|技术:递归命中检测、选中反馈、添加子节点、重置
一、页面架构总览
Column
├── 标题栏 // 标题 + 交互提示
├── 导图画布 // Canvas:连线 + 节点
└── 操作栏 // ➕ 添加子节点 + 🔄 重置
页面极简:画布 + 两个操作按钮。核心交互在画布的触摸(选中节点)与按钮(添加/重置)。
二、触摸命中检测 ★核心交互
typescript
private onTouchChart(e: TouchEvent): void {
const t = e.touches[0];
if (!t) {
return;
}
if (e.type === TouchType.Down) {
const hit = this.hitTest(t.x, t.y);
if (hit !== null) {
this.selectedId = hit.id;
this.hintText = `选中「${hit.text}」` + (hit.children.length === 0 ? ' · 点击左下角 ➕ 添加子节点' : '');
this.redraw();
} else {
this.selectedId = -1;
this.hintText = '点击节点选中 · 点击叶子节点添加子节点';
this.redraw();
}
}
}
命中 → 选中 → 反馈:点击节点 → selectedId 更新 + 提示文案更新 + 重绘(选中态高亮)。点击空白 → 取消选中。
2.1 递归命中
typescript
private hitTest(x: number, y: number): MindNode | null {
if (this.root === null) {
return null;
}
return this.hitRecursive(this.root, x, y);
}
private hitRecursive(n: MindNode, x: number, y: number): MindNode | null {
// 先检查子节点(叶子优先,视觉上更精确)
for (const child of n.children) {
const hit = this.hitRecursive(child, x, y);
if (hit !== null) {
return hit;
}
}
// 再检查自身
if (x >= n.x - this.nodeW / 2 && x <= n.x + this.nodeW / 2 &&
y >= n.y - this.nodeH / 2 && y <= n.y + this.nodeH / 2) {
return n;
}
return null;
}
矩形命中检测 :点 (x,y) 在节点矩形的 x±nodeW/2, y±nodeH/2 范围内即命中------点是否在矩形内的四边界比较。
叶子优先(先子后父) :递归先查子节点再查自身------当触摸点在父子节点重叠区域时,优先命中更"里层"的节点(视觉上更精确,叶子小、父节点大,用户点叶子时不想误选中父)。
命中返回 :从递归深处返回命中的节点,null 表示未命中。
三、添加子节点
typescript
private addChildToSelected(): void {
if (this.selectedId === -1 || this.root === null) {
return;
}
const node = this.findNode(this.root, this.selectedId);
if (node === null) {
return;
}
node.children.push({
id: this.nextId++,
text: `新节点 ${this.nextId - 100}`,
x: 0, y: 0, children: [],
});
this.layoutTree(); // 重新布局(新节点影响整棵树位置)
this.redraw();
}
流程:校验选中 → 递归查找节点 → push 子节点 → 重新布局 → 重绘。
3.1 递归查找
typescript
private findNode(n: MindNode, id: number): MindNode | null {
if (n.id === id) {
return n;
}
for (const child of n.children) {
const found = this.findNode(child, id);
if (found !== null) {
return found;
}
}
return null;
}
按 id 深度优先查找------树遍历的标准递归。命中返回节点引用(可修改其 children)。
为什么加子节点要重新布局 :新节点改变了子树节点数 → 子树高度变化 → 父节点垂直居中位置变化 → 兄弟节点下移------局部变化影响全局位置 ,必须整体重排。layoutTree() 重算所有节点 x/y。
四、重置
typescript
Button('🔄 重置').layoutWeight(1).backgroundColor('#EEF2F7').fontColor('#555555')
.onClick(() => {
this.buildSeedTree(); // 重建种子树 + 布局
this.redraw();
})
重置 = 重建种子树 :buildSeedTree 内部重建 root、重置 nextId、清选中、重新布局------一个方法完成全部初始化,重绘即可。这也解释了为什么 buildSeedTree 里调用了 layoutTree(aboutToAppear 时构建即布局)。
五、交互流程图
启动 ──► aboutToAppear ──► buildSeedTree(建树 + 布局)
│
Canvas onReady ──► redraw(连线 + 节点)
│
触摸画布 ──► onTouchChart
│ ├─ hitTest(递归命中)
│ ├─ 命中 → selectedId + 提示 + 重绘(蓝高亮)
│ └─ 未命中 → selectedId=-1 + 重绘
│
点"➕ 添加子节点" ──► addChildToSelected
│ ├─ 校验选中
│ ├─ findNode 递归查找
│ ├─ push 子节点
│ ├─ layoutTree 重排
│ └─ redraw
│
点"🔄 重置" ──► buildSeedTree + redraw
六、UI 技术要点对照表
| 技术点 | 实现方式 | 生产价值 |
|---|---|---|
| 矩形命中 | 四边界比较 | 点选节点 |
| 递归命中 | 先子后父 | 叶子优先 |
| 选中反馈 | 蓝底白字 + 提示 | 状态可视化 |
| 递归查找 | findNode DFS | 树操作基础 |
| 布局重排 | layoutTree 全量 | 位置一致性 |
| 重置 | buildSeedTree 复用 | 初始化收口 |
七、本篇小结
本篇完成了思维导图的交互层:递归命中检测(先子后父) 、选中反馈(蓝高亮 + 提示) 、添加子节点(findNode + push + 重排) 、重置(复用 buildSeedTree) 。交互层的核心是"递归无处不在"------查找用递归、命中用递归,与布局/绘制的递归形成完整闭环。
下一篇《完整代码与运行效果》将给出 MindMapPage 全量源码解读、操作剧本演示,以及"树形结构可视化"通用模板抽象------适用于组织架构图、文件树、决策树等一切树形结构。
八、UI 深度扩展
1. 节点拖拽
onTouch 三态:
Down:hitTest 命中 → 记录拖拽节点与偏移
Move:节点 x/y = 触摸点 - 偏移(局部重绘)
Up:结束拖拽
拖拽 = 命中 + 移动 + 重绘。注意:拖拽后自动布局会被覆盖,可提供"重新布局"按钮恢复。
2. 节点文本编辑
双击节点(Down 计时 < 300ms 算点击,> 300ms 算长按)→ 弹 TextInput → 保存更新 text。文本编辑提升可用性,实现 = 长按检测 + 输入弹窗 + 节点更新。
3. 多选与删除
长按 → 选中菜单(添加/删除/改名)。删除节点需递归删除子树(先删子再删自身------与数据库"先子后父"同思想)。
4. 折叠/展开
节点加 collapsed 字段:折叠时不布局/绘制子节点,连线到"折叠标记"(如 ⊕)。折叠是思维导图管理大树的必备功能。
5. FAQ
Q1:为什么命中要"先子后父"而不是"先父后子"?
A:叶子节点小、父节点大,用户在叶子附近点击时,先查子能精确命中叶子(避免误中父节点)。从最精确的开始查是命中检测的通用策略。
Q2:添加子节点后为什么全部重排?
A:子树高度变化会"推动"父节点位置和后续兄弟节点------树的位置是全局耦合的。全量重排保证一致,节点少时开销可忽略。
Q3:选中态能多选吗?
A:本实例单选(selectedId 一个值)。多选需数组 selectedIds + 高亮遍历------实现简单,按需扩展。
Q4:提示文案里"点击左下角 ➕"但按钮在下方,表述对吗?
A:按钮是"➕ 添加子节点"在画布下方居中------提示文案为示意,实际位置以按钮为准。文案与 UI 位置应一致,可改为"点击下方 ➕ 按钮"。
Q5:树太大画布放不下怎么办?
A:缩放 + 平移(深度扩展 4)。或"折叠分支"只显示部分。视口管理是大树的必答题,本实例种子树小无需。
九、下篇预告
最后一篇《完整代码与运行效果》将:给出 MindMapPage.ets 完整源码逐段解读;演示选中 → 添加子节点 → 自动重排 → 重置的完整操作链;抽象"树形结构可视化"通用模板,并给出组织架构图、决策树两个改写场景。