React跨路由組件動(dòng)畫(huà)實(shí)現(xiàn)
回顧傳統(tǒng)React動(dòng)畫(huà)
對(duì)于普通的 React 動(dòng)畫(huà),我們大多使用官方推薦的 react-transition-group,其提供了四個(gè)基本組件 Transition、CSSTransition、SwitchTransition、TransitionGroup
Transition
Transition 組件允許您使用簡(jiǎn)單的聲明式 API 來(lái)描述組件的狀態(tài)變化,默認(rèn)情況下,Transition 組件不會(huì)改變它呈現(xiàn)的組件的行為,它只跟蹤組件的“進(jìn)入”和“退出”狀態(tài),我們需要做的是賦予這些狀態(tài)意義。
其一共提供了四種狀態(tài),當(dāng)組件感知到 in prop 變化時(shí)就會(huì)進(jìn)行相應(yīng)的狀態(tài)過(guò)渡
'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
此組件主要用來(lái)做 CSS 樣式過(guò)渡,它能夠在組件各個(gè)狀態(tài)變化的時(shí)候給我們要過(guò)渡的標(biāo)簽添加上不同的類名。所以參數(shù)和平時(shí)的 className 不同,參數(shù)為:classNames
<CSSTransition in={inProp} timeout={300} classNames="fade" unmountOnExit > <div className="star">?</div> </CSSTransition> // 定義過(guò)渡樣式類 .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 用來(lái)做組件切換時(shí)的過(guò)渡,其會(huì)緩存?zhèn)魅氲?children,并在過(guò)渡結(jié)束后渲染新的 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 需要我們?nèi)ミ^(guò)渡,那么我們需要管理每一個(gè) CSSTransition 的 in 狀態(tài),這樣會(huì)很麻煩。
TransitionGroup 可以幫我們管理一組 Transition 或 CSSTransition 組件,為此我們不再需要給 Transition 組件傳入 in 屬性來(lái)標(biāo)識(shí)過(guò)渡狀態(tài),轉(zhuǎn)用 key 屬性來(lái)代替 in
<TransitionGroup> { this.state.list.map((item, index) => { return ( <CSSTransition key = {item.id} timeout = {1000} classNames = 'fade' unmountOnExit > <TodoItem /> </CSSTransition> ) } } </TransitionGroup>
TransitionGroup 會(huì)監(jiān)測(cè)其 children 的變化,將新的 children 與原有的 children 使用 key 進(jìn)行比較,就能得出哪些 children 是新增的與刪除的,從而為他們注入進(jìn)場(chǎng)動(dòng)畫(huà)或離場(chǎng)動(dòng)畫(huà)。
FLIP 動(dòng)畫(huà)
FLIP 是什么?
FLIP 是 First
、Last
、Invert
和 Play
四個(gè)單詞首字母的縮寫(xiě)
First
, 元素過(guò)渡前開(kāi)始位置信息
Last
:執(zhí)行一段代碼,使元素位置發(fā)生變化,記錄最后狀態(tài)的位置等信息.
Invert
:根據(jù) First 和 Last 的位置信息,計(jì)算出位置差值,使用 transform: translate(x,y) 將元素移動(dòng)到First的位置。
Play
: 給元素加上 transition 過(guò)渡屬性,再講 transform 置為 none,這時(shí)候因?yàn)?transition 的存在,開(kāi)始播放絲滑的動(dòng)畫(huà)。
Flip 動(dòng)畫(huà)可以看成是一種編寫(xiě)動(dòng)畫(huà)的范式,方法論,對(duì)于開(kāi)始或結(jié)束狀態(tài)未知的復(fù)雜動(dòng)畫(huà),可以使用 Flip 快速實(shí)現(xiàn)
位置過(guò)渡效果
代碼實(shí)現(xiàn):
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 }) // 觸發(fā)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 實(shí)現(xiàn)的動(dòng)畫(huà) demo
亂序動(dòng)畫(huà):
縮放動(dòng)畫(huà):
React跨路由組件動(dòng)畫(huà)
在 React 中路由之前的切換動(dòng)畫(huà)可以使用 react-transition-group 來(lái)實(shí)現(xiàn),但對(duì)于不同路由上的組件如何做到動(dòng)畫(huà)過(guò)渡是個(gè)很大的難題,目前社區(qū)中也沒(méi)有一個(gè)成熟的方案。
使用flip來(lái)實(shí)現(xiàn)
在路由 A 中組件的大小與位置狀態(tài)可以當(dāng)成 First, 在路由 B 中組件的大小與位置狀態(tài)可以當(dāng)成 Last,
從路由 A 切換至路由B時(shí),向 B 頁(yè)面?zhèn)鬟f First 狀態(tài),B 頁(yè)面中需要過(guò)渡的組件再進(jìn)行 Flip 動(dòng)畫(huà)。
為此我們可以抽象出一個(gè)組件來(lái)幫我們實(shí)現(xiàn) Flip 動(dòng)畫(huà),并且能夠在切換路由時(shí)保存組件的狀態(tài)。
對(duì)需要進(jìn)行過(guò)渡的組件進(jìn)行包裹, 使用相同的 flipId 來(lái)標(biāo)識(shí)他們需要在不同的路由中過(guò)渡。
<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(); } // 用來(lái)存放所有實(shí)例的rect static flipRectMap = new Map(); componentDidMount() { const { flipId, location: { pathname }, animateStyle: lastAnimateStyle, } = this.props; const lastEl = this.flipRef.current; // 沒(méi)有上一個(gè)路由中組件的rect,說(shuō)明不用進(jìn)行動(dòng)畫(huà)過(guò)渡 if (!FlipRouteAnimate.flipRectMap.has(flipId) || flipId === undefined) return; // 讀取緩存的rect const first = FlipRouteAnimate.flipRectMap.get(flipId); if (first.route === pathname) return; // 開(kāi)始FLIP動(dòng)畫(huà) 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)`; // 可能有其他屬性也需要過(guò)渡 for (const styleName in lastAnimateStyle) { lastEl.style[styleName] = lastAnimateStyle[styleName]; } }, 0); } componentWillUnmount() { const { flipId, location: { pathname }, animateStyle = {}, } = this.props; const el = this.flipRef.current; // 組件卸載時(shí)保存自己的位置等狀態(tài) const rect = el.getBoundingClientRect(); FlipRouteAnimate.flipRectMap.set(flipId, { // 當(dāng)前路由路徑 route: pathname, // 組件的大小位置 rect: rect, // 其他需要過(guò)渡的樣式 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> ); } }
實(shí)現(xiàn)效果:
共享組件的方式實(shí)現(xiàn)
要想在不同的路由共用同一個(gè)組件實(shí)例,并不現(xiàn)實(shí),樹(shù)形的 Dom 樹(shù)并不允許我們這么做。
我們可以換個(gè)思路,把組件提取到路由容器的外部,然后通過(guò)某種方式將該組件與路由頁(yè)面相關(guān)聯(lián)。
我們將 Float 組件提升至根組件,然后在每個(gè)路由中使用 Proxy 組件進(jìn)行占位,當(dāng)路由切換時(shí),每個(gè) Proxy 將其位置信息與其他 props 傳遞給 Float 組件,F(xiàn)loat 組件再根據(jù)接收到的狀態(tài)信息,將自己移動(dòng)到對(duì)應(yīng)位置。
我們先封裝一個(gè) Proxy 組件, 使用 PubSub 發(fā)布元信息。
// 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>; };
在路由中使用, 將樣式信息進(jìn)行傳遞
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> ); } }
創(chuàng)建全局變量用于保存代理信息
// 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, };
創(chuàng)建一個(gè)FloatContainer容器組件,用于監(jiān)聽(tīng)代理數(shù)據(jù)的變化, 數(shù)據(jù)變動(dòng)時(shí)驅(qū)動(dòng)組件進(jìn)行移動(dòng)
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`, // 當(dāng)前路由未注冊(cè)Proxy時(shí)進(jìn)行隱藏 display: metadata.attrs.hideComponent ? "none" : "block", }; const propStyle = metadata.props.style || {}; // 注入過(guò)渡樣式屬性 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> ); }
實(shí)現(xiàn)效果:
目前我們實(shí)現(xiàn)了一個(gè)單例的組件,我們將組件改造一下,讓其可以被復(fù)用
首先我們將元數(shù)據(jù)更改為一個(gè)元數(shù)據(jù) map,以 layoutId 為鍵,元數(shù)據(jù)為值
// 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 來(lái)通知注冊(cè)了相同layoutId的floatContainer做出相應(yīng)變更
// 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來(lái)標(biāo)識(shí)同一組
// FloatContainer.tsx // 監(jiān)聽(tīng)到自己同組的Proxy發(fā)送消息時(shí)進(jìn)行rerender PubSub.subscribe("metadataChange", (msg, layoutId) => { if (layoutId === this.props.layoutId) { this.forceUpdate(); } }); // 頁(yè)面中使用 <FloatContainer layoutId='layout1' render={(attrs: any) => <MyImage imgSrc={img} {...attrs} />}></FloatContainer>
實(shí)現(xiàn)多組過(guò)渡的效果
到此這篇關(guān)于React跨路由組件動(dòng)畫(huà)的文章就介紹到這了,更多相關(guān)React路由組件動(dòng)畫(huà)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解Jotai Immer如何實(shí)現(xiàn)undo redo功能示例詳解
這篇文章主要為大家介紹了詳解Jotai Immer如何實(shí)現(xiàn)undo redo功能示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04React實(shí)現(xiàn)路由鑒權(quán)的實(shí)例詳解
React應(yīng)用中的路由鑒權(quán)是確保用戶僅能訪問(wèn)其授權(quán)頁(yè)面的方式,用于已登錄或具有訪問(wèn)特定頁(yè)面所需的權(quán)限,這篇文章就來(lái)記錄下React實(shí)現(xiàn)路由鑒權(quán)的流程,需要的朋友可以參考下2024-07-07react實(shí)現(xiàn)阻止父容器滾動(dòng)
這篇文章主要介紹了react實(shí)現(xiàn)阻止父容器滾動(dòng)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-11-11React 首頁(yè)加載慢問(wèn)題性能優(yōu)化案例詳解
這篇文章主要介紹了React 首頁(yè)加載慢問(wèn)題性能優(yōu)化案例詳解,本篇文章通過(guò)簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-09-09React Antd中如何設(shè)置表單只輸入數(shù)字
這篇文章主要介紹了React Antd中如何設(shè)置表單只輸入數(shù)字問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-06-06React項(xiàng)目中axios的封裝與API接口的管理詳解
Axios是一個(gè)npm軟件包,允許應(yīng)用程序?qū)TTP請(qǐng)求發(fā)送到Web API,下面這篇文章主要給大家介紹了關(guān)于React項(xiàng)目中axios的封裝與API接口的管理的相關(guān)資料,需要的朋友可以參考下2021-09-09基于React和antd實(shí)現(xiàn)自定義進(jìn)度條的示例代碼
在現(xiàn)代 Web 開(kāi)發(fā)中,進(jìn)度條是一種常見(jiàn)且實(shí)用的 UI 組件,用于直觀地向用戶展示任務(wù)的完成進(jìn)度,本文將詳細(xì)介紹如何使用 React 來(lái)構(gòu)建一個(gè)自定義的進(jìn)度條,它不僅能夠動(dòng)態(tài)展示進(jìn)度,還具備額外的信息提示功能,需要的朋友可以參考下2025-03-03