ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

React Native Reanimated 手风琴(Accordion)实现:基于共享值与 withTiming 的平滑展开收起动画

React Native Reanimated 手风琴(Accordion)实现:基于共享值与 withTiming 的平滑展开收起动画 React Native Reanimated 手风琴Accordion实现基于共享值与 withTiming 的平滑展开收起动画【免费下载链接】react-native-reanimatedReact Natives Animated library reimplemented项目地址: https://gitcode.com/GitHub_Trending/re/react-native-reanimated导读手风琴Accordion是一种允许用户通过平滑动画显示/隐藏内容块的基础交互组件常见于 FAQ 区块、折叠面板与设置菜单。本文基于 react-native-reanimated 官方博客示例完整讲解如何利用共享值Shared Value、派生值useDerivedValue与withTiming在 UI 线程上实现高性能的展开/收起动画——全程不触发 React 重渲染无论内容多复杂都能保持 60fps 流畅度。读完本文你将掌握手风琴的完整可运行实现、每个 API 的底层原理以及向多项目、弹性动画等方向扩展的实战技巧。手风琴组件的核心设计思路官方文档对它的定义是An accordion allows to show and hide a piece of content with a smooth animation. Commonly used in FAQ sections.手风琴允许以平滑动画显示和隐藏一块内容常见于 FAQ 区块。实现手风琴最常见、也最容易踩坑的做法是用 React state 保存展开状态再通过条件渲染或LayoutAnimation切换内容。它的问题是每次状态变化都会触发组件重渲染展开高度动画难以拿到内容块的真实尺寸动画过程依赖 JS 线程逐帧驱动容易掉帧。官方示例给出的解决方案是**基于共享值shared values**实现。共享值是 Reanimated 中驱动一切动画的核心数据载体通过 useSharedValue 定义通过.value属性读写数据会在 JavaScript 线程与 UI 线程 之间自动同步。关键在于修改.value不会触发 React 组件重渲染动画逻辑完全跑在 UI 线程上因此既省去了重渲染开销又能保证动画的流畅性。官方文档在介绍中特别强调了这一点Leveraging shared values helps to prevent unnecessary re-renders. We define shared values using theuseSharedValuehook.利用共享值有助于避免不必要的重渲染。我们通过useSharedValue钩子定义共享值。完整示例源码官方示例位于 docs/docs-reanimated/static/examples/Accordion.js交互演示由InteractiveExample组件加载对应文档中的samp idAccordion占位并在 docs/docs-reanimated/static/recordings/examples/ 目录下提供 Androidaccordion_android.mov与 iOSaccordion_ios.mov两段真实运行录屏。下面是完整实现import React from react; import { StyleSheet, View, Button, SafeAreaView } from react-native; import Animated, { useAnimatedStyle, useDerivedValue, useSharedValue, withTiming, } from react-native-reanimated; function AccordionItem({ isExpanded, children, viewKey, style, duration 500, }) { const height useSharedValue(0); const derivedHeight useDerivedValue(() withTiming(height.value * Number(isExpanded.value), { duration, }) ); const bodyStyle useAnimatedStyle(() ({ height: derivedHeight.value, })); return ( Animated.View key{accordionItem_${viewKey}} style{[styles.animatedView, bodyStyle, style]} View onLayout{(e) { height.value e.nativeEvent.layout.height; }} style{styles.wrapper} {children} /View /Animated.View ); } function Item() { return View style{styles.box} /; } function Parent({ open }) { return ( View style{styles.parent} AccordionItem isExpanded{open} viewKeyAccordion Item / /AccordionItem /View ); } export default function App() { const open useSharedValue(false); const onPress () { open.value !open.value; }; return ( SafeAreaView style{styles.container} View style{styles.buttonContainer} Button onPress{onPress} titleClick me / /View View style{styles.content} Parent open{open} / /View /SafeAreaView ); } const styles StyleSheet.create({ container: { flex: 1, justifyContent: center, paddingTop: 24, }, buttonContainer: { flex: 1, paddingBottom: 1rem, display: flex, flexDirection: row, justifyContent: center, alignItems: center, }, content: { flex: 1, justifyContent: center, alignItems: center, }, parent: { width: 200, }, wrapper: { width: 100%, position: absolute, display: flex, alignItems: center, }, animatedView: { width: 100%, overflow: hidden, }, box: { height: 120, width: 120, color: #f8f9ff, backgroundColor: #b58df1, borderRadius: 20, }, });这段代码是整篇教程的主体外层AccordionItem负责动画children则是任意内容。官方文档用CollapsibleCode src{AccordionSrc} showLines{[9,41]}/折叠展示了第 941 行的核心组件下面逐段拆解。AccordionItem封装每个手风琴条目官方文档对该组件的描述是TheAccordionItemcomponent encapsulates each item in the accordion. Aheightshared value manages the height of the item. The height dynamically adjusts based on theisExpandedprop, resulting in smooth expansion and collapse animations. Thedurationprop controls the duration of the animation.AccordionItem封装了手风琴中的每一个条目其核心是一个名为height的共享值它负责管理条目的高度。高度会根据isExpandedprop 动态调整从而产生平滑的展开与收起动画durationprop 则控制动画的时长。1. 共享值 height内容高度的测量缓存const height useSharedValue(0);height初始为 0它的真实值来自内部内容 View 的onLayout回调View onLayout{(e) { height.value e.nativeEvent.layout.height; }} style{styles.wrapper} {children} /View这里运用了一个经典技巧先让内容以自然高度完成布局再把测得的真实高度写入共享值。onLayout返回的e.nativeEvent.layout.height是内容块实际渲染后的高度因此无论children是单行文本、图片还是复杂的嵌套列表都能被准确测量无需硬编码任何魔法数字。从 useSharedValue 的源码可以看到它的实现本质export function useSharedValueValue( initialValue: Value | (() Value) ): SharedValueValue { const [mutable] useState(() { const value typeof initialValue function ? (initialValue as () Value)() : initialValue; return makeMutable(value); }); useEffect(() { return () { cancelAnimation(mutable); }; }, [mutable]); return mutable; }底层通过makeMutable创建一个可变的共享值对象useState保证整个组件生命周期内该对象只创建一次卸载时自动调用cancelAnimation取消未完成的动画避免内存泄漏读写走的是.value属性修改它不会触发 React 重渲染——这正是手风琴高性能的根基。2. useDerivedValue withTiming派生高度动画const derivedHeight useDerivedValue(() withTiming(height.value * Number(isExpanded.value), { duration, }) );useDerivedValue用于基于现有共享值创建新的、保持响应式的共享值。它的 updater 函数会在其依赖的共享值这里是height与isExpanded发生变化时自动重新执行返回的derivedHeight是一个只读的派生共享值类型定义见 useDerivedValue.mdxDerivedValueT extends ReadonlyOmitSharedValueT, set。动画目标值height.value * Number(isExpanded.value)是这段代码最精妙的地方当isExpanded.value true时Number(true)为1目标高度就是完整的内容高度 → 动画展开当isExpanded.value false时Number(false)为0目标高度变为0→ 动画收起因为derivedHeight对isExpanded是响应式的每次点击按钮翻转布尔值都会自动触发一次新的withTiming动画。withTiming是基于时长与缓动函数easing的动画函数也是 Reanimated 三大内置动画函数之一另外两个是withSpring与withDecay。其类型签名与配置项详见 withTiming.mdxinterface WithTimingConfig { duration?: number; // 动画时长毫秒默认 300 easing?: EasingFunction; // 缓动曲线默认 Easing.inOut(Easing.quad) reduceMotion?: ReduceMotion; // 响应系统减弱动态效果设置默认 ReduceMotion.System } function withTimingT extends AnimatableValue( toValue: T, config?: WithTimingConfig, callback?: (finished?: boolean, current?: AnimatableValue) void ): T;官方示例为duration设置了默认值 500msduration 500写在函数参数解构中并在调用处覆盖为任意自定义时长。从 timing.ts 的源码可以看到withTiming的内部行为默认duration: 300、默认缓动Easing.inOut(Easing.quad)动画逐帧推进时onFrame回调根据(now - startTime) / config.duration计算进度再用 easing 函数映射出当前值const runtime now - startTime; if (runtime config.duration) { animation.startTime 0; animation.current toValue; return true; // 动画结束 } const progress animation.easing(runtime / config.duration); animation.current (startValue as number) (toValue - (startValue as number)) * progress; return false;值得注意的是onStart中的连续性处理如果新动画与上一个动画同为timing类型且目标值相同会复制上一段动画的时间线startTime、startValue从而保证快速连续点击时动画不跳变、衔接自然。另外withTiming是惰性声明的它只是返回一个动画对象真正的逐帧驱动发生在它被写入共享值之后由 Reanimated 运行时接管这也是它必须跑在 workletUI 线程中的原因。3. useAnimatedStyle把派生高度绑定到样式const bodyStyle useAnimatedStyle(() ({ height: derivedHeight.value, }));useAnimatedStyle创建一个可用共享值驱动的样式对象它必须传给Animated 组件的style属性详见 useAnimatedStyle.mdx。其 updater 回调会被自动 worklet 化并在 UI 线程上执行——这意味着derivedHeight.value的每一次变化都会在原生侧直接更新height样式整个过程不经过 React 协调器也不会触发任何一次 JS 重渲染。最终样式通过数组语法合并进Animated.ViewAnimated.View key{accordionItem_${viewKey}} style{[styles.animatedView, bodyStyle, style]}这里有三个必须注意的细节key使用viewKey保证每个条目在列表中的身份稳定styles.animatedView中设置了overflow: hidden让内容在收起阶段被裁剪而不是溢出遮挡外层的styleprop 允许调用方追加自定义样式增强了组件的复用性。bodyStyle只负责动态的height静态样式width、overflow等放在StyleSheet中——官方文档明确建议只把动态部分交给useAnimatedStyle静态部分保留在StyleSheet中以此避免无谓的样式重算。为什么这套方案能避免重渲染把整条链路串起来看点击按钮 → 修改open.value共享值useDerivedValue的 updater 感知到isExpanded变化产出新的withTiming动画目标值Reanimated 运行时在 UI 线程逐帧推进动画更新derivedHeight.valueuseAnimatedStyle的 mapperstartMapper见 useAnimatedStyle.ts把新高度直接写入原生视图样式。整个过程中 React 组件的 render 一次都不会触发状态变更全部发生在共享值层。对比用 React state 保存展开状态的做法——后者每次都要走 setState → render → diff → commit 的完整流程内容越复杂开销越大——这套方案在信息密度上有着本质优势。内容区 children任意内容皆可容纳官方文档特别指出Inside theAccordionItem, the children represent the content section. It can accommodate various types of content.children就是内容区可以是任意类型的 React Native 内容文本、图片、列表、表单甚至是另一个Animated.View。测量逻辑依赖onLayout因此内容高度无论怎么变化例如异步加载图片后高度改变onLayout都会再次触发并把新高度同步进共享值动画目标随之更新。官方示例中的Item只是一个 120×120 的紫色圆角方块styles.box用于演示实际使用时完全可以用真实业务内容替换。唯一需要留意的是styles.wrapper中的position: absolute——内容以绝对定位方式参与布局避免在测量过程中撑开外层容器导致高度突变配合overflow: hidden保证收起时视觉上被干净地裁剪。从单条目到多条目FAQ 场景的实战扩展官方示例只演示了一个条目但 FAQ 区块通常有多个问题。基于同一套机制扩展出同一时间只展开一项的手风琴非常直接把open从单个共享值换成当前展开项索引的共享值每个AccordionItem通过isExpanded{index openIndex.value}接收布尔值即可。因为共享值不触发重渲染即使有几十个条目折叠/展开依然顺滑。还可以做的增强弹性动画把withTiming换成withSpring(height.value * Number(isExpanded.value))展开收起会带弹簧回弹效果官方也提供了withSpring的完整文档docs/docs-reanimated/docs/animations/withSpring.mdx动画完成回调给withTiming传第三个参数(finished) {...}在动画结束时执行收尾逻辑例如回收资源注意回调同样会被 worklet 化并在 UI 线程执行响应式联动如果需要在展开状态变化时执行其他副作用可以使用useAnimatedReactiondocs/docs-reanimated/docs/advanced/useAnimatedReaction.mdx纯 CSS 方案如果项目使用 Reanimated 的 CSS 动画能力手风琴也可通过 CSS 过渡实现相关文档见 docs/docs-reanimated/docs/css-transitions/本文讲解的共享值方案则与 Web/原生/新架构Fabric完全兼容。相关资源导航示例源码docs/docs-reanimated/static/examples/Accordion.js官方博客原文docs/docs-reanimated/blog/accordion.md运行录屏Android accordion_android.mov、iOS accordion_ios.mov术语表共享值 / 动画函数 / UI 线程 / workletdocs/docs-reanimated/docs/fundamentals/glossary.mdxAPI 文档useSharedValue · useDerivedValue · useAnimatedStyle · withTiming底层实现useSharedValue 源码、useDerivedValue 源码、useAnimatedStyle 源码、withTiming 源码这套共享值测量高度 派生值驱动动画 动画样式绑定视图的组合拳是 Reanimated 高性能 UI 编程的典型范式掌握它之后抽屉、折叠面板、展开详情等一切内容显隐类动画都能举一反三。【免费下载链接】react-native-reanimatedReact Natives Animated library reimplemented项目地址: https://gitcode.com/GitHub_Trending/re/react-native-reanimated创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表