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