需求

项目中使用 Antd 表格组件, 勾选多行数据时, 通过点击添加或加入的方式实现购物车添加动画

  • 表格中多选选中后加入到购物车中(右上角或指定位置购物车)
  • 点击加入时, 需要有动画效果(更优的方案是抛物线或可配置的路径)
  • 支持动画自定义

在线DEMO
更多参考React 动画
抛物线运动 JS 绘制购物车抛物线动画
Add to cart fly animation
实现抛物线的算法
更多animation参考
获取位置:

**

实现逻辑

  1. 首先实现一个定位的小球(可以是空的标签, 用颜色代替 也可以是一个自定义组件, 提供出API)
  2. 获取到目标元素在页面中的位置 offsetTop 和 offsetLeft的值
  3. 使用定位将子组件包裹在内, 外层使用相对定位, 而 cellball 使用绝对定位, 计算子元素(children)的位置从而计算出left 和 right 的值
  4. 定义好运行的动画 animation, ease即可(可以使用如 aniate.css中的动画效果)
  5. 根据目标元素 和运动元素的位置进行计算出需要偏移的量 使用 transform 结合 translate进行偏移, 支持 transform 其他的属性如 scale 等操作(不排除有图片作为运动元素)
  6. 需要提供通用的 context 组件进行包裹, 且可以提供获取到目标元素的方法, 比如提供 getTarget() => document.getElementById() 方式获取到目标元素的位置

    API

    | 属性 | 描述 | 类型 | 默认值 | | —- | —- | —- | —- | | getTarget | 获取目标元素的方法 | () => document.getElementById() | void(如果为空就需要指定一个默认值或者是必须使用提供的 context进行包裹) | | motionNode | 指定动画的节点 | ReactNode | 默认使用自带的小球, 需要在外层包裹一层用于定位 | |
    | 运动动画 | animation | 执行动画的方式, linear | ease | 自定义的贝塞尔曲线等 | | | | | 支持 自定义的 transform运动, 排除translate 如果传递需要使用正则匹配掉 | | | 是否是抛物线方式 | | false, 当设置为 true时需要进行计算抛物线的轨迹 | | | 单次或批量 | | 单次: 只允许有一个节点进行动画
    批量: 如table 选中多行时需要考虑是依次执行或者是同时执行, 需要提供额外的API | | | 触发方法 | | | | | 监听动画执行完毕效果 | | 当动画执行完毕后, 目标元素的接收动画是否需要执行 | | | 最大执行数量 | | 不可能无限制个数同时执行 |

初版实现

点击查看【codepen】

大致思路为了防止频繁的重绘和重排, 先对每一个需要添加动画的节点元素添加上指定运动元素

  • 首先计算获取到目标元素的位置和高度宽度信息, 通常不是在同一个父组件下, 此时需要提供一个 context或者有其他更好的方法, 总之需要提供出目标元素的信息
  • 对需要加入购物车的源节点上添加带动画的组件(内部包含有自身位置及大小, 且优先使用css添加 class实现基础的运动样式)

需要优化的点: 直线运动太过于单调, 虽然可以实现需求, 但对API的设定及效果优化仍然需要提高

CellBall.tsx

  1. /**
  2. * 表格渲染的小球
  3. * 1. 可能需要使用 js 控制 keyframe 的路径值
  4. * 2. 使用 定时器递增
  5. * 3. 计算 children 的宽度 和高度, 更精准的计算到小球位置
  6. * 4. 通常利用到图片, 如果封装到组件中使用时, 需要创建缩略图
  7. * 5. 购物车如果分类展示, 也是列表时, 需要添加不同的分类或物品时新增一个购物车的目标元素
  8. */
  9. import * as React from "react";
  10. import * as ReactDOM from "react-dom";
  11. import "./animate.css";
  12. export interface CellBallPropsInt {
  13. children?: React.ReactNode;
  14. targetPos: PosType;
  15. fly: boolean;
  16. }
  17. export type PosType = {
  18. top: number;
  19. left: number;
  20. };
  21. // 元素高度, 宽度 位置信息
  22. export type EleType = PosType & {
  23. width: number;
  24. height: number;
  25. };
  26. // function getOffset(targetPos: PosType, pos: PosType) {
  27. // return {
  28. // offsetLeft: targetPos.left - pos.left,
  29. // offsetTop: pos.top - targetPos.top
  30. // };
  31. // }
  32. const CellBall: React.FC<CellBallPropsInt> = props => {
  33. const ballRef = React.useRef<any>();
  34. const [pos, setPos] = React.useState<EleType>({
  35. left: 0,
  36. top: 0,
  37. width: 0,
  38. height: 0
  39. });
  40. const [{ left, top }, setLeftTop] = React.useState({ left: 0, top: 0 });
  41. // const [{ offsetLeft, offsetTop }, setOffsetPos] = React.useState(
  42. // getOffset(props.targetPos, pos)
  43. // );
  44. React.useEffect(() => {
  45. if (ballRef.current) {
  46. const { top, left, width, height } = (ReactDOM.findDOMNode(
  47. ballRef.current
  48. ) as Element).getBoundingClientRect();
  49. setPos({ left, top, width, height });
  50. }
  51. }, []);
  52. /** 重新计算位置 */
  53. // React.useEffect(() => {
  54. // setOffsetPos(getOffset(props.targetPos, pos));
  55. // }, [props.targetPos, pos]);
  56. return (
  57. <span ref={ballRef} className="check-ball-wrapper">
  58. {props.children}
  59. <span
  60. className={`ball ${props.fly ? "show" : ""}`}
  61. style={{
  62. // -2, -1 为了和 checkbox 居中定位
  63. // 计算 -16 是 checkbox 的高度 和宽度, 理应动态计算 children 的高度和宽度
  64. left: props.fly
  65. ? props.targetPos.left
  66. : pos.left - (pos.width - 16) / 2,
  67. top: props.fly
  68. ? props.targetPos.top
  69. : pos.top - (pos.height - 16 - 2) / 2
  70. }}
  71. />
  72. </span>
  73. );
  74. };
  75. export default CellBall;

animate.css

  1. /**
  2. 1. 样式调整
  3. 2. 支持自定义的 transition 动画效果
  4. 3. 如果可以的话引入比如 animate.css 等优秀的 css 动画库, 只引入比较常见的几种动画即可
  5. 4. 开放出更多 bezier 曲线
  6. 5. 如何计算出抛物线动画路径 ?
  7. **/
  8. .check-ball-wrapper {
  9. position: relative;
  10. z-index: 10;
  11. }
  12. .ball {
  13. display: inline-block;
  14. position: fixed;
  15. z-index: -1;
  16. height: 20px;
  17. width: 20px;
  18. border-radius: 100%;
  19. background: blueviolet;
  20. }
  21. .show {
  22. opacity: 1;
  23. /* animation: fly 1s; */
  24. transition: all cubic-bezier(0.28, -0.57, 0.27, 1.55) 2s;
  25. }
  26. @keyframes fly {
  27. 0% {
  28. left: 0%;
  29. }
  30. 100% {
  31. left: 100%;
  32. top: 100%;
  33. }
  34. }

暂定API

  1. // 通过使用 getBoundingClientRect 获取到屏幕中元素的基础位置信息
  2. type TargetPosType = {
  3. top: number;
  4. left: number;
  5. /** 后续考虑计算问题 */
  6. width?: number;
  7. height?: number;
  8. }
API 描述 类型 默认值
targetPos 指定动画元素的位置信息 TargetPosType { top: 0, left: 0 }
fly 是否开启动画 boolean false

后续设计

  • 支持引入 animate.css 动画库的常见动画效果
  • 支持抛物线的形式动画
  • 支持图片缩略图(比如: 购物时的展示图片缩略图而不是小圆球)
  • 假设购物车是多种不同商品的列表, 点击添加时,如果列表中已经存在, 则可以直接使用 targetPos 定位, 但如果不存在, 需要新构建一个目标节点同时获取到位置