深入理解 Reanimated 布局动画:从 Participant 组件内部结构到 Animated.View 的接入改造
2026/9/16 0:43:42 网站建设 项目流程

深入理解 Reanimated 布局动画:从 Participant 组件内部结构到 Animated.View 的接入改造

【免费下载链接】react-native-reanimatedReact Native's Animated library reimplemented项目地址: https://gitcode.com/GitHub_Trending/re/react-native-reanimated

导读

在 React Native 中,组件的挂载与卸载默认是瞬间完成的——新增一个列表项,它立即出现在界面上;移除一个列表项,它在下一帧直接消失。react-native-reanimated 从 v2.3.0 开始引入的Layout Animations(布局动画)正是为了解决这一痛点:它允许你为组件的「进入(entering)」「退出(exiting)」以及「布局位置变化(layout)」三类场景接入预设动画,让列表增删、弹窗显隐等交互变得平滑自然。

本文以 Reanimated 官方教程中的参与者(Participant)列表为例,剖析待动画化组件的内部结构(_participantInternals.md),并以此为起点逐步完成从普通ViewAnimated.View的改造,最终叠加enteringlayoutexiting三类动画。读完本文,你将掌握:哪些组件可以被布局动画驱动、为什么必须使用 Reanimated 提供的动画组件、以及如何用LightSpeedInLeftLightSpeedOutRightLayout.springify()在真实列表场景中落地。

一、教程背景:准备动画化的 Participant 列表

官方教程(见 animated_list.mdx)基于一个已有的「参与者列表」界面:用户可以通过底部输入框添加参与者姓名,也可以点击每个列表项上的红色 Remove 按钮将其删除。列表由ScrollView承载,通过participantList.map(...)渲染出多个Participant组件:

<ScrollView style={[{ width: '100%' }]}> {participantList.map((participant) => ( <Participant key={participant.id} name={participant.name} onRemove={() => removeParticipant(participant.id)} /> ))} </ScrollView>

列表数据层的完整逻辑(完整代码见 _fullCode.md)如下:

const [inputValue, setInputValue] = useState(''); const [participantList, setParticipantList] = useState<EventParticipant[]>([]); const addParticipant = () => { setParticipantList( [{ name: inputValue, id: Date.now().toString() }].concat(participantList) ); setInputValue(''); }; const removeParticipant = (id: string) => { setParticipantList( participantList.filter((participant) => participant.id !== id) ); };

可以看到:

  • 新参与者通过concat插入到数组头部,因此会出现在列表最上方;
  • 删除时通过filterid过滤,对应行会被移除;
  • key={participant.id}使用Date.now().toString()生成唯一 id,保证 React 能正确识别每一个列表项。

在没有布局动画时,插入与删除都是「闪现」式的:新项瞬间出现、被删项瞬间消失,其余项瞬间上移/下移填补空位。这正是后续要改造的体验痛点。

二、Participant 组件内部结构解析(关联文档核心)

Participant组件负责渲染单个列表项,它是我们后续所有动画的载体。其内部结构(见 _participantInternals.md)非常简单——只包含一个根View、一行姓名文本和一个红色删除按钮:

function Participant({ name, onRemove, }: { name: string; onRemove: () => void; }) { return ( <View style={[styles.participantView]}> <Text>{name}</Text> <Button title="Remove" color="red" onPress={onRemove} /> </View> ); }

拆解该组件的职责:

部分说明
Viewstyles.participantView列表项的容器,负责整行的布局与样式,是布局动画要挂载的位置
Text展示参与者姓名name
Button(红色Remove触发onRemove回调,从列表中删除当前项

2.1 为什么动画必须加在根 View 上

教程明确指出:Participant 组件被包在一个 View 组件中,这正是我们添加动画的位置。原因在于布局动画需要作用于「一个独立、可整体感知自身几何变化」的视图节点:

  • 进入/退出动画:需要视图从初始状态(如屏幕外偏移 + 倾斜 + 透明)过渡到最终状态,或反向过渡,这要求动画作用在代表整个列表项的单个视图上;
  • 布局动画:当列表项因兄弟节点增删而改变位置或尺寸时,动画需要驱动该视图的originX/originY/width/height从旧值过渡到新值。

如果把动画拆散到内部的TextButton上,整个列表项的进入/退出/位移就会失去整体性,效果割裂且难以维护。

2.2 可动画化的组件边界:Animated.View 与 createAnimatedComponent

这是整个教程中最重要的技术约束:我们只能对由 Reanimated 提供的组件(如Animated.View),或通过createAnimatedComponent包装过的自定义组件施加布局动画。普通 React Native 的View不具备 Reanimated 的动画挂载点,传入entering/exiting/layout属性不会产生任何效果。

从当前仓库源码 Animated.ts 可以看到,Reanimated 的Animated命名空间导出了一组内置动画组件:

export { ReanimatedFlatList as FlatList } from './component/FlatList'; export { AnimatedImage as Image } from './component/Image'; export { AnimatedScrollView as ScrollView } from './component/ScrollView'; export { AnimatedText as Text } from './component/Text'; export { AnimatedView as View } from './component/View'; export { createAnimatedComponent } from './createAnimatedComponent';

也就是说:

  • 常用基础组件(ViewTextImageScrollViewFlatList)都有对应的Animated.*版本,可直接使用;
  • 对于自定义组件或第三方组件,则需要通过createAnimatedComponent(由 src/index.ts 对外导出)包装后使用,例如const AnimatedCustomView = createAnimatedComponent(CustomView);

在本文的列表场景中,Participant的根元素是基础View,因此最直接的做法就是用Animated.View替换它。

三、Step 1:将 View 替换为 Animated.View

改造的第一步(见 _step1.md)是引入Animated并将根View换成Animated.View

import Animated from 'react-native-reanimated'; function Participant({ name, onRemove, }: { name: string; onRemove: () => void; }) { return ( <Animated.View style={[styles.participantView]}> <Text>{name}</Text> <Button title="Remove" color="red" onPress={onRemove} /> </Animated.View> ); }

这一步本身不会产生可见的动画,它的意义在于:为组件打开接收 Reanimated 布局动画属性的通道。替换之后,Animated.View实例就能识别并处理enteringexitinglayout三个专有属性。样式styles.participantView保持不变,因此界面上没有任何视觉回归。

四、Step 2:添加进入动画 entering

有了Animated.View之后,就可以叠加进入动画(见 _step2.md)。Reanimated 内置了大量预设的进入动画(如FadeInSlideInRightZoomInBounceIn等),这里教程选择视觉效果强烈的LightSpeedInLeft

import Animated, {LightSpeedInLeft} from 'react-native-reanimated'; function Participant({ name, onRemove, }: { name: string; onRemove: () => void; }) { return ( <Animated.View entering={LightSpeedInLeft} style={[styles.participantView]}> <Text>{name}</Text> <Button title="Remove" color="red" onPress={onRemove} /> </Animated.View> ); }

效果:每当新参与者被添加到列表(组件挂载)时,它会带着「从左侧高速飞入 + 倾斜摆动 + 透明度渐变」的复合效果出现,而不是瞬间闪现。

4.1 LightSpeed 系列动画的底层实现

LightSpeedInLeft并非黑盒魔法,它在当前仓库源码 Lightspeed.ts 中定义,继承自ComplexAnimationBuilder,其build()方法返回一个在 UI 线程运行的 worklet,核心动画编排如下:

return (values: EntryExitAnimationsValues) => { 'worklet'; return { animations: { opacity: delayFunction( delay, withTiming(targetValues?.opacity ?? 1, { duration }) ), transform: [ { translateX: delayFunction( delay, animation(targetTranslateX, { ...config, duration: duration * 0.7 }) ), }, { skewX: delayFunction( delay, withSequence( withTiming('-10deg', { duration: duration * 0.7 }), withTiming('5deg', { duration: duration * 0.15 }), withTiming(targetSkewX, { duration: duration * 0.15 }) ) ), }, ], }, initialValues: { opacity: initialValues?.opacity ?? 0, transform: pickTransformValues( [{ translateX: -values.windowWidth }, { skewX: '45deg' }], initialValues ), }, }; };

从这段实现可以读出几个关键细节:

  • 初始状态translateX-windowWidth(屏幕宽度之外的左侧)开始,skewX初始为45degopacity从 0 开始;
  • 位移动画:水平位移在 70% 的时长内完成主要行程,营造高速冲刺感;
  • 倾斜动画:用withSequence串联三段withTiming-10deg → 5deg → 0deg),分别占用 70% / 15% / 15% 的时长,产生「冲过头再回正」的摆动回弹效果;
  • 透明度:与位移同步从 0 过渡到 1。

同一文件还定义了LightSpeedInRightLightSpeedOutLeftLightSpeedOutRight等系列动画,方向相反、编排对称。这也解释了为什么教程标题强调「组件内部结构」——动画的全部初始值与关键帧编排,都建立在列表项这个单一视图的几何信息之上。

五、Step 3:添加布局过渡 layout

接下来为列表项添加布局过渡动画(见 _step3.md)。这一步解决的是「兄弟项移动」的动画:当某个参与者被删除后,它下方的所有列表项会向上移动填补空位;当新项插入时,原有项会向下让位。默认情况下这些位移是瞬间完成的,通过layout属性可以让它们平滑过渡:

import Animated, { LightSpeedInLeft, Layout } from 'react-native-reanimated'; function Participant({ name, onRemove, }: { name: string; onRemove: () => void; }) { return ( <Animated.View entering={LightSpeedInLeft} layout={Layout.springify()} style={[styles.participantView]}> <Text>{name}</Text> <Button title="Remove" color="red" onPress={onRemove} /> </Animated.View> ); }

这里的Layout是线性过渡LinearTransition的别名。查看源码 LinearTransition.ts 末尾:

/** @deprecated Please use {@link LinearTransition} instead. */ export const Layout = LinearTransition;

LinearTransitionbuild()会返回一个 worklet,它基于布局前后快照,对originXoriginYwidthheight四个几何量做插值过渡:

return (values) => { 'worklet'; return { initialValues: { originX: values.currentOriginX, originY: values.currentOriginY, width: values.currentWidth, height: values.currentHeight, }, animations: { originX: delayFunction(delay, animation(values.targetOriginX, config)), originY: delayFunction(delay, animation(values.targetOriginY, config)), width: delayFunction(delay, animation(values.targetWidth, config)), height: delayFunction(delay, animation(values.targetHeight, config)), }, }; };

也就是说:布局动画的实质是「记录当前位置/尺寸 → 在下一帧得知目标位置/尺寸 → 让几何属性平滑变化」。

5.1 链式修饰器:springify 与更多定制手段

Layout.springify()中的springify是一个链式修饰器(modifier),来自基类AnimationConfigBuilder(实现于 ComplexAnimationBuilder.ts)。其核心实现:

springify(duration?: number): this { this.durationV = duration; this.type = withSpring as AnimationFunction; return this; }

它做了两件事:

  1. 将底层动画函数替换为withSpring(弹簧动画),使过渡带有一点弹性过冲;
  2. 可选地传入一个以毫秒为单位的duration来约束弹簧时长。

同一个基类还提供了一系列可链式组合的修饰器,例如:

  • duration(milliseconds):设置动画时长;
  • delay(milliseconds):设置动画延迟;
  • easing(easingFunction):自定义缓动曲线;
  • dampingRatio(ratio)/damping(damping)/stiffness(stiffness)/mass(mass):调整弹簧物理参数;
  • withCallback(callback):动画结束时回调;
  • rotate(degree)randomDelay()等。

这些修饰器同样作用于entering/exiting预设动画,例如LightSpeedInLeft.springify()LightSpeedInLeft.duration(500),这正是源码注释中所说的「You can modify the behavior by chaining methods like.springify()or.duration(500)」。

六、Step 4:添加退出动画 exiting

最后一步为列表项添加退出动画(见 _step4.md)。与进入动画对称,Reanimated 也提供了丰富的预设退出动画,教程选用LightSpeedOutRight

import Animated, { LightSpeedInLeft, LightSpeedOutRight, Layout } from 'react-native-reanimated'; function Participant({ name, onRemove, }: { name: string; onRemove: () => void; }) { return ( <Animated.View entering={LightSpeedInLeft} exiting={LightSpeedOutRight} layout={Layout.springify()} style={[styles.participantView]}> <Text>{name}</Text> <Button title="Remove" color="red" onPress={onRemove} /> </Animated.View> ); }

至此,Animated.View上同时挂载了三类动画:

属性取值触发时机动画效果
enteringLightSpeedInLeft组件挂载(添加参与者)从左侧高速飞入,伴随倾斜摆动与淡入
layoutLayout.springify()兄弟项增删导致位置变化弹性平滑移动到新位置
exitingLightSpeedOutRight组件卸载(删除参与者)向右高速飞出,伴随倾斜摆动与淡出

6.1 退出动画的底层编排

LightSpeedOutRight与进入动画方向相反:初始状态是当前静止位置(translateX: 0skewX: 0degopacity: 1),动画目标是translateX: windowWidth(飞出屏幕右侧)、skewX: -45degopacity: 0。从 Lightspeed.ts 的实现可见其编排与LightSpeedInLeft完全对称:

return (values: EntryExitAnimationsValues) => { 'worklet'; return { animations: { opacity: delayFunction(delay, animation(targetValues?.opacity ?? 0, config)), transform: animateTransformToValues( [{ translateX: values.windowWidth }, { skewX: '-45deg' }], targetValues, animationAndConfig, delayFunction, delay ), }, initialValues: { opacity: initialValues?.opacity ?? 1, transform: pickTransformValues( [{ translateX: 0 }, { skewX: '0deg' }], initialValues ), }, }; };

值得注意的工程细节:exiting 动画完成后,组件才会真正从视图树中移除。这意味着 React Native 的卸载过程不会打断退出动画,动画播放完毕后再销毁原生视图,从而避免「动画刚播一帧就被卸载」的闪烁问题。

七、完整代码与进阶阅读

将以上四步汇总,即得到完整的动画化列表。数据层完整实现可参考 fullCode.md。整个改造过程仅需三个要点:用Animated.View作为动画载体、为增删改挂载entering/exiting/layout三个属性、用链式修饰器微调动画手感。

如果想进一步深入,当前仓库与文档还提供了以下素材:

  • 预设动画全集:进入/退出动画的完整清单与参数说明见 EntryAnimations.md 与 ExitAnimations.md;
  • 布局过渡与自定义动画:LayoutTransitions.md 讲解线性、序列、跳跃等过渡方案,CustomAnimations.md 介绍如何手写动画,KeyframeAnimations.md 则支持基于关键帧的编排;
  • 概念总览:布局动画的设计动机与适用范围见 layout_animations.md;
  • 源码位置LightSpeed系列在 defaultAnimations/Lightspeed.ts,线性过渡在 defaultTransitions/LinearTransition.ts,链式修饰器基类在 animationBuilder/ComplexAnimationBuilder.ts。

八、小结

回顾整条改造链路,_participantInternals.md这一节虽然只展示了一个看似普通的View包裹结构,但它承载了布局动画的全部前提:

  1. 动画必须作用在单一根视图上,才能保证列表项整体进入/退出/移动的一致性;
  2. 普通组件不具备动画能力,必须换成Animated.*内置组件或经createAnimatedComponent包装的组件;
  3. 在此基础上,enteringlayoutexiting三个属性分别接管组件的出现、位移与消失,配合springifyduration等链式修饰器即可精细控制动画手感。

通过源码我们可以看到,这些预设动画本质上都是在 UI 线程运行的 worklet:它们读取视图的几何快照(windowWidthcurrentOriginX/YtargetOriginX/Y等),用withTimingwithSpringwithSequence组合出复杂的运动轨迹。理解了这个机制,你不仅会使用现成预设,还能基于 CustomAnimations.md 构建属于自己的布局动画。

【免费下载链接】react-native-reanimatedReact Native's Animated library reimplemented项目地址: https://gitcode.com/GitHub_Trending/re/react-native-reanimated

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询