日本综合一区二区|亚洲中文天堂综合|日韩欧美自拍一区|男女精品天堂一区|欧美自拍第6页亚洲成人精品一区|亚洲黄色天堂一区二区成人|超碰91偷拍第一页|日韩av夜夜嗨中文字幕|久久蜜综合视频官网|精美人妻一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問(wèn)題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營(yíng)銷解決方案
這是一篇很好的互動(dòng)式文章,F(xiàn)ramerMotion布局動(dòng)畫

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

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

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

  • 布局變化:它們是什么,何時(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)畫

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

.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

}

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)畫,但它有兩個(gè)主要的缺點(diǎn):

  • 不能把所有東西都做成動(dòng)畫。例如,不能對(duì)justify-content?的變化制作動(dòng)畫,因?yàn)閖ustify-content不是一個(gè)可動(dòng)畫的屬性。
  • 性能問(wèn)題。涉及布局變化的CSS動(dòng)畫通常比基于 transform 的動(dòng)畫更昂貴,所以你可能會(huì)發(fā)現(xiàn)你的動(dòng)畫在低端設(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)畫通常比其他CSS動(dòng)畫更昂貴,因?yàn)樗绊懙街車钠渌?。這是因?yàn)闉g覽器必須在動(dòng)畫的每一幀中重新計(jì)算頁(yè)面的布局--對(duì)于一個(gè)60FPS的動(dòng)畫來(lái)說(shuō),這意味著每秒鐘要計(jì)算60次!

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

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

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

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

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

是的,可以!

FLIP

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

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

First

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

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

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

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 (






)
}

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

}

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 (






)
}

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
;
}

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)畫化為零,讓正方形動(dòng)畫化到它的最終位置。

有多種方法可以實(shí)現(xiàn)這個(gè)動(dòng)畫;我個(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
;
}

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)畫的大小

到目前為止,我們只用FLIP來(lái)制作位置變化的動(dòng)畫。但對(duì)于大小來(lái)說(shuō),我們可以用同樣的方法嗎我們?cè)囍鴱?fù)制下面的動(dòng)畫,在這個(gè)動(dòng)畫中,正方形被拉伸到充滿整個(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)畫到0?,而是將比例動(dòng)畫到1(如果我們將比例動(dòng)畫到0,元素將完全消失):

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

使用 position 固定大小

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

嗯,這看起來(lái)有點(diǎn)不對(duì)勁。這里發(fā)生了什么?如果我們?cè)?nbsp;play?  步驟之前暫停動(dòng)畫,我們可以看到在 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)畫應(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)畫,可以無(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)畫相結(jié)合呢?

嘗試

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

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

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

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

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

正確的縮放時(shí)間

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

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

正常情況下,"正確" 反轉(zhuǎn)比例不會(huì)以與父動(dòng)畫相同的方式變化,它有點(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 (






Hello!


)
}

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ò)將子元素包裹在

?中,并將比例校正應(yīng)用于
中,這會(huì)有一些問(wèn)題:

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

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

如果子組件也在做動(dòng)畫,可能會(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)選擇加入比例校正。


Hello! <-- is scale corrected

World!

<-- is not scale corrected

這個(gè)API意味著子組件需要能夠 "鉤住 "父組件的動(dòng)畫,這讓實(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


文章標(biāo)題:這是一篇很好的互動(dòng)式文章,F(xiàn)ramerMotion布局動(dòng)畫
鏈接地址:http://www.dlmjj.cn/article/dhsgohe.html