【HarmonyOS】React Native实战+Tree节点选择状态组件

HarmonyOS实战:React Native实现Tree节点选择状态

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

概述

Tree组件是展示层次化数据的核心UI元素,广泛应用于文件系统浏览、组织架构展示、分类选择等场景。在React Native生态中,Tree组件并非内置组件,需要基于基础组件自行构建。

在OpenHarmony 6.0.0平台上实现Tree组件面临着特殊的挑战:平台特有的渲染机制、手势处理差异以及性能优化需求。本文将深入讲解如何构建一个支持节点选择状态的Tree组件。

选择状态类型

常见选择模式

复制代码
┌─────────────────────────────────────────┐
│  单选模式                    │
│  ○ 节点A  ○ 节点B  ● 节点C              │
└─────────────────────────────────────────┘

┌─────────────────────────────────────────┐
│  多选模式                    │
│  ☑ 节点A  ☐ 节点B  ☑ 节点C              │
└─────────────────────────────────────────┘

┌─────────────────────────────────────────┐
│  级联模式                   │
│  ☑ 父节点                              │
│    ├─ ☑ 子节点1                       │
│    └─ ☑ 子节点2                       │
└─────────────────────────────────────────┘
模式 描述 典型场景
单选 仅允许选择一个节点 选项切换、类别筛选
多选 可同时选择多个节点 批量操作、标签选择
级联 父节点自动选中所有子节点 权限配置、功能开关
半选 部分子节点被选中 状态指示、进度展示

数据结构设计

节点模型

typescript 复制代码
interface TreeNode {
  id: string;                  // 唯一标识
  label: string;               // 显示文本
  children?: TreeNode[];       // 子节点列表
  isSelected?: boolean;        // 是否完全选中
  isPartialSelected?: boolean; // 是否部分选中
  expanded?: boolean;          // 是否展开
  disabled?: boolean;          // 是否禁用
}

扁平化数据结构

为避免OpenHarmony平台上递归组件的性能问题,推荐使用扁平化数据结构:

typescript 复制代码
interface FlatTreeNode {
  id: string;           // 节点ID (如 '1-2-3')
  label: string;        // 显示文本
  level: number;        // 层级深度 (0, 1, 2...)
  hasChildren: boolean; // 是否有子节点
  expanded: boolean;    // 是否展开
  isSelected: boolean;  // 是否选中
}

OpenHarmony平台适配

渲染机制差异

特性 iOS/Android OpenHarmony 6.0.0 适配策略
递归渲染 良好支持 深度嵌套性能差 使用扁平化数据
布局计算 高效 复杂结构耗时高 简化DOM结构
动画支持 流畅 需谨慎使用 减少动画复杂度
内存管理 宽松 较严格 及时释放资源

性能优化策略

  1. 虚拟化渲染:只渲染可见区域的节点
  2. 扁平化数据:避免递归组件带来的性能问题
  3. 状态批处理:合并多个状态更新操作
  4. 懒加载:延迟加载子节点数据

完整实现代码

typescript 复制代码
/**
 * HarmonyOS实战:Tree节点选择状态组件
 *
 * @platform OpenHarmony 6.0.0 (API 20)
 * @react-native 0.72.5
 * @typescript 4.8.4
 */

import React, { useState, useMemo } from 'react';
import {
  View,
  Text,
  TouchableOpacity,
  ScrollView,
  StyleSheet,
} from 'react-native';

// 扁平化树节点数据结构
interface FlatTreeNode {
  id: string;
  label: string;
  level: number;
  hasChildren: boolean;
  expanded: boolean;
  isSelected: boolean;
}

interface TreeSelectionProps {
  onBack: () => void;
}

const TreeSelectionScreen: React.FC<TreeSelectionProps> = ({ onBack }) => {
  // 初始化扁平化树数据
  const [treeData, setTreeData] = useState<FlatTreeNode[]>([
    // 根节点
    { id: 'root', label: '🏢 公司组织', level: 0, hasChildren: true, expanded: true, isSelected: false },
    // 技术部门
    { id: 'tech', label: '💻 技术部', level: 1, hasChildren: true, expanded: false, isSelected: false },
    { id: 'tech-fe', label: '📱 前端组', level: 2, hasChildren: false, expanded: false, isSelected: false },
    { id: 'tech-be', label: '⚙️ 后端组', level: 2, hasChildren: false, expanded: false, isSelected: false },
    { id: 'tech-mobile', label: '📲 移动端组', level: 2, hasChildren: false, expanded: false, isSelected: false },
    // 产品部门
    { id: 'product', label: '📊 产品部', level: 1, hasChildren: true, expanded: false, isSelected: false },
    { id: 'product-design', label: '🎨 设计组', level: 2, hasChildren: false, expanded: false, isSelected: false },
    { id: 'product-pm', label: '📋 产品组', level: 2, hasChildren: false, expanded: false, isSelected: false },
    // 运营部门
    { id: 'ops', label: '📈 运营部', level: 1, hasChildren: true, expanded: false, isSelected: false },
    { id: 'ops-content', label: '✍️ 内容组', level: 2, hasChildren: false, expanded: false, isSelected: false },
    { id: 'ops-data', label: '📊 数据组', level: 2, hasChildren: false, expanded: false, isSelected: false },
  ]);

  // 计算可见节点列表
  const visibleNodes = useMemo(() => {
    const result: FlatTreeNode[] = [];
    const expandedMap = new Map(treeData.map(node => [node.id, node.expanded]));

    for (const node of treeData) {
      // 检查父节点是否展开
      if (node.level > 0) {
        const parentId = node.id.substring(0, node.id.lastIndexOf('-'));
        const parentExpanded = expandedMap.get(parentId);
        if (!parentExpanded) continue;
      }
      result.push(node);
    }
    return result;
  }, [treeData]);

  // 切换节点展开状态
  const toggleExpand = (id: string) => {
    setTreeData(prev => prev.map(node =>
      node.id === id ? { ...node, expanded: !node.expanded } : node
    ));
  };

  // 切换节点选择状态
  const toggleSelect = (id: string) => {
    setTreeData(prev => prev.map(node =>
      node.id === id ? { ...node, isSelected: !node.isSelected } : node
    ));
  };

  // 获取选中数量
  const selectedCount = useMemo(() => {
    return treeData.filter(node => node.isSelected).length;
  }, [treeData]);

  return (
    <ScrollView style={styles.container} showsVerticalScrollIndicator={false}>
      {/* 顶部导航栏 */}
      <View style={styles.navigationBar}>
        <TouchableOpacity onPress={onBack} style={styles.backBtn}>
          <Text style={styles.backText}>← 返回</Text>
        </TouchableOpacity>
        <View style={styles.titleWrapper}>
          <Text style={styles.mainTitle}>Tree节点选择</Text>
          <Text style={styles.subTitle}>层次化数据选择组件</Text>
        </View>
      </View>

      {/* 平台信息 */}
      <View style={styles.versionBanner}>
        <Text style={styles.versionText}>OpenHarmony 6.0.0 | API 20</Text>
      </View>

      {/* 选择统计 */}
      <View style={styles.statsCard}>
        <Text style={styles.statsText}>已选中 {selectedCount} 个节点</Text>
      </View>

      {/* Tree组件主体 */}
      <View style={styles.treeContainer}>
        {visibleNodes.map((node) => (
          <TouchableOpacity
            key={node.id}
            style={[styles.nodeRow, { paddingLeft: 16 + node.level * 20 }]}
            onPress={() => {
              if (node.hasChildren) {
                toggleExpand(node.id);
              } else {
                toggleSelect(node.id);
              }
            }}
            activeOpacity={0.7}
          >
            {/* 展开/折叠图标 */}
            {node.hasChildren ? (
              <View style={styles.iconBox}>
                <Text style={styles.toggleIcon}>
                  {node.expanded ? '▼' : '▶'}
                </Text>
              </View>
            ) : (
              /* 选择框 */
              <View style={[styles.checkbox, node.isSelected && styles.checkboxChecked]}>
                {node.isSelected && <Text style={styles.checkmark}>✓</Text>}
              </View>
            )}

            {/* 节点标签 */}
            <Text style={[styles.nodeLabel, node.isSelected && styles.labelSelected]}>
              {node.label}
            </Text>
          </TouchableOpacity>
        ))}
      </View>

      {/* API说明 */}
      <View style={styles.apiCard}>
        <Text style={styles.cardTitle}>核心API</Text>
        <View style={styles.apiTable}>
          <View style={styles.apiRow}>
            <Text style={styles.apiHeader}>属性</Text>
            <Text style={styles.apiHeader}>类型</Text>
            <Text style={styles.apiHeader}>说明</Text>
          </View>
          <View style={styles.apiRow}>
            <Text style={styles.apiCell}>data</Text>
            <Text style={styles.apiCell}>TreeNode[]</Text>
            <Text style={styles.apiCell}>树形数据</Text>
          </View>
          <View style={styles.apiRow}>
            <Text style={styles.apiCell}>onSelectionChange</Text>
            <Text style={styles.apiCell}>Function</Text>
            <Text style={styles.apiCell}>选择回调</Text>
          </View>
        </View>
      </View>

      {/* 应用场景 */}
      <View style={styles.scenarioCard}>
        <Text style={styles.cardTitle}>应用场景</Text>
        <View style={styles.scenarioList}>
          <Text style={styles.scenarioItem}>🌁 文件系统浏览器</Text>
          <Text style={styles.scenarioItem}>🏢 组织架构展示</Text>
          <Text style={styles.scenarioItem}>📂 分类选择器</Text>
          <Text style={styles.scenarioItem}>🗺️ 多级导航菜单</Text>
        </View>
      </View>

      {/* 适配要点 */}
      <View style={styles.adaptCard}>
        <Text style={styles.adaptTitle}>OpenHarmony适配要点</Text>
        <View style={styles.adaptList}>
          <Text style={styles.adaptItem}>• 扁平化数据避免递归组件</Text>
          <Text style={styles.adaptItem}>• 使用useMemo缓存可见节点</Text>
          <Text style={styles.adaptItem}>• 简化动画提升性能</Text>
          <Text style={styles.adaptItem}>• 避免过度嵌套的视图结构</Text>
        </View>
      </View>
    </ScrollView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f8f9fa',
  },
  navigationBar: {
    flexDirection: 'row',
    alignItems: 'center',
    paddingHorizontal: 16,
    paddingVertical: 12,
    backgroundColor: '#10B981',
    paddingTop: 50,
  },
  backBtn: {
    padding: 8,
  },
  backText: {
    fontSize: 16,
    color: '#fff',
    fontWeight: '600',
  },
  titleWrapper: {
    flex: 1,
    marginLeft: 8,
  },
  mainTitle: {
    fontSize: 20,
    fontWeight: 'bold',
    color: '#fff',
  },
  subTitle: {
    fontSize: 12,
    color: 'rgba(255, 255, 255, 0.85)',
    marginTop: 2,
  },
  versionBanner: {
    backgroundColor: '#d1fae5',
    paddingHorizontal: 16,
    paddingVertical: 8,
  },
  versionText: {
    fontSize: 12,
    color: '#059669',
    textAlign: 'center',
  },
  statsCard: {
    backgroundColor: '#fef3c7',
    marginHorizontal: 16,
    marginTop: 16,
    padding: 12,
    borderRadius: 8,
    alignItems: 'center',
  },
  statsText: {
    fontSize: 14,
    color: '#d97706',
    fontWeight: '600',
  },
  treeContainer: {
    backgroundColor: '#fff',
    margin: 16,
    borderRadius: 12,
  },
  nodeRow: {
    flexDirection: 'row',
    alignItems: 'center',
    paddingVertical: 12,
    paddingRight: 16,
    minHeight: 48,
    borderBottomWidth: 1,
    borderBottomColor: '#f0f0f0',
  },
  iconBox: {
    width: 24,
    alignItems: 'center',
  },
  toggleIcon: {
    fontSize: 12,
    color: '#6b7280',
  },
  checkbox: {
    width: 22,
    height: 22,
    borderWidth: 2,
    borderColor: '#d1d5db',
    borderRadius: 4,
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: '#fff',
  },
  checkboxChecked: {
    backgroundColor: '#10B981',
    borderColor: '#10B981',
  },
  checkmark: {
    color: '#fff',
    fontSize: 14,
    fontWeight: 'bold',
  },
  nodeLabel: {
    fontSize: 15,
    color: '#374151',
    flex: 1,
  },
  labelSelected: {
    color: '#10B981',
    fontWeight: '600',
  },
  apiCard: {
    backgroundColor: '#fff',
    margin: 16,
    padding: 16,
    borderRadius: 12,
  },
  cardTitle: {
    fontSize: 16,
    fontWeight: 'bold',
    color: '#333',
    marginBottom: 12,
  },
  apiTable: {
    borderWidth: 1,
    borderColor: '#e5e7eb',
    borderRadius: 8,
    overflow: 'hidden',
  },
  apiRow: {
    flexDirection: 'row',
    borderBottomWidth: 1,
    borderBottomColor: '#e5e7eb',
  },
  apiHeader: {
    flex: 1,
    padding: 10,
    fontSize: 13,
    fontWeight: 'bold',
    color: '#374151',
    backgroundColor: '#f9fafb',
    textAlign: 'center',
  },
  apiCell: {
    flex: 1,
    padding: 10,
    fontSize: 12,
    color: '#6b7280',
    textAlign: 'center',
  },
  scenarioCard: {
    backgroundColor: '#fff',
    margin: 16,
    padding: 16,
    borderRadius: 12,
  },
  scenarioList: {
    gap: 8,
  },
  scenarioItem: {
    fontSize: 14,
    color: '#6b7280',
    paddingVertical: 4,
  },
  adaptCard: {
    backgroundColor: '#ecfdf5',
    margin: 16,
    marginBottom: 32,
    padding: 16,
    borderRadius: 12,
  },
  adaptTitle: {
    fontSize: 16,
    fontWeight: 'bold',
    color: '#059669',
    marginBottom: 12,
  },
  adaptList: {
    gap: 6,
  },
  adaptItem: {
    fontSize: 13,
    color: '#4b5563',
    lineHeight: 20,
  },
});

export default TreeSelectionScreen;

核心实现要点

1. 扁平化数据结构

typescript 复制代码
// 避免递归组件,使用扁平化数据
const [treeData, setTreeData] = useState([
  { id: '1', label: '根节点', level: 0, hasChildren: true, ... },
  { id: '1-1', label: '子节点1', level: 1, hasChildren: false, ... },
  { id: '1-2', label: '子节点2', level: 1, hasChildren: false, ... },
]);

2. 可见节点计算

typescript 复制代码
// 根据展开状态计算可见节点
const visibleNodes = useMemo(() => {
  const result: FlatTreeNode[] = [];
  const expandedMap = new Map(treeData.map(n => [n.id, n.expanded]));

  for (const node of treeData) {
    if (node.level > 0) {
      const parentId = node.id.substring(0, node.id.lastIndexOf('-'));
      if (!expandedMap.get(parentId)) continue;
    }
    result.push(node);
  }
  return result;
}, [treeData]);

3. OpenHarmony性能优化

优化项 说明 实现方式
数据扁平化 避免递归组件 使用level标识层级
计算缓存 减少重复计算 使用useMemo
状态批处理 减少渲染次数 合并状态更新
懒加载 按需加载子节点 动态加载数据

常见问题排查

问题 可能原因 解决方案
节点不显示 父节点未展开 检查expanded状态
选择状态错误 状态未同步 使用setState更新
滚动卡顿 节点数量过多 实现虚拟滚动
层级显示错误 level计算错误 验证ID生成逻辑

项目源码

完整项目代码:https://atomgit.com/lbbxmx111/AtomGitNewsDemo

开源鸿蒙社区:https://openharmonycrossplatform.csdn.net

相关推荐
Hcourage21 小时前
鸿蒙工程获取C/C++代码覆盖
harmonyos
二流小码农1 天前
鸿蒙开发:上传一张参考图片便可实现页面功能
android·ios·harmonyos
万少2 天前
HarmonyOS 开发必会 5 种 Builder 详解
前端·harmonyos
Huang兄2 天前
鸿蒙-List和Grid拖拽排序:仿微信小程序删除效果
harmonyos·arkts·arkui
Live000003 天前
在鸿蒙中使用 Repeat 渲染嵌套列表,修改内层列表的一个元素,页面不会更新
前端·javascript·react native
anyup3 天前
🔥2026最推荐的跨平台方案:H5/小程序/App/鸿蒙,一套代码搞定
前端·uni-app·harmonyos
Ranger09293 天前
鸿蒙开发新范式:Gpui
rust·harmonyos
Huang兄3 天前
鸿蒙-深色模式适配
harmonyos·arkts·arkui
SummerKaze5 天前
为鸿蒙开发者写一个 nvm:hmvm 的设计与实现
harmonyos
在人间耕耘7 天前
HarmonyOS Vision Kit 视觉AI实战:把官方 Demo 改造成一套能长期复用的组件库
人工智能·深度学习·harmonyos