需求
项目中使用 Antd 表格组件, 勾选多行数据时, 通过点击添加或加入的方式实现购物车添加动画
- 表格中多选选中后加入到购物车中(右上角或指定位置购物车)
- 点击加入时, 需要有动画效果(更优的方案是抛物线或可配置的路径)
- 支持动画自定义
在线DEMO
更多参考React 动画
抛物线运动 JS 绘制购物车抛物线动画
Add to cart fly animation
实现抛物线的算法
更多animation参考
获取位置:
实现逻辑
- 首先实现一个定位的小球(可以是空的标签, 用颜色代替 也可以是一个自定义组件, 提供出API)
- 获取到目标元素在页面中的位置 offsetTop 和 offsetLeft的值
- 使用定位将子组件包裹在内, 外层使用相对定位, 而 cellball 使用绝对定位, 计算子元素(children)的位置从而计算出left 和 right 的值
- 定义好运行的动画 animation, ease即可(可以使用如 aniate.css中的动画效果)
- 根据目标元素 和运动元素的位置进行计算出需要偏移的量 使用 transform 结合 translate进行偏移, 支持 transform 其他的属性如 scale 等操作(不排除有图片作为运动元素)
- 需要提供通用的 context 组件进行包裹, 且可以提供获取到目标元素的方法, 比如提供 getTarget() => document.getElementById() 方式获取到目标元素的位置
API
| 属性 | 描述 | 类型 | 默认值 | | —- | —- | —- | —- | | getTarget | 获取目标元素的方法 | () => document.getElementById() | void(如果为空就需要指定一个默认值或者是必须使用提供的 context进行包裹) | | motionNode | 指定动画的节点 | ReactNode | 默认使用自带的小球, 需要在外层包裹一层用于定位 | |
| 运动动画 | animation | 执行动画的方式, linear | ease | 自定义的贝塞尔曲线等 | | | | | 支持 自定义的 transform运动, 排除translate 如果传递需要使用正则匹配掉 | | | 是否是抛物线方式 | | false, 当设置为 true时需要进行计算抛物线的轨迹 | | | 单次或批量 | | 单次: 只允许有一个节点进行动画
批量: 如table 选中多行时需要考虑是依次执行或者是同时执行, 需要提供额外的API | | | 触发方法 | | | | | 监听动画执行完毕效果 | | 当动画执行完毕后, 目标元素的接收动画是否需要执行 | | | 最大执行数量 | | 不可能无限制个数同时执行 |
初版实现
大致思路为了防止频繁的重绘和重排, 先对每一个需要添加动画的节点元素添加上指定运动元素
- 首先计算获取到目标元素的位置和高度宽度信息, 通常不是在同一个父组件下, 此时需要提供一个 context或者有其他更好的方法, 总之需要提供出目标元素的信息
- 对需要加入购物车的源节点上添加带动画的组件(内部包含有自身位置及大小, 且优先使用css添加 class实现基础的运动样式)
需要优化的点: 直线运动太过于单调, 虽然可以实现需求, 但对API的设定及效果优化仍然需要提高
CellBall.tsx
/*** 表格渲染的小球* 1. 可能需要使用 js 控制 keyframe 的路径值* 2. 使用 定时器递增* 3. 计算 children 的宽度 和高度, 更精准的计算到小球位置* 4. 通常利用到图片, 如果封装到组件中使用时, 需要创建缩略图* 5. 购物车如果分类展示, 也是列表时, 需要添加不同的分类或物品时新增一个购物车的目标元素*/import * as React from "react";import * as ReactDOM from "react-dom";import "./animate.css";export interface CellBallPropsInt {children?: React.ReactNode;targetPos: PosType;fly: boolean;}export type PosType = {top: number;left: number;};// 元素高度, 宽度 位置信息export type EleType = PosType & {width: number;height: number;};// function getOffset(targetPos: PosType, pos: PosType) {// return {// offsetLeft: targetPos.left - pos.left,// offsetTop: pos.top - targetPos.top// };// }const CellBall: React.FC<CellBallPropsInt> = props => {const ballRef = React.useRef<any>();const [pos, setPos] = React.useState<EleType>({left: 0,top: 0,width: 0,height: 0});const [{ left, top }, setLeftTop] = React.useState({ left: 0, top: 0 });// const [{ offsetLeft, offsetTop }, setOffsetPos] = React.useState(// getOffset(props.targetPos, pos)// );React.useEffect(() => {if (ballRef.current) {const { top, left, width, height } = (ReactDOM.findDOMNode(ballRef.current) as Element).getBoundingClientRect();setPos({ left, top, width, height });}}, []);/** 重新计算位置 */// React.useEffect(() => {// setOffsetPos(getOffset(props.targetPos, pos));// }, [props.targetPos, pos]);return (<span ref={ballRef} className="check-ball-wrapper">{props.children}<spanclassName={`ball ${props.fly ? "show" : ""}`}style={{// -2, -1 为了和 checkbox 居中定位// 计算 -16 是 checkbox 的高度 和宽度, 理应动态计算 children 的高度和宽度left: props.fly? props.targetPos.left: pos.left - (pos.width - 16) / 2,top: props.fly? props.targetPos.top: pos.top - (pos.height - 16 - 2) / 2}}/></span>);};export default CellBall;
animate.css
/**1. 样式调整2. 支持自定义的 transition 动画效果3. 如果可以的话引入比如 animate.css 等优秀的 css 动画库, 只引入比较常见的几种动画即可4. 开放出更多 bezier 曲线5. 如何计算出抛物线动画路径 ?**/.check-ball-wrapper {position: relative;z-index: 10;}.ball {display: inline-block;position: fixed;z-index: -1;height: 20px;width: 20px;border-radius: 100%;background: blueviolet;}.show {opacity: 1;/* animation: fly 1s; */transition: all cubic-bezier(0.28, -0.57, 0.27, 1.55) 2s;}@keyframes fly {0% {left: 0%;}100% {left: 100%;top: 100%;}}
暂定API
// 通过使用 getBoundingClientRect 获取到屏幕中元素的基础位置信息type TargetPosType = {top: number;left: number;/** 后续考虑计算问题 */width?: number;height?: number;}
| API | 描述 | 类型 | 默认值 |
|---|---|---|---|
| targetPos | 指定动画元素的位置信息 | TargetPosType | { top: 0, left: 0 } |
| fly | 是否开启动画 | boolean | false |
后续设计
- 支持引入 animate.css 动画库的常见动画效果
- 支持抛物线的形式动画
- 支持图片缩略图(比如: 购物时的展示图片缩略图而不是小圆球)
- 假设购物车是多种不同商品的列表, 点击添加时,如果列表中已经存在, 则可以直接使用 targetPos 定位, 但如果不存在, 需要新构建一个目标节点同时获取到位置
