拖拽排序是一个很常见的业务。这不我这次需求就有一个用户关注列表拖拽的功能,然后是taro小程序。对于我这种cv工程师来说,首先想到的就是打开github,找开源库,能copy就copy,但好像都不兼容小程序,但需求又不能不做吧,只能自己实现一个。
在自己实现之前也看了别人的拖拽是怎么写的,比如看了下 react-sortable-hoc 的源码,他的实现大概是当鼠标按下时隐藏当前的项,然后克隆一个当前的节点进行拖拽,去计算位置计算位置在列表中的下标,并且去处理其他项是向上移动还是向下移动,这块它使用的translate去实现的,最后根据计算出来的索引去更新列表,触发render页面重新渲染。
效果
先看看dom代码吧
<View
className="followers-list-wrap"
style={{
overflowY: draging.status ? "hidden" : "auto",
}}
>
{dragList.map((item, index) => {
return (
<View
key={item[keyName]}
id={`drag-${item[keyName]}`}
className="row-wrap"
style={{
backgroundColor: "#fff",
zIndex: index === startIndex ? 10 : 1,
transform: getTranslateY(index),
}}
>
<View className="row-item">
<View
className="row-item__left"
onTouchStart={(e) => {
e.stopPropagation();
onTouchStart(e, item, index);
}}
onTouchMove={(e) => {
e.stopPropagation();
onTouchMove(e, item, index);
}}
onTouchEnd={(e) => {
e.stopPropagation();
onTouchEnd(e, item, index);
}}
>
<Text>{index}</Text>
</View>
</View>
</View>
);
})}
</View>
再来定义一些变量用来存储一些状态,位置,索引。
const { items, keyName = "id", dragRowItemCom } = props;
const [dragList, setDragList] = useState<Array<T>>(items);
// 拖拽item的高度
const dragItemHeight = useRef(0);
// 起始y轴坐标
const [startY, setStartY] = useState<number>(0);
// 起始索引
const [startIndex, setStartIndex] = useState<number>(0);
// 结束索引
const [endIndex, setEndIndex] = useState<number>(0);
// 拖动项
const [draging, setDraging] = useState<{ status: boolean; direction: DragDirection; y: number; difference: number; transform: string }>({
status: false, //状态 - 按下时设置为true,结束设置为false
direction: DragDirection.UP, // 拖拽的方向
y: 0, //拖动时y轴的坐标
difference: 0, // 拖拽时y轴坐标 - 按下时的坐标
transform: "", // 存储拖拽的transform的值
});
/**
* 获取item的高度
*/
useEffect(() => {
Taro.nextTick(() => {
const idStr = `#drag-${dragList[0][keyName]}`;
Taro.createSelectorQuery()
.select(idStr)
.boundingClientRect()
.exec((res) => {
dragItemHeight.current = res[0].height;
});
});
}, []);
给拖拽的行去绑定onTouchStart,onTouchMove,onTouchEnd,
<View
className="row-item__left"
onTouchStart={(e) => {
e.stopPropagation();
onTouchStart(e, userItem, index);
}}
onTouchMove={(e) => {
e.stopPropagation();
onTouchMove(e, userItem, index);
}}
onTouchEnd={(e) => {
e.stopPropagation();
onTouchEnd(e, userItem, index);
}}
>
<AtIcon value="i-ic_sort_16" prefixClass="iconfont" color="#A8A8CB" size={16}></AtIcon>
<Text className="user-name">{userItem.nickname}</Text>
</View>
在按下时记录当前的索引,当前的y轴的坐标。
const onTouchStart = (e: any, item: T, index: number) => {
const { pageY } = e.changedTouches[0];
setDraging({
...draging,
status: true,
});
setStartY(pageY);
setStartIndex(index);
};
拖拽时需要做有几件事 大部分逻辑都在这块
- 获取拖拽的方向:判断是向上拖还是向下拖
- 保证dom节点跟手指联动的,一起动起来。
const onTouchMove = (e: any, item: T, index: number) => {
const { pageY } = e.changedTouches[0];
setDraging({
status: true,
direction: pageY > startY ? DragDirection.DOWN : DragDirection.UP,
y: pageY,
difference: pageY > startY ? pageY - startY : startY - pageY,
transform: `translateY(${pageY > startY ? pageY - startY : "-" + (startY - pageY)}px)`,
});
};
/**
* 监听拖拽行属性的变化
*/
useEffect(() => {
if (!draging.status || draging.difference < 10) {
return;
}
computeNewIndex(draging.direction, draging.difference);
}, [draging]);
/**
* 计算最新位置的索引
*/
const computeNewIndex = (direction: DragDirection, difference: number) => {
let newIndex;
if (direction == DragDirection.UP) {
newIndex = startIndex - Math.floor(difference / dragItemHeight.current);
} else {
newIndex = startIndex + Math.floor(difference / dragItemHeight.current);
}
setEndIndex(newIndex);
};
在上边我们已经拿到了startIndex,endIndex,那么在松开手指的时候就可以更新列表了。
/**
* 松开
*/
const onTouchEnd = (e: any, item: T, index: number) => {
moveArrayItem();
_reset();
};
/**
* 更新列表
*/
const moveArrayItem = () => {
const newArray = [...dragList];
const [removedItem] = newArray.splice(startIndex, 1);
newArray.splice(endIndex, 0, removedItem);
setDragList(newArray);
};
/**
* 重置
*/
const _reset = () => {
setDraging({
status: false,
direction: DragDirection.UP,
y: 0,
difference: 0,
transform: "translateY(0px)",
});
setStartY(0);
setStartIndex(0);
setEndIndex(0);
};
到此拖拽就实现了,在期间遇到了时间冲突的问题,就是当列表过长时,会出现滚动条,和拖拽的手势有点冲突,我这块简单实现了下,当按下时,将滚动区域设置为overflowY:'hidden'
style={{
overflowY: draging.status ? "hidden" : "auto",
}}
如果做细点,那么可以判断当前手指的y轴坐标是否触顶/底,去将滚动条同步滚动。懒,没写,感觉行,哈哈哈哈哈。
完整逻辑代码,目前没有封装成通用组件,只是把业务代码做了一些删减。前端菜鸟一个,大佬们给点优化建议,指点下,抱拳!
import { View, Text } from "@tarojs/components";
import Taro from "@tarojs/taro";
import { ReactNode, useEffect, useRef, useState } from "react";
export interface DraggableProps<T> {
items: T[];
keyName: string;
dragRowItemCom: ReactNode;
}
enum DragDirection {
UP = "UP",
DOWN = "DOWN",
}
/**
* 拖拽容器组件
*/
const DraggableContainer = <T,>(props: DraggableProps<T>) => {
const { items, keyName = "id", dragRowItemCom } = props;
const [dragList, setDragList] = useState<Array<T>>(items);
// 拖拽item的高度
const dragItemHeight = useRef(0);
// 起始y轴坐标
const [startY, setStartY] = useState<number>(0);
// 起始索引
const [startIndex, setStartIndex] = useState<number>(0);
// 结束索引
const [endIndex, setEndIndex] = useState<number>(0);
// 拖动项
const [draging, setDraging] = useState<{ status: boolean; direction: DragDirection; y: number; difference: number; transform: string }>({
status: false, //状态 - 按下时设置为true,结束设置为false
direction: DragDirection.UP, // 拖拽的方向
y: 0, //拖动时y轴的坐标
difference: 0, // 拖拽时y轴坐标 - 按下时的坐标
transform: "", // 存储拖拽的transform的值
});
/**
* 按下
*/
const onTouchStart = (e: any, item: T, index: number) => {
const { pageY } = e.changedTouches[0];
setDraging({
...draging,
status: true,
});
setStartY(pageY);
setStartIndex(index);
};
/**
* 拖拽
*/
const onTouchMove = (e: any, item: T, index: number) => {
const { pageY } = e.changedTouches[0];
setDraging({
status: true,
direction: pageY > startY ? DragDirection.DOWN : DragDirection.UP,
y: pageY,
difference: pageY > startY ? pageY - startY : startY - pageY,
transform: `translateY(${pageY > startY ? pageY - startY : "-" + (startY - pageY)}px)`,
});
};
/**
* 松开
*/
const onTouchEnd = (e: any, item: T, index: number) => {
moveArrayItem();
_reset();
};
/**
* 更新列表
*/
const moveArrayItem = () => {
const newArray = [...dragList];
const [removedItem] = newArray.splice(startIndex, 1);
newArray.splice(endIndex, 0, removedItem);
setDragList(newArray);
};
/**
* 重置
*/
const _reset = () => {
setDraging({
status: false,
direction: DragDirection.UP,
y: 0,
difference: 0,
transform: "translateY(0px)",
});
setStartY(0);
setStartIndex(0);
setEndIndex(0);
};
/**
* 获取当个item的高度
*/
useEffect(() => {
Taro.nextTick(() => {
const idStr = `#drag-${dragList[0][keyName]}`;
Taro.createSelectorQuery()
.select(idStr)
.boundingClientRect()
.exec((res) => {
dragItemHeight.current = res[0].height;
});
});
}, []);
/**
* 计算最新位置的索引
*/
const computeNewIndex = (direction: DragDirection, difference: number) => {
let newIndex;
if (direction == DragDirection.UP) {
newIndex = startIndex - Math.floor(difference / dragItemHeight.current);
} else {
newIndex = startIndex + Math.floor(difference / dragItemHeight.current);
}
setEndIndex(newIndex);
};
/**
* 位置偏移
*/
const getTranslateY = (index: number) => {
if (!draging.status || draging.difference < 10) {
return "translateY(0px)";
}
if (index === startIndex) {
return draging.transform;
}
if (draging.direction === DragDirection.UP) {
if (index >= endIndex && index < startIndex) {
return `translateY(${dragItemHeight.current}px)`;
}
return "translateY(0px)";
} else if (draging.direction === DragDirection.DOWN) {
if (index <= endIndex && index > startIndex) {
return `translateY(-${dragItemHeight.current}px)`;
}
return "translateY(0px)";
}
};
/**
* 监听拖拽行属性的变化
*/
useEffect(() => {
if (!draging.status || draging.difference < 10) {
return;
}
computeNewIndex(draging.direction, draging.difference);
}, [draging]);
return (
<View
className="followers-list-wrap"
style={{
overflowY: draging.status ? "hidden" : "auto",
}}
>
{dragList.map((item, index) => {
return (
<View
key={item[keyName]}
id={`drag-${item[keyName]}`}
className="row-wrap"
style={{
backgroundColor: "#fff",
zIndex: index === startIndex ? 10 : 1,
transform: getTranslateY(index),
}}
>
<View className="row-item">
<View
className="row-item__left"
onTouchStart={(e) => {
e.stopPropagation();
onTouchStart(e, item, index);
}}
onTouchMove={(e) => {
e.stopPropagation();
onTouchMove(e, item, index);
}}
onTouchEnd={(e) => {
e.stopPropagation();
onTouchEnd(e, item, index);
}}
>
<Text>{index}</Text>
</View>
</View>
</View>
);
})}
</View>
);
};
export default DraggableContainer;