需求背景
项目中需要实现一个点击加入购物车的简单效果,前期是打算用一些三方动画库类似于react-spring做的好看一些,后来由于时间以及三方库兼容问题,转而用js来简单实现。
如何实现
1.两点坐标
首先我们要实现加入购物车动效的话,一定是需要购物车加入的起始位置和结束位置,js提供了getBoundingClientRect方法来帮我们获取一个元素的DOMRect
对象,具体的信息可以上在mdn自己看一下,这里就不多说了;要注意的是,我们所使用的top和left是根据视窗左上角来计算的,而不是具体的某一个元素或者body。
这里楼主还遇到一个问题,由于组件化拆分,我需要的起始位置和结束位置不在同一个组件内,所以没办法直接用ref去获取到对应的document元素,因为使用的是react,所以就使用useContext来去获取了一下结束位置的元素(具体步骤如下,不需要请直接跳过)。
// 创建一个上下文对象导出,用于我们后面react组件树来共享数据
import React from 'react';
const MiddleContext = React.createContext(null);
export default MiddleContext;
// moveEnd: 结束位置元素
<MiddleContext.Provider value={moveEnd}>
......
</MiddleContext.Provider>
// 这里我们拿到需要的位置对象使用即可
const moveEnd = useContext(MiddleContext);
2.移动样式(完整代码在最后)
简单就是我们通过点击事件创建元素,移动元素,销毁元素三个步骤来实现。
// 元素
const body = container || document.getElementById('root');
const flyRound = document.createElement('div');
const flyChild = document.createElement('div');
flyRound.appendChild(flyChild);
body.appendChild(flyRound);
这里要注意的是body元素的设置,添加后没有找到对应的添加元素,检查代码后发现是一些三方库antd这些的弹窗类有的不是挂载在root节点下添加的,所以我们这里最好使用传入的container节点来进行挂载。
// 样式
flyRound.setAttribute('style', `
transition: all 1s cubic-bezier(0, 0.74, 0.53, 1.09) 0s;
position: absolute;
top: ${startTop}px;
left: ${startLeft}px;
z-index: 2000`);
flyChild.setAttribute('style', `
width: 20px;
height: 20px;
border-radius: 50%;
transition: all 1s ease 0s;
background: #1B66FF`);
这里我们设置一下盒子的样式
const flyTimer = setTimeout(() => {
flyRound.style.transform = `translate3d(0, ${(endTop - startTop)}px, 0)`;
flyChild.style.transform = `translate3d(${(endLeft - startLeft)}px, 0, 0)`;
const removeTimer = setTimeout(() => {
clearTimeout(flyTimer);
clearTimeout(removeTimer);
body.removeChild(flyRound);
resolve();
}, 1000);
}, 1);
这里我们通过两个盒子的translate移动来进行动画效果的展示,有一个问题就是如果我们正常设置移动的话,是直着过去的,如果需要过渡的曲线样式,我们可以通过设置父子元素的transform和盒子移动的贝塞尔曲线来进行控制移动速度曲线。
简单来说就是父元素如果是static定位的话,就是以父元素的初始位置来进行移动的,如果父元素是absolute等定位的话,就会以父元素的实时位置来进行偏移,相同的距离或者不同的距离我们通过设置初始x1,y1的速度点以及x2,y2的速度点控制曲线弯曲程度。如果不知道怎么设置的话,可以参考官网给出的compare Demo;这里设置第一个定时器是因为我们需要在样式的设置完成之后来渲染,如果没有定时器的话有可能会导致渲染异常,第二个定时器是我们需要在动画完成后对添加的元素进行一个销毁动作。
完整代码如下
if (moveStart.current && moveEnd.current) {
const startConfig = moveStart.current.getBoundingClientRect();
const endConfig = moveEnd.current.getBoundingClientRect();
const root = document.getElementsByClassName('className')[0];
await shopCartAnimate(root, { top: startConfig.top, left: startConfig.left }, { top: endConfig.top, left: endConfig.left });
}
const shopCartAnimate = (container, startConfig, endConfig) => new Promise<void>((resolve, reject) => {
// 元素
const body = container || document.getElementById('root');
const flyRound = document.createElement('div');
const flyChild = document.createElement('div');
flyRound.appendChild(flyChild);
body.appendChild(flyRound);
// 位置信息
const { top: startTop, left: startLeft } = startConfig;
const { top: endTop, left: endLeft } = endConfig;
// 样式
flyRound.setAttribute('style', `
transition: all 1s cubic-bezier(0, 0.74, 0.53, 1.09) 0s;
position: absolute;
top: ${startTop}px;
left: ${startLeft}px;
z-index: 2000`);
flyChild.setAttribute('style', `
width: 20px;
height: 20px;
border-radius: 50%;
transition: all 1s ease 0s;
background: #1B66FF`);
const flyTimer = setTimeout(() => {
flyRound.style.transform = `translate3d(0, ${(endTop - startTop)}px, 0)`;
flyChild.style.transform = `translate3d(${(endLeft - startLeft)}px, 0, 0)`;
const removeTimer = setTimeout(() => {
clearTimeout(flyTimer);
clearTimeout(removeTimer);
body.removeChild(flyRound);
resolve();
}, 1000);
}, 1);
});