课表拖拽(一)拖拽实现

5,082 阅读2分钟

最近接到一个任务,要求实现一个课表拖拽的功能,支持快速修改个人日程时间。项目采用taro框架。

751CC6CFE376180E3B70EA0372593F85.jpg

基于性能的抉择

container采用grid布局,7colum+12row,共84个单元格

拖拽的方式有两种

  • 盒子跟随手指,并实时显示松手后落入的位置,松手时寻找一个离手指最近的单元格放入

  • 盒子实时在格子之内,根据手指位置实时计算填入的格子,将盒子放入

哪一种性能更高??????

显然第一种方案在第二种方案上多了盒子实时跟随手指这个额外操作,性能不占优势。

catch-move避免滑动穿透

因为课表支持左右滑动查看自己每一周的课程安排,采用了一个Swiper包裹在container之外,在滑动时会带动Swiper的滑动,那该怎么办?????????

不妨请教学长,经过学长的指导,告诉了我一个api

屏幕截图 2024-10-15 105302.png

不得不感慨阅读官方文档的重要性(老实了,以后必须多看官方文档)

如何根据手指的位置,计算所在单元格

const unitwith = 350 / 7;
const unitheight = 600 / 12;

先得到了每一个单元格的宽高

然后通过滑动的事件对象可以获取当前的(x,y),那么动态设置grid样式就可以实现

  const getGridPositionByXY = (xp, yp) => {
    return `gridColumn:${Math.floor(xp / unitwith)} ;gridRow:${Math.floor(
      yp / unitheight
    )}/${Math.floor(yp / unitheight) + 2}`;
  };

handleTouchMove函数的实现

我们需要两个响应式的变量x,y,通过在handleTouchMove函数中修改x,y来带动style的修改

 const [x, setX] = useState(350 / 7);
 const [y, setY] = useState(600 / 12);
 
 const handleTouchMove =(e) => {
    setX(e.changedTouches[0].clientX  + 50);
    setY(e.changedTouches[0].clientY  + 50);
  };

性能优化(节流)

节流:在一定时间内,无论函数被触发多少次,函数只会在固定的时间间隔内执行一次

为防止handleTouchMove的触发频率太高,我们采用节流函数来让它在固定时间内只执行一次

function Throttle(fn, delay) {
  let timer = null;
  return function () {
    if (timer) return;
    timer = setTimeout(() => {
      fn.apply(this, arguments);
      timer = null;
    }, delay);
  };
}

const handleTouchMove = Throttle((e) => {
    setX(e.changedTouches[0].clientX  + 50);
    setY(e.changedTouches[0].clientY  + 50);
}, 10);

提升交互性

我们可以让用户长按激活,随后才能滑动,并且在激活的时候触发震动

直接贴完整代码在这里

import { View, Swiper, SwiperItem } from "@tarojs/components";
import { useState, useRef } from "react";
import Taro, { useLoad } from "@tarojs/taro";
import "./index.css";

function Throttle(fn, delay) {
  let timer = null;
  return function () {
    if (timer) return;
    timer = setTimeout(() => {
      fn.apply(this, arguments);
      timer = null;
    }, delay);
  };
}
export default function Index() {
  const [isLongPress, setIsLongPress] = useState(false);
  useLoad(() => {
    console.log("Page loaded.");
  });
  const timer = useRef(null);

  const [x, setX] = useState(350 / 7);
  const [y, setY] = useState(600 / 12);
  // const [StartPosition, setStartPosition] = useState({ x: 0, y: 0 });

  const unitwith = 350 / 7;
  const unitheight = 600 / 12;
  const getGridPositionByXY = (xp, yp) => {
    return `gridColumn:${Math.floor(xp / unitwith)} ;gridRow:${Math.floor(
      yp / unitheight
    )}/${Math.floor(yp / unitheight) + 2}`;
  };

  const handleTouchMove = Throttle((e) => {
    if (!isLongPress) return;
    setX(e.changedTouches[0].clientX  + 50);
    setY(e.changedTouches[0].clientY  + 50);
  }, 10);

  return (
    <View className='index'>
      <Swiper circular style={{ width: "100vw", height: "100vh" }}>
        <SwiperItem>
          <view className='container'>
            <view
              style={getGridPositionByXY(x, y)}
              className={`items-1 ${isLongPress ? "pressActive" : ""}`}
              catch-move
              onTouchStart={() => {
                timer.current = setTimeout(() => {
                  setIsLongPress(true);
                  Taro.vibrateShort();
                  // console.log("长按");
                }, 1000);
              }}
              onTouchMove={handleTouchMove}
              onTouchEnd={() => {
                clearTimeout(timer.current);
                setIsLongPress(false);
              }}
            ></view>
            <view className="items-2">2</view>
          </view>
        </SwiperItem>
        <SwiperItem>
          <view className="container">
            <view className="items-1">no</view>
          </view>
        </SwiperItem>
        <SwiperItem>
          <view className="container">
            <view className="items-1" catch-move></view>
          </view>
        </SwiperItem>
        <SwiperItem>
          <view className="container">
            <view className="items-1" catch-move></view>
          </view>
        </SwiperItem>
        <SwiperItem>
          <view className="container">
            <view className="items-1" catch-move></view>
          </view>
        </SwiperItem>
      </Swiper>
    </View>
  );
}

//index.css
.container {
  width: 700px;
  height: 1200px;
  background-color: #ccc;
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  grid-template-rows: repeat(12, 1fr);
}
.griditems-1 {
  border: 1px solid #fff;
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: cadetblue;
}
.griditems-2 {
  border: 1px solid #fff;
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: aquamarine;
}
.griditems-3 {
  border: 1px solid #fff;
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: burlywood;
}
.griditems-4 {
  border: 1px solid #fff;
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: darkcyan;
}
.griditems-5 {
  border: 1px solid #fff;
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: darkgoldenrod;
}

.items-1 {
  grid-column: 1; /* 从第1列开始,到第2列结束 */
  grid-row: 1 / 4;
  border-radius: 10px;
  border: 1px solid #fff;
  background-color: burlywood;
}
.items-2 {
  grid-column: 3;
  grid-row: 1 / 4;
  border-radius: 10px;
  border: 1px solid #fff;
  background-color: burlywood;
}
.pressActive {
  border-radius: 10px;
  border: 1px solid #fff;
  background-color: #fff;
  opacity: 0.5;
}

下一期将介绍如何控制方块不重合,以及在展开后方块的处理和对多方块的情况怎么单独管理每一个方块的情况