思维导图编辑体验:触摸交互与节点操作(ArkTS)

实例:思维导图(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 完整源码逐段解读;演示选中 → 添加子节点 → 自动重排 → 重置的完整操作链;抽象"树形结构可视化"通用模板,并给出组织架构图、决策树两个改写场景。

相关推荐
Accerlator1 小时前
从 Demo 到生产级别的 Agent 项目
学习
特立独行的猫A1 小时前
cc-switch 鸿蒙 PC移植:使用Tauri官方 feat/open-harmony 分支最新版本移植实录
harmonyos
海兰2 小时前
【Kafka学习3】Apache Kafka 本地部署环境快速入门指南
学习·kafka·apache
世人万千丶2 小时前
HarmonyOS 股票 K 线图完整源码与金融图表模板
学习·华为·金融·harmonyos·鸿蒙
210Brian2 小时前
STM32学习笔记(七)TIM定时中断(上)
笔记·stm32·学习
大雷神3 小时前
HarmonyOS 真机实战:一张普通照片怎样自动抠出人物并生成旅行海报
harmonyos
yuhulkjv3353 小时前
告别“复制-粘贴-崩格式”:ChatGPT鸿蒙版导出word格式的底层技术重构
ai·chatgpt·word·harmonyos·ai导出鸭
海兰3 小时前
【Kafka学习2】Apache Kafka 典型应用场景
学习·kafka·apache
YM52e3 小时前
ArkTS 2048 手感调优:棋盘渲染与滑动交互
华为·harmonyos