与其他 React FLIP 库的比较
功能 | react-flip-move | react-overdrive | react-flip-toolkit |
---|---|---|---|
位置动画 | ✅ | ✅ | ✅ |
缩放动画 | ❌ | ✅ | ✅ |
透明度动画 | ❌ | ✅ | ✅ |
父元素大小变化时不扭曲子元素 | ❌ | ❌ | ✅ |
使用真正的 FLIP 而非克隆和交叉淡化 | ✅ | ❌ | ✅ |
使用弹簧效果进行动画 | ❌ | ❌ | ✅ |
支持基于弹簧的交错效果 | ❌ | ❌ | ✅ |
可与 React 以外的框架一起使用 | ❌ | ❌ | ✅ |
快速开始
npm install react-flip-toolkit
或 yarn add react-flip-toolkit
-
用单个
Flipper
组件包裹所有需要动画的子元素,该组件有一个flipKey
属性,每当需要触发动画时该属性都会变化。 -
用
Flipped
组件包裹需要动画的元素,这些组件应该有一个在不同渲染间匹配的flipId
属性。
目录
可分叉示例
简单示例:展开的 Div
import React, { useState } from 'react'
import { Flipper, Flipped } from 'react-flip-toolkit'
const AnimatedSquare = () => {
const [fullScreen, setFullScreen] = useState(false)
const toggleFullScreen = () => setFullScreen(prevState => !prevState)
return (
<Flipper flipKey={fullScreen}>
<Flipped flipId="square">
<div
className={fullScreen ? 'full-screen-square' : 'square'}
onClick={toggleFullScreen}
/>
</Flipped>
</Flipper>
)
}
简单示例:两个 Div
import React, { useState } from 'react'
import { Flipper, Flipped } from 'react-flip-toolkit'
const Square = ({ toggleFullScreen }) => (
<Flipped flipId="square">
<div className="square" onClick={toggleFullScreen} />
</Flipped>
)
const FullScreenSquare = ({ toggleFullScreen }) => (
<Flipped flipId="square">
<div className="full-screen-square" onClick={toggleFullScreen} />
</Flipped>
)
const AnimatedSquare = () => {
const [fullScreen, setFullScreen] = useState(false)
const toggleFullScreen = () => setFullScreen(prevState => !prevState)
return (
<Flipper flipKey={fullScreen}>
{fullScreen ? (
<FullScreenSquare toggleFullScreen={toggleFullScreen} />
) : (
<Square toggleFullScreen={toggleFullScreen} />
)}
</Flipper>
)
}
简单示例:列表随机排序
import React, { useState } from 'react'
import { Flipper, Flipped } from 'react-flip-toolkit'
import shuffle from 'lodash.shuffle'
const ListShuffler = () => {
const [data, setData] = useState([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
const shuffleList = () => setData(shuffle(data))
return (
<Flipper flipKey={data.join('')}>
<button onClick={shuffleList}> 随机排序</button>
<ul className="list">
{data.map(d => (
<Flipped key={d} flipId={d}>
<li>{d}</li>
</Flipped>
))}
</ul>
</Flipper>
)
}
列表过渡
通过为卡片的大小和位置变化添加动画,为动态卡片列表增添趣味。
交错效果
react-flip-toolkit
库提供了基于弹簧的交错配置,让你能够实现复杂的序列效果。
对于最基本的交错效果,你只需要在 Flipped
元素上添加一个 stagger
布尔属性:
<Flipped flipId={`element-${i}`} stagger>
<AnimatedListItem/>
</Flipped>
弹簧自定义
react-flip-toolkit
使用弹簧动画。要自定义弹簧效果,你可以传入一个预设名称:
// 弹簧预设可以是以下之一: "stiff", "noWobble", "gentle", "veryGentle", 或 "wobbly"
<Flipper flipKey='foo' spring='wobbly'>
{/* 放置 Flipped 组件 */}
</Flipper>
或者自定义弹簧配置:
<Flipper flipKey='foo' spring={{ stiffness: 280, damping: 22 }} >
{/* 放置 Flipped 组件 */}
</Flipper>
嵌套缩放变换
有趣的动画通常除了简单的平移变换外还包含缩放变换。缩放动画的问题在于子元素 — 如果你将一个 div 放大 2 倍,它的任何子元素也会被放大,从而创建一个看起来很奇怪的动画。这就是为什么这个库允许你用一个带有 inverseFlipId
的 Flipped
组件包裹子元素,以抵消父元素的变换:
<Flipped flipId={id}>
<div>
<Flipped inverseFlipId={id} scale>
<div>不会被扭曲的文本</div>
</Flipped>
</div>
</Flipped>
默认情况下,父元素的缩放和平移变换都会被抵消(这允许子组件进行自己的 FLIP 动画而不受父元素影响)。
但在许多使用场景中,你还需要额外指定 scale
属性来限制调整为仅缩放,并允许定位随父元素移动。
注意: 具有反向变换的 DOM 元素应该紧贴其父容器,以实现最无缝的动画效果。
这意味着任何布局样式 — 内边距、弹性盒子等 — 应该应用于反转容器(用带有 inverseFlipId
的 Flipped
组件包裹的元素)而不是父 Flipped
容器。
使用 React Router 的基于路由的动画
react-flip-toolkit
可以很好地与客户端路由器配合使用,提供路由驱动的过渡效果:
<Route
render={({ location, search }) => {
return (
<Flipper
flipKey={`${location.pathname}-${location.search}`}
>
{/* 包含 Flipped 组件的子路由放在这里 */}
</Flipper>
)
}}
/>
更多示例
组件
Flipper
包含所有需要动画的元素的父包装组件。通常每个页面只需要一个,但有时在页面的不同区域使用多个 Flipper
来处理不同的过渡效果会更方便。
<Flipper flipKey={someKeyThatChanges}>{/* 子元素 */}</Flipper>
基本属性
属性 | 默认值 | 类型 | 详情 |
---|---|---|---|
flipKey (必填) | - | string , number , bool | 改变这个值会告诉 react-flip-toolkit 对包裹在 Flipped 组件中的子元素进行过渡。 |
children (必填) | - | node | 一个或多个元素子节点 |
spring | noWobble | string 或 object | 提供一个字符串引用预设弹簧之一 — noWobble (默认), veryGentle , gentle , wobbly , 或 stiff , 或者提供一个包含 stiffness 和 damping 参数的对象。在这里探索弹簧设置选项。这里提供的属性将作为默认弹簧设置,可以在 Flipped 组件上针对每个元素进行覆盖。 |
applyTransformOrigin | true | bool | react-flip-toolkit 是否应该为动画子元素应用 "0 0" 的 transform-origin (这通常但并不总是适用于 FLIP 动画) |
element | div | string | 如果你想让 Flipped 容器创建的包装元素是 div 以外的其他元素,可以在这里指定。 |
className | - | string | 应用于包装元素的类名,有助于样式设置。 |
staggerConfig | - | object | 为交错的 Flipped 子元素提供配置。配置对象可能看起来像下面的代码片段: |
staggerConfig={{
// "default" 配置将应用于没有显式键的交错元素
default: {
// 默认方向是正向
reverse: true,
// 默认为 .1, 0 < n < 1
speed: .5
},
// 这将应用于具有 stagger='namedStagger' 属性的 Flipped 元素
namedStagger : { speed: .2 }
}}
高级属性
属性 | 默认值 | 类型 | 详情 |
---|---|---|---|
decisionData | - | any | 有时,你可能希望 Flipper 的动画子元素根据状态转换表现不同 — 也许只有某些 Flipped 元素应该响应特定的变化进行动画。通过为 Flipper 组件提供 decisionData 属性,你可以使该数据在每个子 Flipped 组件的 shouldFlip 和 shouldInvert 方法中可用,这样它们可以自行决定是否进行动画。 |
debug | false | boolean | 这个实验性属性会在 FLIP 样式初始应用时暂停你的动画。这允许你在动画开始时检查状态,此时应该看起来与动画开始前的 UI 相似或相同。 |
portalKey | - | string | 通常,Flipper 组件只会对其后代应用过渡效果。这允许多个 Flipper 元素在同一页面上共存,但如果你使用 portals,它会阻止动画工作。你可以为 Flipper 提供一个唯一的 portalKey 属性,告诉它将元素选择范围扩大到整个文档,而不仅限于其子元素,这样 portals 中的元素也可以进行过渡。 |
onStart | - | function | 这个回调属性会在任何单独的 FLIP 动画开始之前被调用。它接收 Flipper 的 HTMLElement 和前面描述的 decisionData 对象作为参数。 |
onComplete | - | function | 这个回调属性会在所有单独的 FLIP 动画完成时被调用。它的唯一参数是在动画过程中被激活的 Flipped 组件的 flipId 列表。如果动画被中断,onComplete 仍会在正在进行的动画终止之前被调用。 |
handleEnterUpdateDelete | - | function | 默认情况下,react-flip-toolkit 会在动画新元素之前完成退出元素的动画,同时更新元素会立即变换。你可能想要对过渡序列有更多控制 — 比如,你想隐藏元素,暂停,更新元素,再次暂停,最后动画显示新元素。或者你可能希望过渡同时发生。如果是这样,请提供 handleEnterUpdateDelete 函数作为属性。理解其工作原理的最佳方式是查看这个交互式示例。 每次发生过渡时,handleEnterUpdateDelete 都会接收以下参数: |
handleEnterUpdateDelete({
// 该函数将进入元素的不透明度设为0,以便后续淡入效果
// 应该立即调用
hideEnteringElements,
// 为所有进入的元素调用 `onAppear`
animateEnteringElements,
// 为所有退出的元素调用 `onExit`
// 返回一个 Promise,在所有元素退出后解析
animateExitingElements,
// 主要事件:对更新的元素执行 `FLIP` 动画
// 同样返回一个 Promise,在动画完成时解析
animateFlippedElements
})
Flipped
包装一个需要动画效果的元素。
例如,在一个组件中你可以有:
<Flipped flipId="coolDiv">
<div className="small" />
</Flipped>
在另一个组件的其他地方你可以有:
<Flipped flipId="coolDiv">
<div className="big" />
</Flipped>
它们将通过 react-flip-toolkit
进行过渡动画。
Flipped
组件不会生成任何标记,它只是将一些属性传递给其包装的子元素。
包装 React 组件
如果你想包装 React 组件而不是像 div
这样的 JSX 元素,你可以提供一个渲染属性,然后在你的组件中将 flippedProps
直接应用到被包装的元素上:
<Flipped>
{flippedProps => <MyCoolComponent flippedProps={flippedProps} />}
</Flipped>
const MyCoolComponent = ({ flippedProps }) => <div {...flippedProps} />
你也可以简单地提供一个普通的 React 组件,只要该组件将未识别的属性直接传递给包装的元素(这种技术适用于包装样式化组件):
<Flipped>
<MyCoolComponent />
</Flipped>
const MyCoolComponent = ({ knownProp, ...rest }) => <div {...rest} />
基本属性
属性 | 默认值 | 类型 | 详情 |
---|---|---|---|
children (必需) | - | node 或 function | 用 Flipped 组件包装单个元素、React 组件或渲染属性子元素 |
flipId (除非提供 inverseFlipId,否则为必需) | - | string | 用于告诉 react-flip-toolkit 如何在渲染之间匹配元素,以便进行动画处理。 |
inverseFlipId | - | string | 引用父 Flipped 容器的 id,其变换你想要抵消。如果提供此属性,Flipped 组件将成为其有限版本,仅负责抵消其父变换。它将读取任何提供的 transform 属性,并忽略所有其他属性(除了 inverseFlipId )。在此处阅读更多关于抵消父变换的信息。 |
transformOrigin | "0 0" | string | 这是一个便捷方法,用于将正确的 CSS transform-origin 应用于被 FLIP 的元素。如果作为属性提供,这将覆盖 react-flip-toolkit 默认应用的 transform-origin: 0 0; 。 |
spring | noWobble | string 或 object | 提供一个字符串引用预设的弹簧之一 — noWobble (默认)、veryGentle 、gentle 、wobbly 或 stiff ,或者提供一个包含 stiffness 和 damping 参数的对象。在此处探索弹簧设置选项。 |
stagger | false | boolean 或 string | 提供一个自然的、基于弹簧的错开效果,其中每个项目的弹簧缓动都固定在前一个的移动上。提供 true 以将元素与所有其他错开的元素错开。如果你想更精细地控制,可以提供一个字符串键,元素将与具有相同键的其他元素错开。 |
delayUntil | false | string (flipId) | 通过提供对另一个 Flipped 组件的引用来延迟动画,它应该等待该组件后再开始动画(另一个 Flipped 组件应该有错开延迟,因为这是唯一需要此属性的用例。) |
回调属性
上面的动画使用 onAppear
和 onExit
回调来实现淡入和淡出动画。
属性 | 参数 | 详情 |
---|---|---|
onAppear | element , index , {previous: decisionData, current: decisionData } | 当元素首次出现在 DOM 中时调用。第一个参数提供正在过渡的 DOM 元素的引用,第二个参数是元素相对于所有出现元素的索引。注意:如果你提供 onAppear 属性,元素的默认不透明度将被设置为 0,以允许你将其动画化而不会有任何初始闪烁。如果你不想要任何不透明度动画,只需在 onAppear 函数中立即将元素的不透明度设置为 1。 |
onStart | element , {previous: decisionData, current: decisionData } | 当元素的 FLIP 动画开始时调用。第一个参数提供正在过渡的 DOM 元素的引用。 |
onStartImmediate | element , {previous: decisionData, current: decisionData } | 类似于 onStart ,但保证在 FLIP 动画的初始刻度上为所有 FLIP 元素运行,在下一帧渲染之前,即使元素有错开延迟。第一个参数提供正在过渡的 DOM 元素的引用。 |
onSpringUpdate | springValue | 使用当前的弹簧值调用(通常在 0 - 1 之间,但可能根据弹簧的"弹性"程度短暂超出或低于该范围)。如果你想要与 FLIP 过渡同时调整其他非 FLIP 动画,这个属性很有用。 |
onComplete | element ,{previous: decisionData, current: decisionData } | 当 FLIP 动画完成时调用。第一个参数提供正在过渡的 DOM 元素的引用。(如果过渡被新的过渡打断,onComplete 仍会被调用。) |
onExit | element , index , removeElement , {previous: decisionData, current: decisionData } | 当元素从 DOM 中移除时调用。它必须在退出过渡完成时调用 removeElement 函数。 |
变换属性
默认情况下,FLIP 元素的 translate、scale 和 opacity 属性都会被变换。但是,某些效果需要更多控制,所以如果你指定了以下任何属性,只有指定的属性会被缓动:
属性 | 类型 | 详情 |
---|---|---|
translate | bool | 缓动 translateX 和 translateY |
scale | bool | 缓动 scaleX 和 scaleY |
opacity | bool |
高级属性
控制 FLIP 何时发生的函数
属性 | 参数 | 详情 |
---|---|---|
shouldFlip | previousDecisionData , currentDecisionData | 一个函数,提供由 Flipper 组件传递的当前和之前的 decisionData 属性。返回一个 boolean 值,表示 Flipped 组件在特定时刻是否应该动画化。 |
shouldInvert | previousDecisionData , currentDecisionData | 一个函数,提供由 Flipper 组件传递的当前和之前的 decisionData 属性。返回一个 boolean 值,表示是否对所有通过 inverseFlipId 请求的 Flipped 子元素应用反转变换。 |
Spring
为了方便,react-flip-toolkit
导出了一个小函数,用于访问用于创建 FLIP 过渡的相同弹簧系统。
import { spring } from 'react-flip-toolkit'
spring({
config: "wobbly",
values: {
translateY: [-15, 0],
opacity: [0, 1]
},
onUpdate: ({ translateY, opacity }) => {
el.style.opacity = opacity;
el.style.transform = `translateY(${translateY}px)`;
},
delay: i * 25,
onComplete: () => console.log('完成')
});
全局配置函数
如果需要在所有地方禁用(或重新启用)FLIP动画,可以通过编程方式调用以下函数。
disableFlip()
全局开关,用于禁用所有Flipper
容器中的所有动画。
enableFlip()
全局开关,用于(重新)启用所有Flipper
容器中的所有动画。默认情况下,动画是启用的。只有在之前使用disableFlip()
禁用了动画时,才需要调用此函数。
isFlipEnabled()
返回一个布尔值,指示动画是全局启用还是禁用。
库详情
- 使用Browserstack在最新的Chrome、Firefox、Safari和Edge浏览器中进行了测试。
- 需要React 16+版本
- 使用Rematrix进行矩阵计算,使用Rebound的简化分支进行弹簧动画
故障排除
问题#1:没有任何反应
- 确保在需要触发动画时更新了
Flipper
组件的flipKey
属性。 - 如果你的
Flipped
组件包裹的是另一个React组件而不是DOM元素,使用渲染属性获取Flipped属性并传递给必要的DOM元素。 - 接收
Flipped
属性的元素是否在DOM中可见?react-flip-toolkit
会尝试优化性能,不会对屏幕外的元素或没有宽度和高度的元素进行动画处理。 display:inline
元素无法进行动画。如果你想让inline
元素产生动画效果,请设置display:inline-block
。- 你是否开启了prefers-reduced-motion设置?从v7.1.0版本开始,该设置会禁用所有动画。
问题#2:效果看起来很奇怪/动画行为异常
- 检查确保所有
flipId
是唯一的。 在任何时候,页面上只能有一个元素具有指定的flipId
。如果页面上有多个具有相同id的Flipped
元素,动画将会出错。 - 确保你在为想要制作动画的元素添加动画,而不是例如包裹它的div。 如果你为内联元素(如文本)制作动画,但将其包裹在一个
div
中,实际上你是在为div添加动画,这可能会在某些时候导致宽度比预期的要大得多,从而影响动画效果。检查是否需要给动画元素添加inline-block
样式。 - 确保相关元素上没有任何冲突的CSS过渡。
- 如果你在为图片制作动画,尝试给图片设置固定尺寸,看看是否能解决问题。(如果你依赖图片的固有尺寸,可能在浏览器完全渲染新图片尺寸之前就开始测量了。)
问题#3:仍然无法正常工作
- 尝试使用
debug
属性。如果你仍然无法找出问题所在,可以直接在Flipper
组件上添加debug
属性来暂停过渡的开始。 - 如果你认为可能确实存在问题,或者完全卡住了,欢迎提出issue。
性能
React-flip-toolkit
在底层做了很多工作来尝试最大化你的动画性能 —— 例如,不会对屏幕外的元素进行动画处理,并且样式更新会被批量处理以防止布局抖动。
然而,如果你正在构建特别复杂的动画 —— 涉及数十个元素或大型图片的动画 —— 还有一些额外的策略可以用来确保动画的性能。
记忆化
当你使用react-flip-toolkit
触发复杂的FLIP动画时,React
可能会在允许动画开始之前花费宝贵的毫秒时间进行不必要的协调工作。如果你注意到动画触发和开始之间有轻微的延迟,这可能就是原因。为了绕过这种可能不必要的工作,尝试使用React.memo
或PureComponent
对你的动画元素进行记忆化,并看看是否可以重构你的代码,以在动画即将发生时最小化对动画子元素的属性更新。
will-change:transform
.box {
will-change: transform;
}
这个CSS属性告诉浏览器预期元素将发生变化。应谨慎使用,因为它可能会增加浏览器资源的使用。如果你注意到动画中存在渲染问题,可以尝试看看它是否能提高动画的性能。