让我们使用 React、styled-components 和 react-flip-toolkit 来创建我们自己的 Stripe 首页动画导航菜单版本。它是一个拥有流畅动画效果的令人印象深刻的菜单,而这三种工具的组合可以使重现它变得相对容易。
这是一篇中级水平的演练,假设您熟悉 React 和基本的动画概念。我们的 React 指南 是一个不错的起点。
以下是我们的目标
查看 CodePen 上 Alex (@aholachek) 的作品 React Stripe 菜单。
分解动画
首先,让我们将动画分解成不同的部分,以便我们更容易地重现它。您可能希望以慢动作查看最终产品(使用切换按钮),以便捕捉所有细节。
- 白色下拉容器更新其大小和位置。
- 下拉容器下半部分的灰色背景过渡其高度。
- 随着下拉容器的移动,先前的内容淡出并稍微向相反的方向移动,就像下拉菜单将它们留在后面一样,而新内容则滑入视图。
在我们着手在 React 中重现此动画时,需要牢记一些有用的指导原则。在可能的情况下,让我们通过让浏览器管理布局来保持简单。我们将通过将元素保留在常规 DOM 流中而不是使用绝对定位和手动计算来实现这一点。与其拥有一个每次用户鼠标位置改变都需要重新定位的单个下拉组件,不如在相应的导航栏部分渲染一个下拉组件。
我们将使用 FLIP 技术 来创建三个独立的下拉组件实际上是一个正在移动的组件的错觉。
使用 styled-components 搭建 UI
首先,我们将构建一个未动画的导航栏组件,它只需接收一个标题和下拉组件的配置对象,并渲染一个导航栏菜单。该组件将在鼠标悬停时显示和隐藏相关下拉菜单。
我们将使用 styled-components 构建 UI 组件。它们不仅是构建模块化 UI 的便捷方式,而且还具有 添加可配置 CSS 关键帧动画的出色 API。事实证明,CSS 动画和 React 能够很好地协同工作,因此我们将在稍后使用 CSS 关键帧添加许多动画。
在组件组装完成且没有任何动画的情况下,我们创建了一些看起来像这样的东西
查看 CodePen 上 Alex (@aholachek) 的作品 动画之前的 React Stripe 菜单。
请注意,菜单底部的灰色背景不见了。它是我们唯一需要从常规 DOM 流中取出并绝对定位的元素,因此我们现在先忽略它。
使用 FLIP 技术为我们的下拉菜单设置动画
我们将使用 react-flip-toolkit 库来帮助我们为下拉菜单的大小和位置设置动画。这是我为了使高级和复杂过渡更容易和可配置而创建的一个库。
它为我们提供了两个组件:一个顶级 <Flipper/>
组件,以及一个 <Flipped/>
组件来包装我们想要设置动画的任何子元素。
首先,让我们在 AnimatedNavbar 的渲染函数中设置 Flipper
包装组件
// currentIndex is the index of the hovered dropdown
<Flipper flipKey={currentIndex}>
<Navbar>
{navbarConfig.map((n, index) => {
// render navbar items here
})}
</Navbar>
</Flipper>
接下来,在我们的 DropdownContainer 组件中,我们将需要设置动画的元素包装在它们自己的 Flipped
组件中,确保为每个组件提供唯一的 flipdId
属性
<DropdownRoot>
<Flipped flipId="dropdown-caret">
<Caret />
</Flipped>
<Flipped flipId="dropdown">
<DropdownBackground>
{children}
</DropdownBackground>
</Flipped>
</DropdownRoot>
我们分别为 <Caret/>
组件和 <DropdownBackground/>
组件设置动画,以便 <DropdownBackground/>
组件上的 overflow:hidden
样式不会干扰 <Caret/>
组件的渲染。
现在我们有了可用的 FLIP 动画,但仍然存在一个问题:下拉菜单的内容在动画过程中看起来奇怪地拉伸了
查看 CodePen 上 Alex (@aholachek) 的作品 React Stripe 菜单 - 错误 #1:没有缩放调整。
这种不希望有的效果是由于缩放转换应用于子元素造成的。如果您对包含一些文本的 div 应用 scaleY(2)
,您也将放大文本并使其变形。
我们可以通过将子元素包装在一个带有 inverseFlipId
的 Flipped
组件中来解决此问题,该组件引用父组件的 flipId
(在本例中为 "dropdown"
)以请求取消父级转换对子级的影响。因为我们仍然希望平移转换影响子元素,所以我们还传递了 scale
属性以将取消限制为仅缩放更改。
<DropdownRoot>
<Flipped flipId="dropdown-caret">
<Caret />
</Flipped>
<Flipped flipId="dropdown">
<DropdownBackground>
<Flipped inverseFlipId="dropdown" scale>
{children}
</Flipped>
</DropdownBackground>
</Flipped>
</DropdownRoot>
哇。所有这些工作,我们创建了一些看起来像这样的东西
查看 CodePen 上 Alex (@aholachek) 的作品 React Stripe 菜单 - 简单 FLIP。
细节决定成败
越来越接近了,但我们仍然需要处理一些细节,使动画看起来很棒:下拉菜单出现和消失时的微妙旋转动画、先前和当前下拉菜单子元素的交叉淡入淡出,以及丝般顺滑的灰色背景高度动画。
使用 styled 组件进行可配置的 CSS 关键帧动画
我们一直在使用 styled-components 来构建此演示的 UI,它提供了一种非常方便的方式来创建 可配置的关键帧动画。 我们将此功能用于下拉菜单进入动画和内容的交叉淡入淡出。我们可以传入有关所需动画的一些基本信息——内容是淡入还是淡出,以及用户鼠标移动的方向——并自动应用相应的动画。例如,以下是 <FadeContents>
组件中交叉淡入淡出动画的代码
const getFadeContainerKeyFrame = ({ animatingOut, direction }) => {
if (!direction) return;
return keyframes`
from {
transform: translateX(${
animatingOut ? 0 : direction === "left" ? 20 : -20
}px);
}
to {
transform: translateX(${
!animatingOut ? 0 : direction === "left" ? -20 : 20
}px);
opacity: ${animatingOut ? 0 : 1};
}
`;
};
const FadeContainer = styled.div`
animation-name: ${getFadeContainerKeyFrame};
animation-duration: ${props => props.duration * 0.5}ms;
animation-fill-mode: forwards;
position: ${props => (props.animatingOut ? "absolute" : "relative")};
opacity: ${props => (props.direction && !props.animatingOut ? 0 : 1)};
animation-timing-function: linear;
top: 0;
left: 0;
`;
每次用户悬停新项目时,我们不仅会将当前下拉菜单,还会将上一个下拉菜单作为子元素提供给 DropdownContainer
组件,以及有关用户鼠标移动方向的信息。然后,DropdownContainer
组件会将它的两个子元素包装在一个新的组件 FadeContents
中,该组件将使用上面的关键帧动画代码添加适当的过渡。
这是一个指向 FadeContents
组件完整代码 的链接。
下拉菜单的进入/退出动画 的工作方式非常相似。
最后的润色:流畅的背景动画
最后,让我们添加灰色背景动画。为了使此动画保持清晰,我们需要偏离我们之前保持正常 DOM 嵌套并让浏览器处理布局的策略,而是执行一些手动定位计算。我们还需要直接与 DOM 交互。简而言之,它会变得有点混乱。
这是一个我们基本方法的可视化表示
查看 CodePen 上 Alex (@aholachek) 的作品 React Stripe Menu — Animated Background。
我们将一个灰色的 div 绝对定位在 DropdownContainer
的顶部。在 DropdownContainer
的 componentDidMount
生命周期函数中,我们将更新灰色背景的 translateY
变换。如果下拉容器组件只有一个子元素(这意味着用户到目前为止只悬停了一个下拉菜单),我们将把灰色 div 的 translateY
设置为第一个下拉菜单部分的高度。如果有两个子元素,包括之前的一个下拉菜单,我们将把初始 translateY
设置为之前下拉菜单的第一个部分的高度,然后将 translateY
动画化到当前下拉菜单的第一个部分的高度。以下是 componentDidMount
中调用的函数
const updateAltBackground = ({
altBackground,
prevDropdown,
currentDropdown
}) => {
const prevHeight = getFirstDropdownSectionHeight(prevDropdown)
const currentHeight = getFirstDropdownSectionHeight(currentDropdown)
// we'll use this function when we want a change
// to happen immediately, without CSS transitions
const immediateSetTranslateY = (el, translateY) => {
el.style.transform = `translateY(${translateY}px)`
el.style.transition = "transform 0s"
requestAnimationFrame(() => (el.style.transitionDuration = ""))
}
if (prevHeight) {
// transition the grey ("alt") background from its previous height
// to its current height
immediateSetTranslateY(altBackground, prevHeight)
requestAnimationFrame(() => {
altBackground.style.transform = `translateY(${currentHeight}px)`
})
} else {
// immediately set the background to the appropriate height
// since we don't have a stored value
immediateSetTranslateY(altBackground, currentHeight)
}
}
这种方法需要 DropdownContainer
使用 ref
并进入其子元素以在 getFirstDropdownSectionHeight
函数中获取 DOM 尺寸,这感觉有点笨拙。如果您有任何替代实现的想法,请在评论中告诉我!
总结
希望本文能帮助您澄清一些技术,以便您下次在 React 中构建动画时可以使用。通常有多种方法可以实现任何效果,但通常最好从最简单的实现开始——基本组件与一些 CSS 过渡或关键帧动画——并在必要时从那里扩展复杂性。在我们的例子中,这意味着包含一个额外的库 react-flip-toolkit,这样我们就不必担心手动转换下拉组件在屏幕上的位置。为了完全重现动画,我们确实需要编写大量的代码。但是通过将此动画分解成单独的部分并逐一解决它们,我们设法在 React 中复制了一个非常酷的 UI 效果。
很棒~
不错。但使用 React 是否有点过头了?使用原生 JavaScript 可以用更少的代码行完成。
这绝对有点过头了。这篇文章已经存在一段时间了:https://codyhouse.co/gem/stripe-navigation
我认为作者的重点是专门展示如何在 React 中实现这一点。使用 React 是教程的一部分,而不仅仅是工具选择。就像使用 React 或 Vue 创建待办事项应用程序的教程用于介绍框架一样,本文旨在展示使用 React 的复杂 UI 动画。这根本不是过度的情况。
好文章!也希望看到类似 VueJS 的东西……
是的,在 VueJS 中看到这个会非常酷。我正在考虑从头开始制作或使用一些外部组件
一篇很棒的文章!React 是用于此类目的最佳工具之一。