手撸一个taro拖拽排序列表

1,285 阅读2分钟

拖拽排序是一个很常见的业务。这不我这次需求就有一个用户关注列表拖拽的功能,然后是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);
    };

拖拽时需要做有几件事 大部分逻辑都在这块

  1. 获取拖拽的方向:判断是向上拖还是向下拖
  2. 保证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;