【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

相关推荐
胖鱼罐头15 小时前
RNGH:指令式 vs JSX 形式深度对比
前端·react native
麟听科技16 小时前
HarmonyOS 6.0+ APP智能种植监测系统开发实战:农业传感器联动与AI种植指导落地
人工智能·分布式·学习·华为·harmonyos
前端不太难16 小时前
HarmonyOS PC 焦点系统重建
华为·状态模式·harmonyos
空白诗17 小时前
基础入门 Flutter for Harmony:Text 组件详解
javascript·flutter·harmonyos
lbb 小魔仙17 小时前
【HarmonyOS】React Native实战+Popover内容自适应
react native·华为·harmonyos
motosheep18 小时前
鸿蒙开发(四)播放 Lottie 动画实战(Canvas 渲染 + 资源加载踩坑总结)
华为·harmonyos
左手厨刀右手茼蒿18 小时前
Flutter for OpenHarmony 实战:Barcode — 纯 Dart 条形码与二维码生成全指南
android·flutter·ui·华为·harmonyos
lbb 小魔仙19 小时前
【HarmonyOS】React Native of HarmonyOS实战:手势组合与协同
react native·华为·harmonyos
果粒蹬i20 小时前
【HarmonyOS】React Native实战项目+NativeStack原生导航
react native·华为·harmonyos
waeng_luo20 小时前
HarmonyOS 应用开发 Skills
华为·harmonyos