移动端滑动选择📲UniApp触摸事件

1,342 阅读5分钟

写在开头

Hi,各位UU好呀。😉

今是2025年06月28日,下雨天,直接选择躺平,没出门,午餐在宿舍找到之前买的一个安徽板面,第一次吃,挺好吃的,就是有点儿辣。😂

img_v3_02no_36803e5b-fc66-4a99-91c2-438eb46ddebg.jpg

然后,连续三次周末洗鞋子,放阳台被雨淋湿,真是...无语住了,我发誓,下次我一定在工作日的时间洗了。🤢

那么,回到正题,本次要分享的是关于移动端滑动快速选择功能,效果如下,请诸君按需食用哈!

063001.gif

需求背景

最近,小编在做一个移动端项目的时候,遇到了一个滑动选择的需求。大概就是和咱们手机相册中滑动快速选中图片的功能一样,需要实现滑动选择批量的图片,并且也需要在多次滑动选中的时候进行叠加,或者是滑动选中的时候取消选中。

具体分析:

  • 用户可以通过滑动手势快速选择多个元素。
  • 多次滑动未选中的元素应该进行叠加。
  • 已选中的元素再次滑动时应该取消选中。
  • 单击也能切换选中状态。

需求大概就是这么一个需求,接下来,咱们来一步一步来实现这个滑动选择功能,GO!🏃

实现过程

首先,咱们需要创建一个基础的滑动选择功能。

第1️⃣步:基础的滑动选择

在 UniApp 中,我们主要依靠三个核心触摸事件:

  • @touchstart:手指触摸开始。
  • @touchmove:手指滑动过程。
  • @touchend:手指离开屏幕。

HTML结构:

<template>
    <view class="content">
        <view
            id="container"
            @touchstart="touchStart"
            @touchend="touchEnd"
            @touchmove="touchMove"
        >
            <view 
                v-for="item in list" 
                :key="item.id" 
                :class="['box', `box_${item.id}`, { 'box-selected': item.selected }]"
                @tap="toggleSelection(item)"
            >
                <view class="box-content">
                    <text class="box-number">{{ item.id }}</text>
                </view>
                <view class="selection-indicator" v-if="item.selected"></view>
            </view>
        </view>
        <view class="bottom-info">
            <view class="selected-info">已选中:{{ selectedCount }}个</view>
            <button @click="clearSelection" class="clear-btn">清除选中</button>
        </view>
    </view>
</template>

数据结构:

const list = ref([]);

function init() {
  list.value = Array(30).fill(0).map((_, index) => ({
    id: index + 1,
    selected: false,
    clientX: 0,
    clientY: 0,
    width: 0,
    height: 0,
  }))
}
init()

为了演示,咱们先自己造一些数据,关键是看看每个元素的基础数据结构是长什么样子的。小编设计了一个包含位置信息的数据结构,每个元素都有记录了自己的位置与是否选中状态等信息!✨

初始化元素的位置等信息:

const instance = getCurrentInstance();
const query = uni.createSelectorQuery().in(instance.proxy);

function getBoxInfo() {
    // 页面加载完成后获取位置信息
    nextTick(() => {
        query.selectAll('.box').boundingClientRect((data) => {
            // 将位置信息存储到对应的list项中
            data.forEach((rect, index) => {
                if (list.value[index]) {
                    list.value[index].clientX = rect.left;
                    list.value[index].clientY = rect.top;
                    list.value[index].width = rect.width;
                    list.value[index].height = rect.height;
                }
            });
        }).exec();
    });
}
getBoxInfo();

这一步是整个功能的基础,通过 uni.createSelectorQuery() 获取每个元素的精确位置,为后续的碰撞检测做准备。🎯

基础触摸事件处理:

const touchClient = reactive({
    x: 0,
    y: 0,
});
const startTouchClient = reactive({
    x: 0,
    y: 0,
});
// 标记是否正在拖动选择
let isDragging = ref(false);

function touchStart(e) {
    touchClient.x = 0;
    touchClient.y = 0;
    startTouchClient.x = e.changedTouches[0].clientX;
    startTouchClient.y = e.changedTouches[0].clientY;
    isDragging.value = false;
    
    // 根据按下位置的元素状态确定选择模式,看下面第3步说明
    determineSelectionMode(startTouchClient.x, startTouchClient.y);

    // 清除临时选中记录
    tempSelectedItems.value.clear();
}
function touchMove(e) {
    const changedTouches = e.changedTouches[0];
    touchClient.x = changedTouches.clientX;
    touchClient.y = changedTouches.clientY;
    // 计算移动距离,判断是否为拖动操作
    const moveDistance = Math.sqrt(
        Math.pow(touchClient.x - startTouchClient.x, 2) + 
        Math.pow(touchClient.y - startTouchClient.y, 2)
    );
    if (moveDistance > 10) {
        isDragging.value = true;
        // 更新选中状态
        updateSelection();
    }
}
function touchEnd(e) {
    // 不要里面立马重置,否则在快速操作下会连续触发单机事件
    setTimeout(() => {
        isDragging.value = false;
    }, 100);
}

这里小编加了一个移动距离的判断,只有当手指移动超过10像素时才认为是拖动操作,这样可以避免误触。⏰

更新选中状态:

// 存储当前滑动过程中临时选中的元素
let tempSelectedItems = ref(new Set());

/** @name 更新选中状态,根据模式切换 **/
function updateSelection() {
    // 清除当前滑动的临时选中状态记录
    tempSelectedItems.value.clear();
    list.value.forEach(item => {
        if (touchClient.x !== 0 && touchClient.y !== 0) {
            const isInCurrentSelection = isElementInSelection(
                item,
                startTouchClient.x,
                startTouchClient.y,
                touchClient.x,
                touchClient.y
            );
            if (isInCurrentSelection) {
                tempSelectedItems.value.add(item.id);
                // 根据选择模式设置元素状态
                item.selected = selectionMode.value;
            }
        }
    });
}

第2️⃣步:碰撞检测

滑动选择的核心在于判断哪些元素在选择区域内,这就需要用到"碰撞检测":

function isElementInSelection(element, startX, startY, currentX, currentY) {
    const minX = Math.min(startX, currentX);
    const maxX = Math.max(startX, currentX);
    const minY = Math.min(startY, currentY);
    const maxY = Math.max(startY, currentY);

    // 判断元素是否与选择区域有交集
    return (
        element.clientX < maxX &&
        element.clientX + element.width > minX &&
        element.clientY < maxY &&
        element.clientY + element.height > minY
    );
}

这个"碰撞检测"的精髓在于:

  1. 计算选择区域的边界,支持任意方向的拖拽。
  2. 判断元素矩形与选择区域矩形是否有交集。
  3. 使用AABB(轴对齐包围盒)碰撞检测算法。

这里小编其实踩了一个小坑,最初只考虑了滑动从左上到右下的,后来发现也可以从中间扩散滑动出来,反正用户的操作应该是多样的,这需要考虑齐全!😅

第3️⃣步:叠加与取消模式

这里咱们要实现的是:

  • 如果滑动起始位置的元素已选中,则滑动过程中取消选中经过的元素。
  • 如果滑动起始位置的元素未选中,则滑动过程中选中经过的元素。
// 记录按下时的选择模式:true为选中模式,false为取消选中模式
let selectionMode = ref(true);

function determineSelectionMode(startX, startY) {
    // 找到按下位置的元素
    const targetElement = list.value.find(item => {
        return (
            startX >= item.clientX &&
            startX <= item.clientX + item.width &&
            startY >= item.clientY &&
            startY <= item.clientY + item.height
        );
    });
    if (targetElement) {
        // 如果按下的元素是选中状态,则设为取消选中模式
        // 如果按下的元素是未选中状态,则设为选中模式
        selectionMode.value = !targetElement.selected;
    } else {
        // 如果没有按到元素,默认为选中模式
        selectionMode.value = true;
    }
}

第4️⃣步:单击切换

除了滑动选择,单击切换咱们也别忘记了:

/**
 * @name 单击切换
 * @param {object}
 */
function toggleSelection(item) {
    if (isDragging.value) return;
    item.selected = !item.selected;
}

需要区分点击和拖动事件,避免拖动时误触点击事件。小编这里是通过增加一个 isDragging 标志位来解决这个问题。🤔

完整源码

传送门





至此,本篇文章就写完啦,撒花撒花。

image.png

希望本文对你有所帮助,如有任何疑问,期待你的留言哦。
老样子,点赞+评论=你会了,收藏=你精通了。