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结构 |
| 动画支持 | 流畅 | 需谨慎使用 | 减少动画复杂度 |
| 内存管理 | 宽松 | 较严格 | 及时释放资源 |
性能优化策略
- 虚拟化渲染:只渲染可见区域的节点
- 扁平化数据:避免递归组件带来的性能问题
- 状态批处理:合并多个状态更新操作
- 懒加载:延迟加载子节点数据
完整实现代码
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生成逻辑 |