前陣子种樱,開發(fā)過程中需要用到購物車動(dòng)畫,所以封裝了動(dòng)畫hooks俊卤,在此做一下總結(jié)歸納嫩挤。
一、思考
首先消恍,購物車動(dòng)畫的軌跡是一個(gè)拋物線效果岂昭,這個(gè)我們可以通過CSS動(dòng)畫來實(shí)現(xiàn)。其次狠怨,我們的拋物線需要一個(gè)起始點(diǎn)约啊、一個(gè)目標(biāo)點(diǎn)邑遏、一個(gè)運(yùn)動(dòng)小球。
然后恰矩,通過計(jì)算起始點(diǎn)和目標(biāo)點(diǎn)兩者之間 x 軸和 y 軸的距離记盒,然后通過 CSS 來改變運(yùn)動(dòng)小球的位置和移動(dòng)速度,從而實(shí)現(xiàn)加入購物車效果外傅。
那么纪吮,這個(gè)拋物線動(dòng)畫效果如何實(shí)現(xiàn)?
高中物理告訴我們萎胰,當(dāng)物體運(yùn)動(dòng)時(shí)碾盟,X軸方向上和Y軸方向上的速度不一致時(shí),物體的運(yùn)動(dòng)效果就是拋物線技竟,類似我們向外拋球巷疼,小球的運(yùn)動(dòng)軌跡。
所以灵奖,想要有拋物線效果嚼沿,我們只需要控制運(yùn)動(dòng)小球,從起始點(diǎn)運(yùn)動(dòng)到目標(biāo)點(diǎn)的過程中瓷患,X軸和Y軸方向上的速度不一致即可骡尽。
因此,我們可以通過X軸方向上的速度不變擅编,通過Y軸方向上的速度變化攀细。
那么,如何控制Y軸上的速度變化爱态?
搜索前端 CSS 樣式谭贪,我們可以發(fā)現(xiàn),可以使用 transition-timing-function: linear|ease|ease-in|ease-out|ease-in-out|cubic-bezier(n,n,n,n);
屬性來實(shí)現(xiàn)過渡效果的速度變化锦担。
其中俭识,三階貝塞爾曲線cubic-bezier(x1, y1, x2, y2)
: 四個(gè)參數(shù)值分別在 0 到 1 之間,其中 (x1, y1)(x2, y2) 是控制曲線的變化程度洞渔。
二本辐、基本框架
我們思考一下,想要把這個(gè)動(dòng)畫效果封裝起來通用,我們需要傳入哪些必傳參數(shù)? 需要暴露哪些參數(shù)或方法給外層組件調(diào)用? 需要提供哪個(gè)參數(shù)便于個(gè)性化擴(kuò)展?
- 需要起始
Dom
節(jié)點(diǎn)慎皱、目標(biāo)Dom
節(jié)點(diǎn)环葵; - 需要暴露
running
方法,用于開啟動(dòng)畫效果宝冕; - 需要運(yùn)動(dòng)小球张遭,小球包含兩層,外層
flyOuter
控制X軸勻速運(yùn)動(dòng)地梨,內(nèi)層flyInner
控制Y軸變速運(yùn)動(dòng)菊卷; - 需要提供屬性,支持自定義小球的內(nèi)容
children
宝剖、小球內(nèi)外層樣式flyOuterStyle / flyInnerStyle
洁闰、小球運(yùn)動(dòng)時(shí)間設(shè)置runTime
、小球開始運(yùn)動(dòng)回調(diào)beforeRun
万细、小球開始運(yùn)動(dòng)回調(diào)afterRun
扑眉;
hook封裝實(shí)現(xiàn)
import React, { useRef, useEffect, useImperativeHandle } from 'react';
import ReactDOM from 'react-dom';
/**
* 動(dòng)畫球
* @params children - 小球擴(kuò)展內(nèi)容
* @params flyOuterStyle - 小球外層擴(kuò)展樣式
* @params flyInnerStyle - 小球內(nèi)層擴(kuò)展樣式
* @params runTime - 小球運(yùn)動(dòng)時(shí)間
* @params ref - 小球dom實(shí)例
*/
const flyOuter = React.forwardRef(
({ children, flyOuterStyle = {}, flyInnerStyle = {}, runTime = 0.8 }, ref) => {
const flyOuterRef = useRef();
const flyInnerRef = useRef();
useImperativeHandle(ref, () => ({ flyOuterRef, flyInnerRef }));
// 運(yùn)動(dòng)小球外層樣式
const flyOuter_Style = Object.assign(
{
position: 'absolute',
width: '20px',
height: '20px',
transition: `transform ${runTime}s`,
display: 'none',
margin: ' -20px 0 0 -20px',
transitionTimingFunction: 'linear',
zIndex: 3,
},
flyOuterStyle,
);
// 運(yùn)動(dòng)小球內(nèi)層樣式
const flyInner_Style = Object.assign(
{
position: 'absolute',
width: '100%',
height: '100%',
borderRadius: '50%',
backgroundColor: '#FF8A2B',
color: '#ffffff',
textAlign: 'center',
lineHeight: '1',
transition: `transform ${runTime}s`,
justifyContent: 'center',
alignItems: 'center',
// transitionTimingFunction: 'cubic-bezier(.55,0,.85,.36)', // 向上拋物線的右邊
transitionTimingFunction: 'cubic-bezier(0, 0, .25, 1.3)', // 向下拋物線的左邊
},
flyInnerStyle,
);
return (
<div style={flyOuter_Style} ref={flyOuterRef}>
<div style={flyInner_Style} ref={flyInnerRef}>
{children}
</div>
</div>
);
},
);
/**
* 拋物線動(dòng)畫效果
* @params startRef - 起始點(diǎn)dom節(jié)點(diǎn)
* @params endRef - 目標(biāo)點(diǎn)dom節(jié)點(diǎn)
* @params flyOuterStyle - 小球外層擴(kuò)展樣式
* @params flyInnerStyle - 小球內(nèi)層擴(kuò)展樣式
* @params runTime - 小球運(yùn)動(dòng)時(shí)間
* @params beforeRun - 小球開始運(yùn)動(dòng)回調(diào)
* @params afterRun - 小球結(jié)束運(yùn)動(dòng)回調(diào)
* @params children - 小球擴(kuò)展內(nèi)容
* @returns { running } - 小球開始運(yùn)動(dòng)函數(shù)
*/
export default function useParabola(
{
startRef,
endRef,
flyOuterStyle,
flyInnerStyle,
runTime = 800,
beforeRun = () => {},
afterRun = () => {},
},
children,
) {
const containerRef = useRef(document.createElement('div'));
const innerRef = useRef();
let isRunning = false;
// 掛載到dom上
useEffect(() => {
const container = containerRef.current;
document.body.appendChild(container);
return () => {
document.body.removeChild(container);
};
}, []);
useEffect(() => {
if (startRef?.current && endRef?.current) {
ReactDOM.render(
React.createElement(
flyOuter,
{ ref: innerRef, flyOuterStyle, flyInnerStyle, runTime: runTime / 1000 },
children,
),
containerRef.current,
);
}
}, [startRef, endRef]); // eslint-disable-line
function running() {
if (startRef && endRef && innerRef) {
beforeRun();
const flyOuterRef = innerRef.current.flyOuterRef.current;
const flyInnerRef = innerRef.current.flyInnerRef.current;
// 現(xiàn)在起點(diǎn)距離終點(diǎn)的距離
const startDot = startRef.current.getBoundingClientRect();
const endDot = endRef.current.getBoundingClientRect();
// 中心點(diǎn)的水平垂直距離
const offsetX = endDot.left + endDot.width / 4 - (startDot.left + startDot.width / 2);
// let offsetY = endDot.top + endDot.height / 2 - (startDot.top + startDot.height / 2);
const offsetY = endDot.top + endDot.height / 4 - (startDot.top + startDot.height / 2);
// 頁面滾動(dòng)尺寸
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop || 0;
const scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft || 0;
if (!isRunning) {
// 初始定位
flyOuterRef.style.display = 'block';
flyOuterRef.style.left = `${
startDot.left + scrollLeft + startRef.current.clientWidth / 2
}px`;
flyOuterRef.style.top = `${startDot.top + scrollTop + startRef.current.clientHeight / 2}px`;
// 開始動(dòng)畫
flyOuterRef.style.transform = `translateX(${offsetX}px)`;
flyInnerRef.style.transform = `translateY(${offsetY}px)`;
// 動(dòng)畫標(biāo)志量
isRunning = true;
setTimeout(() => {
flyOuterRef.style.display = 'none';
flyOuterRef.style.left = '';
flyOuterRef.style.top = '';
flyOuterRef.style.transform = '';
flyInnerRef.style.transform = '';
isRunning = false;
afterRun();
}, runTime);
}
}
}
return { running };
}
三、測(cè)試用例
實(shí)現(xiàn)效果:
js代碼
import React, { useRef, useState } from 'react';
import { Button, notification } from 'antd';
import { ShoppingCartOutlined, PayCircleOutlined } from '@ant-design/icons';
import useParabola from '@/hooks/use-parabola';
import styles from './index.less';
/*
* @Description: 購物車動(dòng)畫-demo
* @version: 0.0.1
* @Date: 2020-04-20 23:21:33
*/
export default React.forwardRef(() => {
const [num, setNum] = useState(1);
const startRef = useRef();
const endRef_1 = useRef();
const endRef_2 = useRef();
const endRef_3 = useRef();
const endRef_4 = useRef();
const res_1 = useParabola(
{
startRef,
endRef: endRef_1,
flyOuterStyle: {
width: '40px',
height: '40px',
transition: 'transform 3s',
margin: ' -40px 0 0 -40px',
},
flyInnerStyle: {
color: '#FF0000',
transition: 'transform 3s',
lineHeight: '40px',
},
runTime: 3000,
beforeRun: () => {
notification.warning({ message: '12號(hào)球開始運(yùn)動(dòng)啦啦~~' });
},
afterRun: () => {
notification.success({ message: '12號(hào)球運(yùn)動(dòng)結(jié)束啦啦~~' });
},
},
<span>12</span>,
);
const res_2 = useParabola(
{
startRef,
endRef: endRef_2,
flyInnerStyle: {
transitionTimingFunction: 'cubic-bezier(.55,0,.85,.36)',
},
},
'2',
);
const res_3 = useParabola(
{
startRef,
endRef: endRef_3,
flyOuterStyle: { transition: 'transform 2.5s' },
flyInnerStyle: { transition: 'transform 2.5s' },
runTime: 2500,
},
'3',
);
const res_4 = useParabola(
{
startRef,
endRef: endRef_4,
flyInnerStyle: {
transitionTimingFunction: 'cubic-bezier(.55,0,.85,.36)',
},
},
'4',
);
function startRunning() {
if (num % 4 === 1) {
res_1.running(1);
}
if (num % 4 === 2) {
res_2.running(2);
}
if (num % 4 === 3) {
res_3.running(3);
}
if (num % 4 === 0) {
res_4.running(4);
}
setNum(num + 1);
}
return (
<div className={styles['cart-animation']}>
<div className={styles.center}>
<div ref={startRef}>
<Button danger icon={<PayCircleOutlined />} onClick={startRunning}>
發(fā)射中心
</Button>
</div>
</div>
<div className={styles.left}>
<div ref={endRef_1}>
<Button type="primary" icon={<ShoppingCartOutlined />} className={styles['left-top']}>
購物車1號(hào)
</Button>
</div>
<div ref={endRef_2}>
<Button type="primary" icon={<ShoppingCartOutlined />} className={styles['left-bottom']}>
購物車2號(hào)
</Button>
</div>
</div>
<div className={styles.right}>
<div ref={endRef_3}>
<Button type="primary" icon={<ShoppingCartOutlined />} className={styles['right-top']}>
購物車3號(hào)
</Button>
</div>
<div ref={endRef_4}>
<Button type="primary" icon={<ShoppingCartOutlined />} className={styles['right-bottom']}>
購物車4號(hào)
</Button>
</div>
</div>
</div>
);
});
css代碼
@import '~antd/lib/style/themes/default.less';
.cart-animation {
position: relative;
height: 300px;
.center {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.left,
.right {
position: absolute;
top: 50px;
}
.left {
left: 0;
}
.right {
right: 0;
}
.left-top,
.right-top {
margin-bottom: 200px;
}
button {
display: block;
}
}