React Native for Harmony 企业级折叠面板 (Accordion) 组件

目录

[一、核心知识点:折叠面板组件 完整技术体系 + 本次引入的 UI 组件说明](#一、核心知识点:折叠面板组件 完整技术体系 + 本次引入的 UI 组件说明)

[本次引入的 2 款核心 UI 组件](#本次引入的 2 款核心 UI 组件)

[1、react-native-collapsible ------ 折叠动画核心依赖](#1、react-native-collapsible —— 折叠动画核心依赖)

[2、react-native-paper ------ 鸿蒙质感 UI 美化依赖](#2、react-native-paper —— 鸿蒙质感 UI 美化依赖)

[折叠面板 (Accordion) 核心实现原理](#折叠面板 (Accordion) 核心实现原理)

二、组件封装核心设计思想

1、双层组件分层封装,职责清晰无耦合

[2、Props 属性全解耦,所有功能一键配置](#2、Props 属性全解耦,所有功能一键配置)

3、状态驱动视图更新,动画与状态同步

三、实战:企业级折叠面板组件

[四、鸿蒙端 折叠面板 高频踩坑指南](#四、鸿蒙端 折叠面板 高频踩坑指南)

[五、 扩展用法:企业级折叠面板 无缝扩展](#五、 扩展用法:企业级折叠面板 无缝扩展)

[✅ 扩展 1:自定义折叠动画时长](#✅ 扩展 1:自定义折叠动画时长)

[✅ 扩展 2:折叠面板嵌套折叠面板](#✅ 扩展 2:折叠面板嵌套折叠面板)

[✅ 扩展 3:折叠面板加载态](#✅ 扩展 3:折叠面板加载态)

[✅ 扩展 4:自定义标题栏样式](#✅ 扩展 4:自定义标题栏样式)

[✅ 扩展 5:折叠 / 展开回调事件](#✅ 扩展 5:折叠 / 展开回调事件)


一、核心知识点:折叠面板组件 完整技术体系 + 本次引入的 UI 组件说明

本次引入的 2 款核心 UI 组件

1、react-native-collapsible ------ 折叠动画核心依赖
2、react-native-paper ------ 鸿蒙质感 UI 美化依赖
javascript 复制代码
# 安装折叠动画核心依赖
npm install react-native-collapsible --save
# 安装鸿蒙质感UI组件库
npm install react-native-paper --save

折叠面板 (Accordion) 核心实现原理

折叠面板的本质是 「可切换显隐的容器 + 平滑过渡动画」 ,是 RN 开发中最基础的交互组件之一,核心逻辑只有 2 点,结合本次引入的 UI 组件后,实现逻辑更简洁,无任何复杂封装,新手也能轻松理解,也是本次组件封装的核心思路:

  1. 状态控制显隐 :通过useState定义「折叠状态」isCollapsed(布尔值:true = 折叠,false = 展开),点击面板标题栏时,执行setIsCollapsed(!isCollapsed)切换状态,这是折叠面板的核心驱动逻辑;
  2. 动画实现过渡 :将需要折叠的「内容区」包裹在<Collapse>组件中,绑定折叠状态对应的高度,由react-native-collapsible自动实现「高度渐变」的平滑动画,无需手动计算高度、无需写 Animated 动画,一行代码实现丝滑折叠效果。

二、组件封装核心设计思想

本次折叠面板组件的封装,延续了头像、宫格组件的核心设计思路,结合本次引入的 UI 组件,进一步优化了「组件分层、样式解耦、状态管理」,核心设计思想3 点,简单清晰、无任何复杂逻辑,也是鸿蒙端封装通用交互组件的最优思路,保证组件的复用性、扩展性和可维护性,你可以沿用该思路封装所有 RN 组件:

1、双层组件分层封装,职责清晰无耦合

本次封装「折叠面板父组件CollapsePanel + 折叠项子组件CollapseItem」,双层结构各司其职,完美解耦:

  • 父组件CollapsePanel:负责管理所有折叠项的状态(尤其是手风琴模式的「独占展开」逻辑)、统一配置全局样式(主题色、圆角、间距)、接收数据源,是整个折叠面板的「控制器」;
  • 子组件CollapseItem:负责单个折叠项的「标题栏渲染、折叠状态展示、内容区包裹」,内部绑定折叠动画、箭头旋转、选中高亮,是折叠面板的「展示单元」。

2、Props 属性全解耦,所有功能一键配置

声明完整的 TS 接口CollapseItemTypeCollapsePanelProps,将「面板标题、图标、角标、禁用状态、默认展开、手风琴模式、主题色」等所有功能都作为 props 参数,组件内部根据 props 自动渲染对应样式和状态,外部调用时只需传参,无需关心内部实现,一行代码即可实现任意形态的折叠面板。

3、状态驱动视图更新,动画与状态同步

所有折叠状态、选中状态均由useState管理,状态变更自动触发视图刷新和动画执行:点击标题栏→切换折叠状态→触发<Collapse>动画→同步旋转箭头→更新标题高亮样式,所有操作一气呵成,无任何手动操作 DOM,符合 React 的核心开发思想,也保证了动画的流畅性和一致性。


三、实战:企业级折叠面板组件

javascript 复制代码
import React, { useState, useRef } from 'react';
import {
  View, Text, StyleSheet, TouchableOpacity, Animated,
  SafeAreaView, LayoutAnimation, Platform, UIManager
} from 'react-native';
import Collapsible from 'react-native-collapsible';
import { Card, List, Divider } from 'react-native-paper';


export interface CollapseItemType {
  id: string | number;
  title: string;
  badge?: number | string;
  disabled?: boolean;
  content: React.ReactNode;
}
export interface CollapsePanelProps {
  data: CollapseItemType[];
  accordion?: boolean;
  activeColor?: string;
  borderRadius?: number;
  defaultOpenId?: string | number;
}

const CollapseItem: React.FC<{
  item: CollapseItemType;
  isOpen: boolean;
  activeColor: string;
  borderRadius: number;
  onPress: () => void;
}> = ({ item, isOpen, activeColor, borderRadius, onPress }) => {
  const rotateAnim = useRef(new Animated.Value(0)).current;
  
  React.useEffect(() => {
    Animated.timing(rotateAnim, {
      toValue: isOpen ? 1 : 0,
      duration: 280,
      useNativeDriver: true
    }).start();
  }, [isOpen]);

  const rotateInterpolate = rotateAnim.interpolate({
    inputRange: [0, 1],
    outputRange: ['0deg', '90deg']
  });

  const mainColor = item.disabled ? '#BFBFBF' : (isOpen ? activeColor : '#333333');
  const bgColor = item.disabled ? '#F9F9F9' : '#FFFFFF';
  const borderWidth = isOpen && !item.disabled ? 1.5 : 0;

  return (
    <Card style={[styles.cardBox, { borderRadius, marginBottom: 14, elevation: 2 }]}>
      <View style={[styles.cardInner, { borderRadius, backgroundColor: bgColor }]}>
        <TouchableOpacity
          activeOpacity={item.disabled ? 1 : 0.82}
          onPress={onPress}
          disabled={item.disabled}
          style={[styles.headerBox, { borderColor: activeColor, borderWidth }]}
        >
          <View style={styles.headerLeft}>
            <Text style={[styles.titleText, { color: mainColor }]}>{item.title}</Text>
          </View>

          <View style={styles.headerRight}>
            {/* Badge 渲染 */}
            {item.badge && (
              <View style={[styles.badgeStyle, { backgroundColor: activeColor }]}>
                <Text style={styles.badgeText}>{item.badge}</Text>
              </View>
            )}

            <Animated.View style={{ transform: [{ rotate: rotateInterpolate }], marginLeft: 6 }}>
              <Text style={{ color: mainColor, fontSize: 21, lineHeight: 21 }}>▸</Text>
            </Animated.View>
          </View>
        </TouchableOpacity>

        <Divider style={styles.dividerStyle} />

        <Collapsible collapsed={!isOpen} duration={280} style={styles.contentBox}>
          {item.content}
        </Collapsible>
      </View>
    </Card>
  );
};

const CollapsePanel: React.FC<CollapsePanelProps> = ({
  data,
  accordion = true,
  activeColor = '#007DFF',
  borderRadius = 12,
  defaultOpenId
}) => {
  const [openIds, setOpenIds] = useState<{ [key: string | number]: boolean }>(() => {
    const initState: { [key: string | number]: boolean } = {};
    data.forEach(item => initState[item.id] = item.id === defaultOpenId);
    return initState;
  });

  const handleClick = (id: string | number) => {
    const target = data.find(item => item.id === id);
    if (target?.disabled) return;

    LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
    
    setOpenIds(prev => {
      const newState = { ...prev };
      if (accordion) {
        Object.keys(newState).forEach(key => newState[key] = false);
        newState[id] = !prev[id];
      } else {
        newState[id] = !prev[id];
      }
      return newState;
    });
  };

  return (
    <View style={styles.panelWrap}>
      {data.map(item => (
        <CollapseItem
          key={item.id}
          item={item}
          isOpen={openIds[item.id] || false}
          activeColor={activeColor}
          borderRadius={borderRadius}
          onPress={() => handleClick(item.id)}
        />
      ))}
    </View>
  );
};

const App = () => {
  const settingData = [
    {
      id: 1,
      title: '账户安全',
      content: (
        <View style={styles.contentWrap}>
          <List.Item title="修改登录密码" titleStyle={styles.contentText} />
          <List.Item title="绑定手机号" titleStyle={styles.contentText} />
          <List.Item title="设置支付密码" titleStyle={styles.contentText} />
        </View>
      )
    },
    {
      id: 2,
      title: '消息通知',
      badge: 3,
      content: (
        <View style={styles.contentWrap}>
          <List.Item title="系统通知提醒" titleStyle={styles.contentText} />
          <List.Item title="订单状态推送" titleStyle={styles.contentText} />
          <List.Item title="优惠活动提醒" titleStyle={styles.contentText} />
        </View>
      )
    },
    {
      id: 3,
      title: '隐私设置',
      content: (
        <View style={styles.contentWrap}>
          <List.Item title="授权管理" titleStyle={styles.contentText} />
          <List.Item title="个人信息可见性" titleStyle={styles.contentText} />
          <List.Item title="清除缓存" titleStyle={styles.contentText} />
        </View>
      )
    },
    {
      id: 4,
      title: '关于应用',
      disabled: true,
      content: (
        <View style={styles.contentWrap}>
          <Text style={styles.contentText}>版本号:v2.0.0 鸿蒙适配版</Text>
          <Text style={styles.contentText}>更新时间:2026.01.13</Text>
        </View>
      )
    }
  ];

  const orderData = [
    {
      id: 1,
      title: '待付款订单',
      badge: 2,
      content: (
        <View style={styles.orderWrap}>
          <Text style={styles.orderText}>订单编号:20260113001</Text>
          <Text style={styles.orderText}>金额:¥ 99.00</Text>
          <Text style={[styles.orderText, { color: '#FF4D4F' }]}>倒计时:00:28:15</Text>
        </View>
      )
    },
    {
      id: 2,
      title: '待发货订单',
      badge: 1,
      content: (
        <View style={styles.orderWrap}>
          <Text style={styles.orderText}>订单编号:20260112002</Text>
          <Text style={styles.orderText}>金额:¥ 159.00</Text>
          <Text style={[styles.orderText, { color: '#FF9500' }]}>今日18:00前发货</Text>
        </View>
      )
    },
    {
      id: 3,
      title: '待收货订单',
      content: (
        <View style={styles.orderWrap}>
          <Text style={styles.orderText}>订单编号:20260110003</Text>
          <Text style={styles.orderText}>金额:¥ 229.00</Text>
          <Text style={[styles.orderText, { color: '#00C48C' }]}>已发货 · 运输中</Text>
        </View>
      )
    }
  ];

  return (
    <SafeAreaView style={styles.pageContainer}>
      <Text style={styles.pageTitle}>账户设置 (手风琴模式 · 独占展开)</Text>
      <CollapsePanel 
        data={settingData} 
        accordion={true} 
        defaultOpenId={1}
        activeColor="#007DFF" 
        borderRadius={12}
      />

      <Text style={styles.pageTitle}>我的订单 (独立模式 · 自由展开)</Text>
      <CollapsePanel 
        data={orderData} 
        accordion={false}
        activeColor="#FF7D00" 
        borderRadius={12}
      />
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  pageContainer: {
    flex: 1,
    backgroundColor: '#F5F7FA',
    paddingHorizontal: 16,
    paddingTop: 22,
  },
  pageTitle: {
    fontSize: 19,
    color: '#1D2129',
    fontWeight: '600',
    marginBottom: 16,
    marginTop: 20,
  },
  panelWrap: {
    width: '100%',
  },
  cardBox: {
    backgroundColor: '#FFFFFF',
    overflow: 'visible',
  },
  cardInner: {
    overflow: 'hidden',
    borderRadius: 12,
  },
  headerBox: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    padding: 18,
    borderRadius: 12,
  },
  headerLeft: {
    flexDirection: 'row',
    alignItems: 'center',
  },
  titleText: {
    fontSize: 17,
    fontWeight: '500',
  },
  headerRight: {
    flexDirection: 'row',
    alignItems: 'center',
  },
  badgeStyle: {
    paddingHorizontal: 5,
    paddingVertical: 1,
    borderRadius: 999,
    marginRight: 6,
    justifyContent: 'center',
    alignItems: 'center',
    minWidth: 20,
  },
  badgeText: {
    fontSize: 11,
    color: '#FFFFFF',
    fontWeight: '500',
    textAlign: 'center',
  },
  dividerStyle: {
    backgroundColor: '#F2F3F5',
    marginHorizontal: 18,
  },
  contentBox: {
    padding: 18,
  },
  contentWrap: {
    gap: 12,
  },
  contentText: {
    fontSize: 15,
    color: '#666C74',
    lineHeight: 24,
  },
  orderWrap: {
    gap: 10,
  },
  orderText: {
    fontSize: 15,
    color: '#666C74',
    lineHeight: 22,
  },
});

export default App;

四、鸿蒙端 折叠面板 高频踩坑指南

  1. 问题 :安装react-native-collapsible后报错 Cannot find module 'react-native-collapsible'✅ 根源:依赖未安装成功 / 项目未重启,RN 未识别新安装的依赖✅ 解决方案:重新执行npm install react-native-collapsible --save,然后重启 RN 项目(必做),即可解决。

  2. 问题 :TS 报错 Property 'source' does not exist on type 'IconProps'✅ 根源:react-native-paper的 Icon 组件 source 属性类型未声明✅ 解决方案:本次代码中已使用兼容写法,直接传字符串图标名(如source="chevron-right"),无需传组件,杜绝类型报错。

  3. 问题 :手风琴模式下,点击面板无法「独占展开」,多个面板同时展开✅ 根源:未在父组件中实现「关闭所有→打开当前」的逻辑✅ 解决方案:本次代码中已封装手风琴核心逻辑,通过遍历openIds关闭所有面板,再打开当前,完美解决(代码已实现)。


五、 扩展用法:企业级折叠面板 无缝扩展

本次封装的折叠面板组件是「无耦合设计」,基于基础代码可快速扩展鸿蒙端常用的进阶功能,无需修改核心组件代码,只需新增 props 和样式即可,全部是企业级开发刚需,无缝集成、一行代码扩展,满足更多业务场景,实用性拉满,也是你后续开发中一定会用到的功能:

✅ 扩展 1:自定义折叠动画时长

<Collapse>组件添加duration属性,如duration={500},即可自定义折叠 / 展开的动画时长,数值越大动画越慢,鸿蒙端支持任意时长配置。

✅ 扩展 2:折叠面板嵌套折叠面板

item.content中再次引入<CollapsePanel>组件,即可实现「折叠面板嵌套」,适用于多级分类、多级设置等场景,鸿蒙 APP 标配功能,无兼容问题。

✅ 扩展 3:折叠面板加载态

添加loading属性,面板加载时显示骨架屏占位,加载完成后显示内容,适用于异步请求数据的场景,提升用户体验。

✅ 扩展 4:自定义标题栏样式

CollapseItem的标题栏中添加图片、开关、按钮等组件,即可实现自定义标题栏,如「带开关的折叠面板」「带按钮的折叠面板」,无任何内容限制。

✅ 扩展 5:折叠 / 展开回调事件

在父组件中新增onCollapseonExpand回调 props,在状态切换时执行,可用于埋点统计、请求数据、页面刷新等业务逻辑。


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

相关推荐
Larry_Yanan2 小时前
Qt安卓开发(三)双摄像头内嵌布局
android·开发语言·c++·qt·ui
2501_948195342 小时前
RN for OpenHarmony英雄联盟助手App实战:关于实现
javascript·react native·react.js
旭日猎鹰3 小时前
鸿蒙环境添加React Native的bundle包
react native·react.js·harmonyos
我最厉害。,。3 小时前
内网对抗-横向移动篇&入口差异&切换上线&IPC管道&AT&SC任务&Impacket套件&UI插件
ui
不爱吃糖的程序媛4 小时前
OpenHarmony 平台 C/C++ 三方库移植实战指南
react native·react.js·harmonyos
2501_948122634 小时前
React Native for OpenHarmony 实战:Steam 资讯 App 隐私政策实现
javascript·react native·react.js·游戏·ecmascript·harmonyos
2501_948122634 小时前
React Native for OpenHarmony 实战:Steam 资讯 App 主题设置实现
javascript·react native·react.js·游戏·ecmascript·harmonyos
晴栀ay5 小时前
JS的超集——TypeScript
前端·react.js·typescript
yyyao5 小时前
🔥🔥🔥 React18 源码学习 - Render 阶段(构造 Fiber 树)
react.js·源码阅读