ARTICLE DETAIL

资讯详情

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

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

深入理解 Reanimated 布局动画:从 Participant 组件内部结构到 Animated.View 的接入改造 深入理解 Reanimated 布局动画从 Participant 组件内部结构到 Animated.View 的接入改造【免费下载链接】react-native-reanimatedReact Natives 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并以此为起点逐步完成从普通View到Animated.View的改造最终叠加entering、layout、exiting三类动画。读完本文你将掌握哪些组件可以被布局动画驱动、为什么必须使用 Reanimated 提供的动画组件、以及如何用LightSpeedInLeft、LightSpeedOutRight、Layout.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] useStateEventParticipant[]([]); const addParticipant () { setParticipantList( [{ name: inputValue, id: Date.now().toString() }].concat(participantList) ); setInputValue(); }; const removeParticipant (id: string) { setParticipantList( participantList.filter((participant) participant.id ! id) ); };可以看到新参与者通过concat插入到数组头部因此会出现在列表最上方删除时通过filter按id过滤对应行会被移除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 titleRemove colorred onPress{onRemove} / /View ); }拆解该组件的职责部分说明根Viewstyles.participantView列表项的容器负责整行的布局与样式是布局动画要挂载的位置Text展示参与者姓名nameButton红色Remove触发onRemove回调从列表中删除当前项2.1 为什么动画必须加在根 View 上教程明确指出Participant 组件被包在一个 View 组件中这正是我们添加动画的位置。原因在于布局动画需要作用于「一个独立、可整体感知自身几何变化」的视图节点进入/退出动画需要视图从初始状态如屏幕外偏移 倾斜 透明过渡到最终状态或反向过渡这要求动画作用在代表整个列表项的单个视图上布局动画当列表项因兄弟节点增删而改变位置或尺寸时动画需要驱动该视图的originX/originY/width/height从旧值过渡到新值。如果把动画拆散到内部的Text或Button上整个列表项的进入/退出/位移就会失去整体性效果割裂且难以维护。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;也就是说常用基础组件View、Text、Image、ScrollView、FlatList都有对应的Animated.*版本可直接使用对于自定义组件或第三方组件则需要通过createAnimatedComponent由 src/index.ts 对外导出包装后使用例如const AnimatedCustomView createAnimatedComponent(CustomView);。在本文的列表场景中Participant的根元素是基础View因此最直接的做法就是用Animated.View替换它。三、Step 1将 View 替换为 Animated.View改造的第一步见 _step1.md是引入Animated并将根View换成Animated.Viewimport Animated from react-native-reanimated; function Participant({ name, onRemove, }: { name: string; onRemove: () void; }) { return ( Animated.View style{[styles.participantView]} Text{name}/Text Button titleRemove colorred onPress{onRemove} / /Animated.View ); }这一步本身不会产生可见的动画它的意义在于为组件打开接收 Reanimated 布局动画属性的通道。替换之后Animated.View实例就能识别并处理entering、exiting、layout三个专有属性。样式styles.participantView保持不变因此界面上没有任何视觉回归。四、Step 2添加进入动画 entering有了Animated.View之后就可以叠加进入动画见 _step2.md。Reanimated 内置了大量预设的进入动画如FadeIn、SlideInRight、ZoomIn、BounceIn等这里教程选择视觉效果强烈的LightSpeedInLeftimport 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 titleRemove colorred 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。同一文件还定义了LightSpeedInRight、LightSpeedOutLeft、LightSpeedOutRight等系列动画方向相反、编排对称。这也解释了为什么教程标题强调「组件内部结构」——动画的全部初始值与关键帧编排都建立在列表项这个单一视图的几何信息之上。五、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 titleRemove colorred onPress{onRemove} / /Animated.View ); }这里的Layout是线性过渡LinearTransition的别名。查看源码 LinearTransition.ts 末尾/** deprecated Please use {link LinearTransition} instead. */ export const Layout LinearTransition;LinearTransition的build()会返回一个 worklet它基于布局前后快照对originX、originY、width、height四个几何量做插值过渡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; }它做了两件事将底层动画函数替换为withSpring弹簧动画使过渡带有一点弹性过冲可选地传入一个以毫秒为单位的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 也提供了丰富的预设退出动画教程选用LightSpeedOutRightimport 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 titleRemove colorred onPress{onRemove} / /Animated.View ); }至此Animated.View上同时挂载了三类动画属性取值触发时机动画效果enteringLightSpeedInLeft组件挂载添加参与者从左侧高速飞入伴随倾斜摆动与淡入layoutLayout.springify()兄弟项增删导致位置变化弹性平滑移动到新位置exitingLightSpeedOutRight组件卸载删除参与者向右高速飞出伴随倾斜摆动与淡出6.1 退出动画的底层编排LightSpeedOutRight与进入动画方向相反初始状态是当前静止位置translateX: 0、skewX: 0deg、opacity: 1动画目标是translateX: windowWidth飞出屏幕右侧、skewX: -45deg、opacity: 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包裹结构但它承载了布局动画的全部前提动画必须作用在单一根视图上才能保证列表项整体进入/退出/移动的一致性普通组件不具备动画能力必须换成Animated.*内置组件或经createAnimatedComponent包装的组件在此基础上entering、layout、exiting三个属性分别接管组件的出现、位移与消失配合springify、duration等链式修饰器即可精细控制动画手感。通过源码我们可以看到这些预设动画本质上都是在 UI 线程运行的 worklet它们读取视图的几何快照windowWidth、currentOriginX/Y、targetOriginX/Y等用withTiming、withSpring、withSequence组合出复杂的运动轨迹。理解了这个机制你不仅会使用现成预设还能基于 CustomAnimations.md 构建属于自己的布局动画。【免费下载链接】react-native-reanimatedReact Natives Animated library reimplemented项目地址: https://gitcode.com/GitHub_Trending/re/react-native-reanimated创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表