自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

這是一篇很好的互動(dòng)式文章,F(xiàn)ramer Motion 布局動(dòng)畫(huà)

開(kāi)發(fā) 前端
我選擇不以這種方式實(shí)現(xiàn),因?yàn)槲也幌朊撾x核心的比例校正概念。如果你有興趣,可以看看 Framer Motion源代碼,他們使用一種叫做 "投影節(jié)點(diǎn)( "projection nodes")"的東西來(lái)維護(hù)自己的類似DOM的運(yùn)動(dòng)組件樹(shù)。

重現(xiàn)framer的神奇布局動(dòng)畫(huà)的指南。

到目前為止,我最喜歡 Framer Motion 的部分是它神奇的布局動(dòng)畫(huà)--將 layout prop  拍在任何運(yùn)動(dòng)組件上,看著該組件從頁(yè)面的一個(gè)部分無(wú)縫過(guò)渡到下一個(gè)部分。

<motion.div layout />

圖片

在這篇文章中,我們主要介紹:

  • 布局變化:它們是什么,何時(shí)發(fā)生。
  • 基于CSS的方法以及為什么它們并不總是有效。
  • FLIP:是Framer Motion使用的技術(shù)。

布局變化

當(dāng)頁(yè)面上的一個(gè)元素影響其他元素改變位置時(shí),就會(huì)發(fā)生布局變化。例如,改變一個(gè)元素的寬度或高度就是一種布局變化,因?yàn)槿魏蜗噜彽脑囟急仨氁苿?dòng),以便為該元素的新尺寸騰出空間。

同樣,改變?cè)氐膉ustify-content屬性也是一種布局變化,因?yàn)樗鼘?dǎo)致該元素的子元素改變位置。

圖片

不過(guò),像scale屬性的變化并不是布局的改變,因?yàn)樗淖兓挥绊戫?yè)面上的其他元素。

圖片

用CSS做動(dòng)畫(huà)

那么,我們?nèi)绾螌⒉季肿兓龀蓜?dòng)畫(huà)呢?一種方法是直接使用 CSS過(guò)渡使屬性產(chǎn)生動(dòng)畫(huà):

.square {
transition: width 0.2s ease-out;
}

現(xiàn)在,當(dāng) square 

// Motion.js
import React from 'react'
import './styles.css'

export default function Motion({ toggled }) {
return <div className={`active ${toggled ? 'toggled' : ''}`} />
}

style.css

.active {
border: 1px solid hsl(208, 77.5%, 76.9%);
background: hsl(209, 81.2%, 84.5%);
width: 120px;
height: 120px;
border-radius: 8px;
transition: width 0.5s ease-out;
}

.toggled {
width: 200px;
}

看上去,CSS 也可以做動(dòng)畫(huà),但它有兩個(gè)主要的缺點(diǎn):

  • 不能把所有東西都做成動(dòng)畫(huà)。例如,不能對(duì)justify-content?的變化制作動(dòng)畫(huà),因?yàn)閖ustify-content不是一個(gè)可動(dòng)畫(huà)的屬性。
  • 性能問(wèn)題。涉及布局變化的CSS動(dòng)畫(huà)通常比基于 transform 的動(dòng)畫(huà)更昂貴,所以你可能會(huì)發(fā)現(xiàn)你的動(dòng)畫(huà)在低端設(shè)備上不那么流暢。

我們先來(lái)看看性能問(wèn)題。

性能

不要預(yù)先優(yōu)化 如果在低端設(shè)備上沒(méi)有注意到任何性能問(wèn)題,而且CSS transition 對(duì)你有效,那么就不要擔(dān)心!只有在需要時(shí)才進(jìn)行優(yōu)化。

涉及布局變化的CSS動(dòng)畫(huà)通常比其他CSS動(dòng)畫(huà)更昂貴,因?yàn)樗绊懙街車钠渌?。這是因?yàn)闉g覽器必須在動(dòng)畫(huà)的每一幀中重新計(jì)算頁(yè)面的布局--對(duì)于一個(gè)60FPS的動(dòng)畫(huà)來(lái)說(shuō),這意味著每秒鐘要計(jì)算60次!

回顧上面動(dòng)畫(huà)。注意到灰色的盒子看起來(lái)也在做動(dòng)畫(huà),盡管我們只過(guò)渡了藍(lán)色的盒子:

圖片

發(fā)生這種情況的原因是,每次藍(lán)框的尺寸發(fā)生變化時(shí),瀏覽器都會(huì)重新計(jì)算灰框的位置。

另一方面,瀏覽器可以更快地對(duì) transform 等CSS屬性進(jìn)行動(dòng)畫(huà)處理,因?yàn)樗鼈儾挥绊懖季帧?/p>

圖片

注意,隨著藍(lán)色方框的增長(zhǎng),灰色方框保持原狀!

所以,如果 transform? 的動(dòng)畫(huà)成本更低,我們是否可以用 transform 

是的,可以!

FLIP

FLIP 是 First, Last, Inverse, Play? 的縮寫(xiě),它是一種技術(shù),可以讓我們使用 "快速" 的 CSS 屬性(如transform?)對(duì) "slow"  的布局變化制作動(dòng)畫(huà)。FLIP甚至可以對(duì) "不可動(dòng)畫(huà)" 的屬性(如justify-content)進(jìn)行動(dòng)畫(huà)處理。Framer Motion使用FLIP來(lái)實(shí)現(xiàn)其布局動(dòng)畫(huà)。

顧名思義,F(xiàn)LIP是一種四步技術(shù),它通過(guò)顛倒瀏覽器所做的任何布局變化來(lái)工作。我們通過(guò)動(dòng)畫(huà)演示justify-content從flex-start到flex-end的變化來(lái)弄清楚它是如何工作的。

圖片

First

在 First 中,在任何布局變化發(fā)生之前,測(cè)量我們要做動(dòng)畫(huà)的元素的位置:

圖片

獲取元素位置的一種方法是使用HTML元素的.getBoundingClientRect()方法:

const Motion = (props) => {
const ref = React.useRef();
React.useLayoutEffect(() => {
const { x, y } = ref.current.getBoundingClientRect();
}, []);
return <div ref={ref} {...props} />;
};

Last

在 Last 這一步中,我們測(cè)量布局變化后元素的位置:

圖片

為了在代碼中實(shí)現(xiàn)這一點(diǎn),我們首先假設(shè)布局的改變意味著組件剛剛重新渲染了。所以我們先從useEffect鉤子中刪除依賴數(shù)組,使鉤子每次渲染都能運(yùn)行。

試著觸發(fā)幾次布局變化,檢查控制臺(tái),看看顯示的x和y值是什么。

App.js

import React from 'react'
import Motion from './Motion'
import './styles.css'

export default function App() {
const [toggled, toggle] = React.useReducer(state => !state, false)

return (
<div id="main">
<button notallow={toggle}>Toggle</button>
<div id="wrapper" style={{ justifyContent: toggled ? 'flex-end' : 'flex-start' }}>
<Motion />
</div>
</div>
)
}

Motion.js

import React from 'react'

export default function Motion() {
const squareRef = React.useRef()

React.useLayoutEffect(() => {
const box = squareRef.current?.getBoundingClientRect()
if (box) { console.log(box.x, box.y) }
})

return <div id="motion" ref={squareRef} />
}

Inverse

在 inverse 階段,我們修改正方形的位置,使其看起來(lái)像是根本沒(méi)有移動(dòng)過(guò)。要做到這一點(diǎn),我們要比較我們所做的兩個(gè)測(cè)量,并計(jì)算出一個(gè) transform ,然后應(yīng)用到正方形上。

圖片

使用 React 實(shí)現(xiàn)的代碼:

App.js

import React from 'react'
import Motion from './Motion'
import './styles.css'

export default function App() {
const [toggled, toggle] = React.useReducer(state => !state, false)

return (
<div id="main">
<button notallow={toggle}>Toggle</button>
<div id="wrapper" style={{ justifyContent: toggled ? 'flex-end' : 'flex-start' }}>
<Motion />
</div>
</div>
)
}

Motion.js

import React from 'react'

export default function Motion() {
const squareRef = React.useRef();
const initialPositionRef = React.useRef();

React.useLayoutEffect(() => {
const box = squareRef.current?.getBoundingClientRect();
if (moved(initialPositionRef.current, box)) {
// get the difference in position
const deltaX = initialPositionRef.current.x - box.x;
const deltaY = initialPositionRef.current.y - box.y;
console.log(deltaX, deltaY);

// apply the transform to the box
squareRef.current.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
}
initialPositionRef.current = box;
});

return <div id="motion" ref={squareRef} />;
}

const moved = (initialBox, finalBox) => {
// we just mounted, so we don't have complete data yet
if (!initialBox || !finalBox) return false;

const xMoved = initialBox.x !== finalBox.x;
const yMoved = initialBox.y !== finalBox.y;

return xMoved || yMoved;
}

Play

到目前為止,我們有一個(gè)正方形,它被施加了一個(gè) transform,在按下切換鍵后沒(méi)有移動(dòng)。

在FLIP的最后一步,即 Play 步驟中,我們將這個(gè) transform 動(dòng)畫(huà)化為零,讓正方形動(dòng)畫(huà)化到它的最終位置。

圖片

有多種方法可以實(shí)現(xiàn)這個(gè)動(dòng)畫(huà);我個(gè)人選擇使用Popmotion的animate函數(shù)。

import React from 'react'
import { animate } from 'popmotion'

export default function Motion() {
const squareRef = React.useRef();
const initialPositionRef = React.useRef();

React.useLayoutEffect(() => {
const box = squareRef.current?.getBoundingClientRect();
if (moved(initialPositionRef.current, box)) {
// get the difference in position
const deltaX = initialPositionRef.current.x - box.x;
const deltaY = initialPositionRef.current.y - box.y;

// inverse the change using a transform
squareRef.current.style.transform = `translate(${deltaX}px, ${deltaY}px)`;

// animate back to the final position
animate({
from: 1,
to: 0,
duration: 2000,
onUpdate: progress => {
squareRef.current.style.transform =
`translate(${deltaX * progress}px, ${deltaY * progress}px)`;
}
})
}
initialPositionRef.current = box;
});

return <div id="motion" ref={squareRef} />;
}

const moved = (initialBox, finalBox) => {
// we just mounted, so we don't have complete data yet
if (!initialBox || !finalBox) return false;

const xMoved = initialBox.x !== finalBox.x;
const yMoved = initialBox.y !== finalBox.y;

return xMoved || yMoved;
}

把所有東西放在一起

把所有步驟做起來(lái),我們得到:

圖片

動(dòng)畫(huà)的大小

到目前為止,我們只用FLIP來(lái)制作位置變化的動(dòng)畫(huà)。但對(duì)于大小來(lái)說(shuō),我們可以用同樣的方法嗎我們?cè)囍鴱?fù)制下面的動(dòng)畫(huà),在這個(gè)動(dòng)畫(huà)中,正方形被拉伸到充滿整個(gè)容器。

圖片

測(cè)量尺寸變化

我們首先要測(cè)量布局改變前后的正方形的大小。碰巧是提,我們用來(lái)測(cè)量正方形的.getBoundingClientRect()?方法也剛好返回元素的 width? 和 height:

const { width, height } = squareRef.current.getBoundingClientRect();

圖片

反轉(zhuǎn)尺寸變化

為了反轉(zhuǎn)尺寸變化,我們將用最終尺寸除以初始尺寸:

const deltaWidth = box.width / initialBoxRef.current.width;

得到一個(gè)比例后,我們可以將其傳遞給 scale 屬性:

squareRef.current.style.transform = `scaleX(${deltaWidth})`;

我們不會(huì)像position?那樣將比例動(dòng)畫(huà)到0?,而是將比例動(dòng)畫(huà)到1(如果我們將比例動(dòng)畫(huà)到0,元素將完全消失):

animate({
from: deltaWidth,
to: 1,
// ...
});

圖片

使用 position 固定大小

到目前為止,我們已經(jīng)能夠使用FLIP為位置和大小的變化制作動(dòng)畫(huà)。當(dāng)我們?cè)噲D將大小和位置都做成動(dòng)畫(huà)時(shí)會(huì)發(fā)生什么?

圖片

嗯,這看起來(lái)有點(diǎn)不對(duì)勁。這里發(fā)生了什么?如果我們?cè)?nbsp;play?  步驟之前暫停動(dòng)畫(huà),我們可以看到在 inverse 

圖片

修復(fù)轉(zhuǎn)換的起點(diǎn)

我們?cè)囍闱宄@個(gè)問(wèn)題。

當(dāng)我們把位置和大小的變化結(jié)合起來(lái)時(shí),我們?cè)谀嫦虿襟E中進(jìn)行了兩個(gè)獨(dú)立的變換--平移和縮放。如果我們單獨(dú)看一下這些變換,我們就可以知道這個(gè)正方形是如何結(jié)束的:

圖片

我們的算法首先將最終位置的左上角與原始位置的左上角對(duì)齊,然后將其縮小到初始尺寸。

縮放變換似乎是這里的罪魁禍?zhǔn)?-它從正方形的中心開(kāi)始縮放,導(dǎo)致正方形最終出現(xiàn)在錯(cuò)誤的位置?,F(xiàn)在,如果我們把變換的原點(diǎn)改為左上角,使其與平移相一致......

squareRef.current.style.transformOrigin = "top left";

圖片

對(duì)了!這就對(duì)了

如果 Transform Origin 發(fā)生變化怎么辦?

當(dāng)然,這個(gè)解決方案的最大問(wèn)題是,我們已經(jīng)硬編碼了 transform origin 的值。如果用戶想要一個(gè)不同的變換原點(diǎn)呢?在這種情況下,布局動(dòng)畫(huà)應(yīng)該仍然有效。

訣竅在于確保 inverse  步驟比較了兩個(gè)方塊的變換原點(diǎn)之間的距離。換句話說(shuō),這個(gè)錯(cuò)誤的發(fā)生是因?yàn)闇y(cè)量的距離和變換原點(diǎn)之間的差異:getBoundingClientRect()返回元素的左上角,而變換原點(diǎn)默認(rèn)是在元素的中心。

只有當(dāng)兩個(gè)正方形的大小相同時(shí),左上角的點(diǎn)之間的距離和中心之間的距離才是相等的。

圖片

為了簡(jiǎn)單起見(jiàn),我在這里只比較水平距離--如果我們考慮到垂直距離,同樣的概念也適用。

當(dāng)最終的正方形較大時(shí),中心之間的距離大于左上角各點(diǎn)之間的距離。同樣,當(dāng)最終的正方形較小時(shí),中心之間的距離小于左上角各點(diǎn)之間的距離。

有了這個(gè)見(jiàn)解,我們也可以通過(guò)使用中心之間的距離而不是左上角的點(diǎn)來(lái)解決這個(gè)問(wèn)題。

圖片

糾正子元素的變形

到目前為止,我們已經(jīng)能夠制作一個(gè)布局動(dòng)畫(huà),可以無(wú)縫過(guò)渡到大小和位置的變化?,F(xiàn)在讓我們?cè)黾右粋€(gè)測(cè)試--如果我們的元素有子元素會(huì)怎樣?

圖片

如上圖可以看到文字大小被改了。我們?cè)鯓硬拍芙鉀Q這個(gè)問(wèn)題呢?

導(dǎo)致該問(wèn)題的原因還 是inverse 比例變換。當(dāng)我們反轉(zhuǎn)到一個(gè)較小的正方形時(shí),文本最終會(huì)變小,因?yàn)檎叫伪话幢壤s小。同樣地,當(dāng)我們反轉(zhuǎn)到一個(gè)較大的正方形時(shí),文本最終會(huì)變大,因?yàn)檎叫伪话幢壤糯罅恕?/p>

反比例公式

一種方法是在子元素上應(yīng)用另一種變換,"抵消"父元素的變換。子元素的變換公式:

childScale = 1 / parentScale

例如:父元素變大兩倍,那么子方需要將其尺寸減半,才能保持相同的尺寸。試著移動(dòng)下面的滑塊,注意文字是如何保持相同大小的,而不管廣場(chǎng)的大小如何。

圖片

現(xiàn)在,如何將其與我們的布局動(dòng)畫(huà)相結(jié)合呢?

嘗試

我嘗試的第一件事是,在父元素要做動(dòng)畫(huà)之前,先計(jì)算一次反比例,然后在子元素上單獨(dú)運(yùn)行一個(gè)動(dòng)畫(huà)。

const inverseTransform = {
scaleX: 1 / parentTransform.scaleX,
scaleY: 1 / parentTransform.scaleY,
};
play({
from: inverseTransform,
to: { scaleX: 1, scaleY: 1 },
});

例如,如果父元素動(dòng)畫(huà)從scaleX: 2到scaleX: 1?,那么子代將從scaleX: 1 / 2到scaleX:1,只要比例校正的時(shí)間與父元素動(dòng)畫(huà)相同,這種方法應(yīng)該是可行的。

但是,運(yùn)行起來(lái)效果卻是錯(cuò)誤的:

圖片

在整個(gè)動(dòng)畫(huà)過(guò)程中,文字明顯地在改變。

正確的縮放時(shí)間

這里的問(wèn)題就在于這個(gè)假設(shè):

只要比例校正的時(shí)間與父動(dòng)畫(huà)相同,這種方法應(yīng)該是有效的。

正常情況下,"正確" 反轉(zhuǎn)比例不會(huì)以與父動(dòng)畫(huà)相同的方式變化,它有點(diǎn)像做自己的事情。

圖片

在上面的例子中,藍(lán)線表示父方的比例,而黃線表示子方的比例。請(qǐng)注意,藍(lán)線是一條直線,而黃線則有點(diǎn)像曲線。這告訴我們,反比例的時(shí)間與父比例的時(shí)間是不一樣的!

為了解決這個(gè)問(wèn)題,我們可以這么做:

  • 提前計(jì)算出正確的時(shí)間
  • 每當(dāng)父元素比例發(fā)生變化時(shí),計(jì)算反比例。

(2)恰好比(1)簡(jiǎn)單得多,而且還允許我們?cè)诟冈厣咸幚砀鞣N不同的時(shí)序。這也是 Framer Motion使用的方法。

animate({
from: inverseTransform,
to: {
x: 0,
y: 0,
scaleX: 1,
scaleY: 1,
},
onUpdate: ({ x, y, scaleX, scaleY }) => {
parentRef.style.transform = `...`;
const inverseScaleX = 1 / scaleX;
const inverseScaleY = 1 / scaleY;
childRef.style.transform = `scaleX(${inverseScaleX}) scaleY(${inverseScaleY}) ...`;
},
});

App.js

import React from 'react'
import Motion from './Motion'
import './styles.css'

export default function App() {
const [toggled, toggle] = React.useReducer(state => !state, false)
const [corrected, toggleCorrected] = React.useReducer(state => !state, false)

return (
<div id="main">
<div>
<button notallow={toggle}>Toggle</button>
<label>
<input type="checkbox" checked={corrected} notallow={toggleCorrected} />
Corrected
</label>
</div>
<div id="wrapper" style={{ justifyContent: 'center' }}>
<Motion toggled={toggled} corrected={corrected}>Hello!</Motion>
</div>
</div>
)
}

Motion.js

const changed = (initialBox, finalBox) => {
// we just mounted, so we don't have complete data yet
if (!initialBox || !finalBox) return false;

// deep compare the two boxes
return JSON.stringify(initialBox) !== JSON.stringify(finalBox);
}

const invert = (el, from, to) => {
const { x: fromX, y: fromY, width: fromWidth, height: fromHeight } = from;
const { x, y, width, height } = to;

const transform = {
x: x - fromX - (fromWidth - width) / 2,
y: y - fromY - (fromHeight - height) / 2,
scaleX: width / fromWidth,
scaleY: height / fromHeight,
};

el.style.transform = `translate(${transform.x}px, ${transform.y}px) scaleX(${transform.scaleX}) scaleY(${transform.scaleY})`;

return transform;
}

其實(shí)不是這樣的?

在這種情況下,使比例校正工作的方式是通過(guò)將子元素包裹在<div>?中,并將比例校正應(yīng)用于<div>中,這會(huì)有一些問(wèn)題:

一個(gè)運(yùn)動(dòng)組件在DOM中有兩個(gè)元素,從用戶體驗(yàn)的角度來(lái)看,這可能是個(gè)問(wèn)題

所有子組件都進(jìn)行了比例校正,不可能一個(gè)子組件被校正而另一個(gè)子組件不被校正

如果子組件也在做動(dòng)畫(huà),可能會(huì)有問(wèn)題--我沒(méi)有測(cè)試過(guò),但我認(rèn)為比例校正會(huì)導(dǎo)致問(wèn)題,因?yàn)槲覀兣で俗咏M件的坐標(biāo)空間

Framer Motion 的做法有點(diǎn)不同,我們必須讓子組件成為布局組件來(lái)選擇加入比例校正。

<motion.article layout>
<motion.h1 layout>Hello!</motion.h1> <-- is scale corrected
<p>World!</p> <-- is not scale corrected
</motion.article>

這個(gè)API意味著子組件需要能夠 "鉤住 "父組件的動(dòng)畫(huà),這讓實(shí)現(xiàn)變得更加復(fù)雜。

我選擇不以這種方式實(shí)現(xiàn),因?yàn)槲也幌朊撾x核心的比例校正概念。如果你有興趣,可以看看 Framer Motion源代碼,他們使用一種叫做 "投影節(jié)點(diǎn)( "projection nodes")"的東西來(lái)維護(hù)自己的類似DOM的運(yùn)動(dòng)組件樹(shù)。

今天的內(nèi)容就到這里,感謝大家的閱讀。

來(lái)源:https://www.nan.fyi/magic-motion

責(zé)任編輯:武曉燕 來(lái)源: 大遷世界
相關(guān)推薦

2019-12-13 16:19:15

戴爾

2024-05-11 07:56:07

編程語(yǔ)言Java字節(jié)碼

2021-06-07 09:31:50

HTML網(wǎng)頁(yè)布局CSS

2020-12-04 08:40:29

SVG動(dòng)畫(huà)元素

2021-12-28 17:52:29

Android 動(dòng)畫(huà)估值器

2020-10-09 08:15:11

JsBridge

2018-04-03 10:10:45

UPS后備式在線式

2023-11-28 08:29:31

Rust內(nèi)存布局

2018-02-28 10:27:36

2021-05-12 16:09:18

Python分布式進(jìn)程接口

2024-04-11 13:30:59

PythonProcess

2022-02-21 09:44:45

Git開(kāi)源分布式

2023-05-12 08:19:12

Netty程序框架

2021-06-30 00:20:12

Hangfire.NET平臺(tái)

2021-01-28 08:55:48

Elasticsear數(shù)據(jù)庫(kù)數(shù)據(jù)存儲(chǔ)

2019-04-17 15:16:00

Sparkshuffle算法

2024-06-25 08:18:55

2021-04-09 08:40:51

網(wǎng)絡(luò)保險(xiǎn)網(wǎng)絡(luò)安全網(wǎng)絡(luò)風(fēng)險(xiǎn)

2017-09-05 08:52:37

Git程序員命令

2012-12-25 10:14:48

英特爾智能數(shù)字標(biāo)牌
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)