- UI组件
- 移动开发
- 跨平台
【免费下载链接】gluestack-ui
React & React Native Components & Patterns (copy-paste components & patterns crafted with Tailwind CSS (NativeWind))
本文以 gluestack-ui 仓库中 Accordion 组件的动画迁移总结为基础,完整剖析它如何从旧版 React NativeAnimatedAPI 平滑迁移到 React Native Reanimated,实现 UI 线程驱动、可配置、向后兼容的 60fps 展开/折叠动画。你将掌握新的文件架构(AnimatedHeight、AnimatedIcon、animation-config)、自动图标旋转的用法、三级自定义方式(配置参数 / 直接改组件 / 深入核心),以及这一模式如何推广到 Drawer、Modal 等其它组件。
迁移背景:为什么要抛弃旧的 Animated API
在本次迁移之前,Accordion 的展开/折叠动画由核心包内的旧实现驱动。仓库中保留了迁移前的工作版本,位于 packages/gluestack-core/src/accordion/creator/AnimatedHeight.tsx,从中可以清楚看到旧方案的四个硬伤:
Animated.timing(opacityValue, { toValue: hide ? 0 : 1, duration: 200, // ① 时长硬编码在核心包里,用户无法修改 useNativeDriver: false, // ② 走 JS 驱动,动画发生在 JS 线程 }).start(); Animated.timing(heightValue, { toValue: hide ? 0 : 1, duration: 200, useNativeDriver: false, }).start();旧实现的问题可以归纳为:
- 使用已标记废弃的 React Native
AnimatedAPI,且useNativeDriver: false,动画完全运行在JS 线程; - 200ms 动画时长硬编码在核心包源码中,使用者无法调整;
- 没有任何图标旋转动画——展开/收起时箭头图标需要开发者手动在
ChevronUpIcon/ChevronDownIcon之间切换; - 零可定制性:动画逻辑锁死在核心包内部,想改缓动曲线或时长只能 fork 核心包。
由于 JS 线程同时要处理 API 请求、导航、列表渲染等任务,旧方案在负载较重时容易出现丢帧、卡顿。
新方案概览:Reanimated + UI 线程 + 可配置
迁移完成后的 Accordion 实现了三个核心目标,与之对应的是三个新文件(详见 apps/starter-kit-monorepo/packages/ui/components/ui/accordion):
accordion/ ├── AnimatedHeight.tsx # Reanimated 实现的内容高度动画 ├── AnimatedIcon.tsx # Reanimated 实现的图标旋转动画 ├── animation-config.ts # 用户可直接修改的动画配置(单一配置源) ├── ANIMATION_CUSTOMIZATION.md # 动画自定义指南 ├── USAGE_EXAMPLE.tsx # 新旧用法对比示例 ├── MIGRATION_SUMMARY.md # 本文所依据的迁移总结文档 └── index.tsx # 组件主入口,接入以上动画组件新方案的关键改进:
- 采用React Native Reanimated(当前仓库中使用的版本为
react-native-reanimated@~4.2.1,配套react-native-worklets@0.7.1,见 apps/starter-kit-monorepo/apps/expo/package.json); - 动画通过worklet 运行在 UI 线程,不阻塞 JS 线程;
- 动画时长与旋转角度全部可配置,默认值在配置文件 animation-config.ts 中统一管理;
- 自动图标旋转:展开时图标自动旋转 180°,无需再手动切换图标;
- 动画逻辑从核心包下沉到style 文件(样式层),用户改样式文件即可定制,无需 fork 核心包。
从源码结构看,这套迁移方案不止存在于 monorepo:src/components/ui/accordion与apps/starter-kit-expo/components/ui/accordion中同样保留了AnimatedHeight.tsx、AnimatedIcon.tsx、animation-config.ts等同构实现,说明该模式已在多个 starter-kit 中落地复用。
架构剖析:状态流与三个关键组件
动画运行链路
整个动画流程从一次点击开始(结合 apps/starter-kit-monorepo/packages/ui/components/ui/accordion/index.tsx 与核心包的AccordionTrigger实现可以还原完整调用链):
- 用户点击
AccordionTrigger,触发核心层 useAccordionItem 返回的toggle,进而调用 useAccordion 中的toggleItem更新选中状态(支持single/multiple/isCollapsible组合逻辑); AccordionItemContext中的isExpanded状态随之更新(该 Context 由核心包导出,见 Context.tsx 与 index.tsx 中的export { AccordionContext, AccordionItemContext });- 样式层
AccordionContent通过React.useContext(AccordionItemContext)拿到isExpanded,传给AnimatedHeight; AnimatedHeight通过onLayout测量内容真实高度,写入 shared value;- Reanimated 在UI 线程上以 worklet 驱动高度从 0 过渡到测量值;
- 内容平滑展开/收起的同时,
AnimatedIcon并行执行图标旋转(同样在 UI 线程)。
AnimatedHeight:测量 + 高度动画
AnimatedHeight.tsx 的完整实现:
import Animated, { useSharedValue, useAnimatedStyle, withTiming, } from 'react-native-reanimated'; export const AnimatedHeight: React.FC<AnimatedHeightProps> = ({ isExpanded, children, duration = 300, // 组件层默认 300ms style, }) => { const measuredHeight = useSharedValue(0); const progress = useSharedValue(isExpanded ? 1 : 0); const onLayout = (event: LayoutChangeEvent) => { const height = event.nativeEvent.layout.height; if (height > 0 && measuredHeight.value !== height) { measuredHeight.value = height; } }; React.useEffect(() => { progress.value = withTiming(isExpanded ? 1 : 0, { duration }); }, [isExpanded, duration]); const animatedStyle = useAnimatedStyle(() => { const height = progress.value * measuredHeight.value; return { height, opacity: progress.value, overflow: 'hidden', }; }, [progress, measuredHeight]); return ( <Animated.View style={[animatedStyle, style]}> <Animated.View onLayout={onLayout} style={{ position: 'absolute', width: '100%' }} > {children} </Animated.View> </Animated.View> ); };实现要点:
- 高度来源:内部子层
Animated.View使用position: 'absolute'常驻布局,通过onLayout把真实内容高度写入measuredHeight共享值,避免对未知高度内容的硬编码; - 动画驱动:
progress共享值在useEffect中用withTiming驱动到 0/1,useAnimatedStyle在 UI 线程计算height = progress * measuredHeight,同时联动opacity与overflow: 'hidden'; - 可配置时长:
duration作为 props 传入,由样式层从配置文件中取值下发。
AnimatedIcon:插值旋转
AnimatedIcon.tsx 使用interpolate实现平滑旋转:
const animatedValue = useSharedValue(0); useEffect(() => { animatedValue.value = withTiming(isExpanded ? 1 : 0, { duration }); }, [isExpanded, duration]); const animatedStyle = useAnimatedStyle(() => { const rotate = interpolate(animatedValue.value, [0, 1], [0, rotation]); return { transform: [{ rotate: `${rotate}deg` }], }; }, [rotation, animatedValue]);核心逻辑:展开时animatedValue从 0 平滑过渡到 1,interpolate将其映射为从 0° 到可配置角度(默认 180°)的旋转。旋转与内容展开使用独立的动画时长,二者并行互不干扰。
样式层如何接入动画
在 index.tsx 中,AccordionContent与AccordionIcon分别被动画组件包裹,时长与角度统一取自配置:
const AccordionContent = React.forwardRef(...function AccordionContent({ className, ...props }, ref) { const { isExpanded } = React.useContext(AccordionItemContext); return ( <AnimatedHeight isExpanded={isExpanded} duration={accordionAnimationConfig.contentDuration}> <UIAccordion.Content ... /> </AnimatedHeight> ); }); const AccordionIcon = React.forwardRef(...function AccordionIcon({ className, ...props }, ref) { const { isExpanded } = React.useContext(AccordionItemContext); return ( <AnimatedIcon isExpanded={isExpanded} duration={accordionAnimationConfig.iconDuration} rotation={accordionAnimationConfig.iconRotation} > <UIAccordion.Icon ... /> </AnimatedIcon> ); });同时,核心包的AccordionContent(见 packages/gluestack-core/src/accordion/creator/AccordionContent.tsx)已删除旧的AnimatedHeight依赖,注释明确说明"动画现在由样式层处理",核心只负责渲染与无障碍语义(regionProps、web 端折叠内容的inert/aria-hidden处理)。这就是文档强调的"动画逻辑属于样式文件,而不是核心"。
新特性一:自动图标旋转
迁移前,展开状态的箭头图标需要手动切换:
<AccordionTrigger> {({ isExpanded }) => ( <> <AccordionTitleText>Question?</AccordionTitleText> {isExpanded ? ( <AccordionIcon as={ChevronUpIcon} /> // 手动切换 ) : ( <AccordionIcon as={ChevronDownIcon} /> )} </> )} </AccordionTrigger>迁移后,只需一个图标,旋转由AnimatedIcon自动完成:
<AccordionTrigger> <AccordionTitleText>Question?</AccordionTitleText> <AccordionIcon as={ChevronDownIcon} /> {/* 自动旋转 180° */} </AccordionTrigger>完整的可用示例见 USAGE_EXAMPLE.tsx,其中包含:
BasicAccordionExample:基础用法,演示type="single"+isCollapsible与自动旋转图标;OldApproach/NewApproach:新旧写法对比;ManualControlExample:如果你仍然想用 render prop 控制标题文案,图标旋转依然会自动生效,二者可以并存;MultipleAccordionsExample:type="multiple"多展开模式。
图标组件采用as属性注入(示例中来自lucide-react-native),配合cssInterop(PrimitiveIcon, ...)映射 className 样式,保持 gluestack-ui 一贯的图标使用方式。
新特性二:动画配置自定义(推荐方式)
所有动画参数集中在 animation-config.ts,是"单一配置源":
export const accordionAnimationConfig = { /** * 内容展开/收起动画时长(毫秒) * @default 300 */ contentDuration: 200, /** * 图标旋转动画时长(毫秒) * @default 300 */ iconDuration: 200, /** * 展开时图标的旋转角度(度) * @default 180(倒转) */ iconRotation: 180, }; export type AccordionAnimationConfig = typeof accordionAnimationConfig;注意:文档注释中的默认值是 300ms,而当前仓库的实际取值是contentDuration: 200、iconDuration: 200(即生效时长以仓库现状为准)。AnimatedHeight/AnimatedIcon的 props 默认值同样为 300,因此最终生效时长取决于样式层传入的配置值。文件还导出了AccordionAnimationConfig类型,自定义时可直接复用保证类型安全。
按 ANIMATION_CUSTOMIZATION.md 的说明,常见自定义场景:
| 场景 | 配置 | 效果 |
|---|---|---|
| 更快动画 | contentDuration: 150, iconDuration: 150 | 展开收起与旋转都更干脆 |
| 更慢更有戏剧性 | contentDuration: 500, iconDuration: 400 | 动画更明显、更柔和 |
| 四分之一转 | iconRotation: 90 | 图标只转 90° 而非倒转 |
| 关闭旋转 | iconDuration: 0, iconRotation: 0 | 无旋转动画(立即到位) |
新特性三:高级自定义(直接修改动画组件)
如果预设参数无法满足需求,可以直接修改AnimatedHeight.tsx与AnimatedIcon.tsx,实现:
- 自定义缓动曲线:例如给
withTiming传入Easing.bezier(0.25, 0.1, 0.25, 1); - 弹簧物理:将
withTiming替换为withSpring(如withSpring(measuredHeight.value, { damping: 15, stiffness: 150 })); - 其它动画类型与复杂编排:所有标准 Reanimated API 均可用(
withSpring、withSequence、withRepeat、interpolate等)。
这正是文档所述"样式文件内即插即改、无需 fork"的设计理念。需注意 ANIMATION_CUSTOMIZATION.md 同时指出:这些动画针对原生平台优化,在 Web 端会退化为基于 JS 的动画实现(与 Reanimated 的 Web 支持策略一致)。
性能对比与原理
迁移总结文档记录了旧新实现的性能对比:
| 场景 | 旧(Animated API) | 新(Reanimated) | 改善 |
|---|---|---|---|
| 常规使用 | 50-60 fps | 60 fps | 稳定 |
| 数据请求期间 | 30-40 fps | 60 fps | 2 倍流畅 |
| 导航切换期间 | 25-35 fps | 60 fps | 2.4 倍流畅 |
| 低端设备 | 20-30 fps | 60 fps | 2-3 倍流畅 |
(上表为迁移总结文档记录的实测结果。)性能提升的根本原因在于动画执行线程的变化:
旧(Animated API): Animation → JS 线程 → Bridge → UI 线程 → 屏幕 ↑ 会被 API 调用、导航等 JS 任务阻塞 新(Reanimated): Animation → UI 线程 → 屏幕 ↑ 与 JS 工作相互独立Reanimated 通过 worklet 将动画计算搬到 UI 线程,动画进度不再受 JS 线程繁忙程度影响,因此即使在数据拉取、列表渲染、页面导航等场景下也能保持流畅。
迁移模式推广:如何改造其它组件
这份迁移不只是修好一个 Accordion,更重要的是确立了一套可复用的迁移套路(文档称之为 Migration Path)。推荐优先级:
- Drawer:类似的滑动动画,可叠加手势驱动;
- Modal:淡入/缩放动画;
- Actionsheet:底部滑入 + 手势;
- AlertDialog:与 Modal 类似;
- Menu / Popover:简单淡入/缩放。
迁移的标准流程:
1. 在样式文件(style file)中用 Reanimated 创建 AnimatedXXX 组件 2. 创建 animation-config.ts 提供可定制参数 3. 更新组件 index.tsx 使用动画包裹层 4. 从核心包移除动画逻辑(保持核心简单、框架无关) 5. 从核心包导出所需 Context(如 AccordionItemContext) 6. 编写自定义文档从仓库源码看,这套模式确实已经在推进:apps/starter-kit-monorepo/packages/ui/components/ui下的drawer、modal、popover、alert-dialog、menu、toast等组件均已引入react-native-reanimated(其中 drawer 通过Animated.createAnimatedComponent(Pressable/View)构建动画底座),Accordion 是这一迁移浪潮的先例与模板。
兼容性:无破坏性变更
迁移总结明确给出结论:Breaking Changes: None,这是一次完全向后兼容的改进:
- 现有 Accordion 代码无需任何改动即可继续工作;
- 公开 API(
Accordion、AccordionItem、AccordionHeader、AccordionTrigger、AccordionTitleText、AccordionIcon、AccordionContent、AccordionContentText)保持不变; - 自动图标旋转作为附加新功能默认生效;
- 如果开发者偏好旧的手动图标切换写法,依然可用(见 USAGE_EXAMPLE.tsx 的
OldApproach示例)。
工程落地前提:依赖与构建配置
要在自己的项目中复刻这套方案,需要满足的工程前提(以 monorepo 的 expo app 为例,见 apps/starter-kit-monorepo/apps/expo/package.json):
- 安装
react-native-reanimated(仓库使用~4.2.1)与react-native-worklets(0.7.1); - 在 Babel 配置中加入 Reanimated 插件(见 apps/starter-kit-monorepo/apps/expo/babel.config.js):
module.exports = function (api) { api.cache(true); return { presets: [['babel-preset-expo'], 'nativewind/babel'], plugins: [ ['module-resolver', { root: ['./'], alias: { '@': './', 'tailwind.config': './tailwind.config.js' } }], 'react-native-reanimated/plugin', // ← Reanimated 的 Babel 插件 ], }; };- 样式层依赖
@gluestack-ui/core、@gluestack-ui/utils、nativewind、react、react-native(peerDependencies 见 apps/starter-kit-monorepo/packages/ui/package.json),并通过 package.json 的exports字段暴露./ui/accordion入口。
迁移确立的四条核心原则
- 动画逻辑属于样式文件,而非核心包:用户无需 fork 即可定制,核心保持简洁与框架无关;
- 默认即可定制:通过
animation-config.ts覆盖常见需求,通过直接修改组件支持高级需求; - 用 Reanimated 换取原生性能:UI 线程动画保障 60fps,与原生优先的组件库竞争;
- 充分文档化:配套自定义指南、用法示例与迁移总结。
总结
本次 Accordion 迁移为 gluestack-ui 确立了"Reanimated + 样式层动画 + 可配置"的组件动画范式。最终成果可以概括为:性能更优(UI 线程 60fps)、技术更现代(Reanimated 取代废弃的 Animated API)、完全可定制(配置参数到组件源码逐级可改)、文档完备(指南/示例/总结三件套)、向后兼容(API 不变、旧代码零改动),并为后续所有带动画的组件(Drawer、Modal、Actionsheet、AlertDialog、Menu/Popover)铺平了迁移道路。开发者如需继续深入,可查阅 ANIMATION_CUSTOMIZATION.md 与 USAGE_EXAMPLE.tsx 获取实操示例。
- UI组件
- 移动开发
- 跨平台
【免费下载链接】gluestack-ui
React & React Native Components & Patterns (copy-paste components & patterns crafted with Tailwind CSS (NativeWind))
相关推荐
gluestack-ui Accordion 组件 Reanimated 迁移实战:从旧 Animated API 到 UI 线程 60fps 动画
gluestack ui Accordion 组件 Reanimated 迁移实战:从旧 Animated API 到 UI 线程 60fps 动画 Accor
UI组件移动开发跨平台gluestack-ui Accordion 动画定制指南:基于 React Native Reanimated 的 UI 线程高性能实现
gluestack ui Accordion 动画定制指南:基于 React Native Reanimated 的 UI 线程高性能实现 本指南围绕 glue
UI组件移动开发跨平台gluestack-ui Accordion 动画定制指南:基于 React Native Reanimated 的高性能折叠动画
gluestack ui Accordion 动画定制指南:基于 React Native Reanimated 的高性能折叠动画 本篇技术指南围绕 glues
UI组件移动开发跨平台
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考