我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。

本文作者:佳岚

回顾传统React动画

对于普通的 React 动画,我们大多使用官方推荐的 react-transition-group,其提供了四个基本组件 Transition、CSSTransition、SwitchTransition、TransitionGroup

Transition

Transition 组件允许您使用简单的声明式 API 来描述组件的状态变化,默认情况下,Transition 组件不会改变它呈现的组件的行为,它只跟踪组件的“进入”和“退出”状态,我们需要做的是赋予这些状态意义。

其一共提供了四种状态,当组件感知到 in prop 变化时就会进行相应的状态过渡

  • 'entering'
  • 'entered'
  • 'exiting'
  • 'exited'
const defaultStyle = {
transition: `opacity ${duration}ms ease-in-out`,
opacity: 0,
} const transitionStyles = {
entering: { opacity: 1 },
entered: { opacity: 1 },
exiting: { opacity: 0 },
exited: { opacity: 0 },
}; const Fade = ({ in: inProp }) => (
<Transition in={inProp} timeout={duration}>
{state => (
<div style={{
...defaultStyle,
...transitionStyles[state]
}}>
I'm a fade Transition!
</div>
)}
</Transition>
);

CSSTransition

此组件主要用来做 CSS 样式过渡,它能够在组件各个状态变化的时候给我们要过渡的标签添加上不同的类名。所以参数和平时的 className 不同,参数为:classNames

<CSSTransition
in={inProp}
timeout={300}
classNames="fade"
unmountOnExit
>
<div className="star"></div>
</CSSTransition> // 定义过渡样式类
.fade-enter {
opacity: 0;
}
.fade-enter-active {
opacity: 1;
transition: opacity 200ms;
}
.fade-exit {
opacity: 1;
}
.fade-exit-active {
opacity: 0;
transition: opacity 200ms;
}

SwitchTransition

SwitchTransition 用来做组件切换时的过渡,其会缓存传入的 children,并在过渡结束后渲染新的 children

function App() {
const [state, setState] = useState(false);
return (
<SwitchTransition>
<CSSTransition
key={state ? "Goodbye, world!" : "Hello, world!"}
classNames='fade'
>
<button onClick={() => setState(state => !state)}>
{state ? "Goodbye, world!" : "Hello, world!"}
</button>
</CSSTransition>
</SwitchTransition>
);
}

TransitionGroup

如果有一组 CSSTransition 需要我们去过渡,那么我们需要管理每一个 CSSTransition 的 in 状态,这样会很麻烦。

TransitionGroup 可以帮我们管理一组 Transition 或 CSSTransition 组件,为此我们不再需要给 Transition 组件传入 in 属性来标识过渡状态,转用 key 属性来代替 in

<TransitionGroup>
{
this.state.list.map((item, index) => {
return (
<CSSTransition
key = {item.id}
timeout = {1000}
classNames = 'fade'
unmountOnExit
>
<TodoItem />
</CSSTransition>
)
}
}
</TransitionGroup>

TransitionGroup 会监测其 children 的变化,将新的 children 与原有的 children 使用 key 进行比较,就能得出哪些 children 是新增的与删除的,从而为他们注入进场动画或离场动画。

FLIP 动画

FLIP 是什么?

FLIPFirstLastInvertPlay四个单词首字母的缩写

First, 元素过渡前开始位置信息

Last:执行一段代码,使元素位置发生变化,记录最后状态的位置等信息.

Invert:根据 First 和 Last 的位置信息,计算出位置差值,使用 transform: translate(x,y) 将元素移动到First的位置。

Play:  给元素加上 transition 过渡属性,再讲 transform 置为 none,这时候因为 transition 的存在,开始播放丝滑的动画。

Flip 动画可以看成是一种编写动画的范式,方法论,对于开始或结束状态未知的复杂动画,可以使用 Flip 快速实现

位置过渡效果

代码实现:

  const container = document.querySelector('.flip-container');
const btnAdd = document.querySelector('#add-btn')
const btnDelete = document.querySelector('#delete-btn')
let rectList = [] function addItem() {
const el = document.createElement('div')
el.className = 'flip-item'
el.innerText = rectList.length + 1;
el.style.width = (Math.random() * 300 + 100) + 'px' // 加入新元素前重新记录起始位置信息
recordFirst(); // 加入新元素
container.prepend(el)
rectList.unshift({
top: undefined,
left: undefined
}) // 触发FLIP
update()
} function removeItem() {
const children = container.children;
if (children.length > 0) {
recordFirst();
container.removeChild(children[0])
rectList.shift()
update()
}
} // 记录位置
function recordFirst() {
const items = container.children;
for (let i = 0; i < items.length; i++) {
const rect = items[i].getBoundingClientRect();
rectList[i] = {
left: rect.left,
top: rect.top
}
}
} function update() {
const items = container.children;
for (let i = 0; i < items.length; i++) {
// Last
const rect = items[i].getBoundingClientRect();
if (rectList[i].left !== undefined) { // Invert
const transformX = rectList[i].left - rect.left;
const transformY = rectList[i].top - rect.top; items[i].style.transform = `translate(${transformX}px, ${transformY}px)`
items[i].style.transition = "none" // Play
requestAnimationFrame(() => {
items[i].style.transform = `none`
items[i].style.transition = "all .5s"
})
}
}
} btnAdd.addEventListener('click', () => {
addItem()
})
btnDelete.addEventListener('click', () => {
removeItem()
})

使用 flip 实现的动画 demo

乱序动画:

缩放动画:

React跨路由组件动画

在 React 中路由之前的切换动画可以使用 react-transition-group 来实现,但对于不同路由上的组件如何做到动画过渡是个很大的难题,目前社区中也没有一个成熟的方案。

使用flip来实现

在路由 A 中组件的大小与位置状态可以当成 First, 在路由 B 中组件的大小与位置状态可以当成 Last

从路由 A 切换至路由B时,向 B 页面传递 First 状态,B 页面中需要过渡的组件再进行 Flip 动画。

为此我们可以抽象出一个组件来帮我们实现 Flip 动画,并且能够在切换路由时保存组件的状态。

对需要进行过渡的组件进行包裹, 使用相同的 flipId 来标识他们需要在不同的路由中过渡。

<FlipRouteAnimate className="about-profile" flipId="avatar" animateStyle={{ borderRadius: "15px" }}>
<img src={require("./touxiang.jpg")} alt="" />
</FlipRouteAnimate>

完整代码:

import React, { createRef } from "react";
import withRouter from "./utils/withRouter";
class FlipRouteAnimate extends React.Component {
constructor(props) {
super(props);
this.flipRef = createRef();
}
// 用来存放所有实例的rect
static flipRectMap = new Map();
componentDidMount() {
const {
flipId,
location: { pathname },
animateStyle: lastAnimateStyle,
} = this.props; const lastEl = this.flipRef.current; // 没有上一个路由中组件的rect,说明不用进行动画过渡
if (!FlipRouteAnimate.flipRectMap.has(flipId) || flipId === undefined) return; // 读取缓存的rect
const first = FlipRouteAnimate.flipRectMap.get(flipId);
if (first.route === pathname) return; // 开始FLIP动画
const firstRect = first.rect;
const lastRect = lastEl.getBoundingClientRect(); const transformOffsetX = firstRect.left - lastRect.left;
const transformOffsetY = firstRect.top - lastRect.top; const scaleRatioX = firstRect.width / lastRect.width;
const scaleRatioY = firstRect.height / lastRect.height; lastEl.style.transform = `translate(${transformOffsetX}px, ${transformOffsetY}px) scale(${scaleRatioX}, ${scaleRatioY})`;
lastEl.style.transformOrigin = "left top"; for (const styleName in first.animateStyle) {
lastEl.style[styleName] = first.animateStyle[styleName];
} setTimeout(() => {
lastEl.style.transition = "all 2s";
lastEl.style.transform = `translate(0, 0) scale(1)`;
// 可能有其他属性也需要过渡
for (const styleName in lastAnimateStyle) {
lastEl.style[styleName] = lastAnimateStyle[styleName];
}
}, 0);
} componentWillUnmount() {
const {
flipId,
location: { pathname },
animateStyle = {},
} = this.props;
const el = this.flipRef.current;
// 组件卸载时保存自己的位置等状态
const rect = el.getBoundingClientRect(); FlipRouteAnimate.flipRectMap.set(flipId, {
// 当前路由路径
route: pathname,
// 组件的大小位置
rect: rect,
// 其他需要过渡的样式
animateStyle,
});
}
render() {
return (
<div
className={this.props.className}
style={{ display: "inline-block", ...this.props.style, ...this.props.animateStyle }}
ref={this.flipRef}
>
{this.props.children}
</div>
);
}
}

实现效果:

共享组件的方式实现

要想在不同的路由共用同一个组件实例,并不现实,树形的 Dom 树并不允许我们这么做。

我们可以换个思路,把组件提取到路由容器的外部,然后通过某种方式将该组件与路由页面相关联。

我们将 Float 组件提升至根组件,然后在每个路由中使用 Proxy 组件进行占位,当路由切换时,每个 Proxy 将其位置信息与其他 props 传递给 Float 组件,Float 组件再根据接收到的状态信息,将自己移动到对应位置。

我们先封装一个 Proxy 组件,  使用 PubSub 发布元信息。

// FloatProxy.tsx
const FloatProxy: React.FC<any> = (props: any) => {
const el = useRef(); // 保存代理元素引用,方便获取元素的位置信息
useEffect(() => {
PubSub.publish("proxyElChange", el);
return () => {
PubSub.publish("proxyElChange", null);
}
}, []); useEffect(() => {
PubSub.publish("metadataChange", props);
}, [props]); const computedStyle = useMemo(() => {
const propStyle = props.style || {};
return {
border: "dashed 1px #888",
transition: "all .2s ease-in",
...propStyle,
};
}, [props.style]); return <div {...props} style={computedStyle} ref={el}></div>;
};

在路由中使用, 将样式信息进行传递

class Bar extends React.Component {
render() {
return (
<div className="container">
<p>bar</p>
<div style={{ marginTop: "140px" }}>
<FloatProxy style={{ width: 120, height: 120, borderRadius: 15, overflow: "hidden" }} />
</div>
</div>
);
}
}

创建全局变量用于保存代理信息

// floatData.ts
type ProxyElType = {
current: HTMLElement | null;
};
type MetaType = {
attrs: any;
props: any;
}; export const metadata: MetaType = {
attrs: {
hideComponent: true,
left: 0,
top: 0
},
props: {},
}; export const proxyEl: ProxyElType = {
current: null,
};

创建一个FloatContainer容器组件,用于监听代理数据的变化,  数据变动时驱动组件进行移动

import { metadata, proxyEl } from "./floatData";
class FloatContainer extends React.Component<any, any> {
componentDidMount() {
// 将代理组件上的props绑定到Float组件上
PubSub.subscribe("metadataChange", (msg, props) => {
metadata.props = props;
this.forceUpdate();
}); // 切换路由后代理元素改变,保存代理元素的位置信息
PubSub.subscribe("proxyElChange", (msg, el) => {
if (!el) {
metadata.attrs.hideComponent = true;
// 在下一次tick再更新dom
setTimeout(() => {
this.forceUpdate();
}, 0);
return;
} else {
metadata.attrs.hideComponent = false;
}
proxyEl.current = el.current;
const rect = proxyEl.current?.getBoundingClientRect()!;
metadata.attrs.left = rect.left;
metadata.attrs.top = rect.top
this.forceUpdate();
});
} render() {
const { timeout = 500 } = this.props;
const wrapperStyle: React.CSSProperties = {
position: "fixed",
left: metadata.attrs.left,
top: metadata.attrs.top,
transition: `all ${timeout}ms ease-in`,
// 当前路由未注册Proxy时进行隐藏
display: metadata.attrs.hideComponent ? "none" : "block",
}; const propStyle = metadata.props.style || {}; // 注入过渡样式属性
const computedProps = {
...metadata.props,
style: {
transition: `all ${timeout}ms ease-in`,
...propStyle,
},
};
console.log(metadata.attrs.hideComponent) return <div className="float-element" style={wrapperStyle}>{this.props.render(computedProps)} </div>;
}
}

将组件提取到路由容器外部,并使用 FloatContainer 包裹

function App() {
return (
<BrowserRouter>
<div className="App">
<NavLink to={"/"}>/foo</NavLink>
<NavLink to={"/bar"}>/bar</NavLink>
<NavLink to={"/baz"}>/baz</NavLink>
<FloatContainer render={(attrs: any) => <MyImage {...attrs}/>}></FloatContainer>
<Routes>
<Route path="/" element={<Foo />}></Route>
<Route path="/bar" element={<Bar />}></Route>
<Route path="/baz" element={<Baz />}></Route>
</Routes>
</div>
</BrowserRouter>
);
}

实现效果:

目前我们实现了一个单例的组件,我们将组件改造一下,让其可以被复用

首先我们将元数据更改为一个元数据 map,以 layoutId 为键,元数据为值

// floatData.tsx
type ProxyElType = {
current: HTMLElement | null;
};
type MetaType = {
attrs: {
hideComponent: boolean,
left: number,
top: number
};
props: any;
}; type floatType = {
metadata: MetaType,
proxyEl: ProxyElType
} export const metadata: MetaType = {
attrs: {
hideComponent: true,
left: 0,
top: 0
},
props: {},
}; export const proxyEl: ProxyElType = {
current: null,
}; export const floatMap = new Map<string, floatType>()

在代理组件中传递layoutId 来通知注册了相同layoutId的floatContainer做出相应变更

// FloatProxy.tsx 

// 保存代理元素引用,方便获取元素的位置信息
useEffect(() => {
const float = floatMap.get(props.layoutId);
if (float) {
float.proxyEl.current = el.current;
} else {
floatMap.set(props.layoutId, {
metadata: {
attrs: {
hideComponent: true,
left: 0,
top: 0,
},
props: {},
},
proxyEl: {
current: el.current,
},
});
}
PubSub.publish("proxyElChange", props.layoutId);
return () => {
if (float) {
float.proxyEl.current = null
PubSub.publish("proxyElChange", props.layoutId);
}
};
}, []); // 在路由中使用
<FloatProxy layoutId='layout1' style={{ width: 200, height: 200 }} />

在FloatContainer组件上也加上layoutId来标识同一组

// FloatContainer.tsx

// 监听到自己同组的Proxy发送消息时进行rerender
PubSub.subscribe("metadataChange", (msg, layoutId) => {
if (layoutId === this.props.layoutId) {
this.forceUpdate();
}
}); // 页面中使用
<FloatContainer layoutId='layout1' render={(attrs: any) => <MyImage imgSrc={img} {...attrs} />}></FloatContainer>

实现多组过渡的效果

最后

欢迎关注【袋鼠云数栈UED团队】~

袋鼠云数栈UED团队持续为广大开发者分享技术成果,相继参与开源了欢迎star

React跨路由组件动画的更多相关文章

  1. 067——VUE中vue-router之使用transition设置酷炫的路由组件过渡动画效果

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  2. Context - React跨组件访问数据的利器

    Context提供了一种跨组件访问数据的方法.它无需在组件树间逐层传递属性,也可以方便的访问其他组件的数据 在经典的React应用中,数据是父组件通过props向子组件传递的.但是在某些特定场合,有些 ...

  3. 链接进入react二级路由,引发的子组件二次挂载

    这个问题很怪,我两个二级路由从链接进入的时候,会挂载两次子组件. 从链接进入,是因为新页面在新标签页打开的. 有子组件是因为公共组件提取 同样的操作,有一些简单的二级路由页面,就不会挂载两次. 讲道理 ...

  4. react第六单元(react组件通信-父子组件通信-子父组件通信-跨级组件的传参方式-context方式的传参)

    第六单元(react组件通信-父子组件通信-子父组件通信-跨级组件的传参方式-context方式的传参) #课程目标 1.梳理react组件之间的关系 2.掌握父子传值的方法 3.掌握子父传值的方法 ...

  5. Javascript - Vue - webpack中的组件、路由和动画

    引入vue.js 1.cnpm i vue -S 2.在mian.js中引入vue文件 import Vue from "vue"//在main.js中使用这种方式引用vue文件时 ...

  6. ARouter 路由 组件 跳转 MD

    目录 简介 支持的功能 典型应用 简单使用 进阶使用 更多功能 其他 Q&A Markdown版本笔记 我的GitHub首页 我的博客 我的微信 我的邮箱 MyAndroidBlogs bai ...

  7. react实现页面切换动画效果

    一.前情概要 注:(我使用的路由是react-router4)     如下图所示,我们需要在页面切换时有一个过渡效果,这样就不会使页面切换显得生硬,用户体验大大提升:     but the 问题是 ...

  8. react-router4.x 实用例子(路由过渡动画、代码分割)

    react-router4.2.0实用例子 代码分割 官网上面写的代码分割是不支持create-react-app脚手架的,要使用import实现 创建一个bundle.js文件 import { C ...

  9. 12. 前后端联调 + ( proxy代理 ) + ( axios拦截器 ) + ( css Modules模块化方案 ) + ( css-loader ) + ( 非路由组件如何使用history ) + ( bodyParser,cookieParser中间件 ) + ( utility MD5加密库 ) + ( nodemon自动重启node ) + +

    (1) proxy 前端的端口在:localhost:3000后端的端口在:localhost:1234所以要在webpack中配置proxy选项 (proxy是代理的意思) 在package.jso ...

  10. react-router 路由切换动画

    路由切换动画 因为项目的需求,需要在路由切换的时候,加入一些比较 zb 的视觉效果,所以研究了一下.把这些学习的过程记录下来,以便以后回顾.同时也希望这些内容能够帮助一些跟我一样的菜鸟,让他们少走些坑 ...

随机推荐

  1. 前端 vue 自定义导航栏组件高度及返回箭头 自定义 tabbar 图标

    前端vue自定义导航栏组件高度及返回箭头 自定义tabbar图标, 下载完整代码请访问uni-app插件市场地址:https://ext.dcloud.net.cn/plugin?id=12986 效 ...

  2. 如何优化数据warehouse的搜索和查询

    目录 1. 引言 2. 技术原理及概念 2.1 基本概念解释 2.2 技术原理介绍 2.2.1 查询优化 2.2.2 索引优化 2.2.3 数据访问优化 2.3 相关技术比较 2.3.1 SQL 2. ...

  3. 实例讲解看nsenter带你“上帝视角”看网络

    摘要:本文重点关注进入目标进程的"网络ns"视角,即站在「容器中的进程视角」看待容器里面的网络世界,并在那个视角中执行命令. 本文分享自华为云社区<<跟唐老师学习云网络 ...

  4. Prometheus-4:服务自动发现Service Discovery

    自动发现 Prometheus的服务发现的几种类型: 基于文件的服务发现: 基于DNS的服务发现: 基于API的服务发现:Kubernetes.Consul.Azure...... Prometheu ...

  5. Flutter ncnn 使用

    Flutter 实现手机端 App,如果想利用 AI 模型添加新颖的功能,那么 ncnn 就是一种可考虑的手机端推理模型的框架. 本文即是 Flutter 上使用 ncnn 做模型推理的实践分享.有如 ...

  6. Redis核心技术与实践 03 | 高性能IO模型:为什么单线程Redis能那么快?

    原文地址:https://time.geekbang.org/column/article/268262 个人博客地址:http://njpkhuan.cn/archives/redis-he-xin ...

  7. 浅析本地缓存技术-Guava Cache

    1 引言 作为java开发工作者,相信大家对于guava这个工具包都不会太陌生,而对于本地缓存技术guava cache,大家在日常的工作开发中也都有所了解,接下来本文就从各个角度入手来对于Googl ...

  8. React报错:You are running `create-react-app` 5.0.0, which is behind the latest release (5.0.1).

    错误 解决方案 说白了就是版本过低,升级下就好.或者按照提示卸载掉原来的版本,之后输入临时创建命令即可,如下图所示 参考链接 https://stackoverflow.com/questions/7 ...

  9. Trackbar调色板

    我们将会建立一个简单的应用,显示我们指定的颜色.将会建立一个窗口,显示三个trackbar指定RGB三个颜色通道值.可以滑动trackbar来改变相应的颜色.默认情况下,初始颜色为黑色. cv2.ge ...

  10. Vue报错:Uncaught (in promise) NavigationDuplicated: Avoided redundant navigation to current location

    错误原因,我猜测多半是版本问题 在router/index.js中添加如下代码 const originalPush = VueRouter.prototype.push VueRouter.prot ...