H5 列表数据左滑,出现操作按钮

前言

我们知道 H5 可视区域相对较小,为了将内容更多的呈现,许多需求都会将 列表数据项 的操作,选择以对数据项左滑形式出现。这类交互常见于商品购物车页面,左滑出现删除等操作(美团、京东 等平台)。

最近刚好遇到这样一个交互需求,便以文章的形式将实现原理记录下来。

一、实现分析

从需求交互来看,列表数据项 由两部分组成:左侧 内容区域右侧 操作区域

默认可视区域仅展示内容,右侧的操作区域被隐藏,这里可以使用 定位负值 来实现布局。

经过分析我们得出 DOM 的结构如下:

html 复制代码
<div class="slide-operate">
  <!-- 滑动块 -->
  <div class="slide-operate-content">
    <!-- 实际内容,作为组件使用时,表示 vue slot 或者 react children -->
    <div class="content-slot">滑块内容</div>
  </div>
  <!-- 滑动之后出现的操作按钮 -->
  <div class="slide-operate-btns">
    <button>删除</button>
  </div>
</div>

其中 .slide-operate-content 表示内容区域,可以根据需求来自定义内容;.slide-operate-btns 则表示操作区域,比如有删除操作。

在样式布局上,我们需要为 .slide-operate-btns 操作区域 定位成负值,结合 .slide-operate 容器设置 overflow: hidden 进行隐藏。

css 复制代码
.slide-operate{
  position: relative;
  overflow: hidden;
}

.slide-operate-btns{
  position: absolute;
  top: 0;
  /* 按钮最初为隐藏,right 使用负值,具体值根据 按钮数量 及 单个按钮宽度计算 */
  /* right: -50px;
  width: 50px; */
  height: 100%;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

// 假设这是实际内容
.content-slot{
  background-color: aquamarine;
  border-radius: 5px;
  height: 50px;
}

由于在不同场景下,操作项的个数及单个按钮宽度会存在不一致,建议实际的操作区域宽度由外部传入,并以 JS 方式为其设置 rightwidth

假设我们的场景中只有一个删除操作,给到的宽度为 50px,这里使用 distance 变量表示。(这个值也将作为滑动距离的参照值

js 复制代码
const sildeOperateContent = document.querySelector(".slide-operate-content");
const sildeOperateBtns = document.querySelector(".slide-operate-btns");

// 假设右侧按钮容器的宽度为 50,也是向左滑动的距离。如果是封装组件,这个变量将由外部传入
const distance = 50;
sildeOperateBtns.style.setProperty("width", `${distance}px`); // 按钮的整体宽度
sildeOperateBtns.style.setProperty("right", `-${distance}px`); // 最初 right 位置

现在初版布局完成了,接下来要让内容向左动起来,包含两个部分:元素位置移动(CSS transform-translate) 和 触发移动行为(JS touch 事件)

让元素位置发生移动类似于如下代码:

css 复制代码
sildeOperateContent.style.cssText = `transform: translate(${-distance}px, 0)`;
sildeOperateBtns.style.cssText = `transform: translate(${-distance}px, 0)`;

而触发用户滑动行为,可以使用 H5 的 touch 相关事件。

二、滑动实现

1. 定义状态变量

在滑动过程中,一定会涉及从手指按下时到移动过程中的位置计算,这里我们定义 state 来记录这些信息:

js 复制代码
let state = {
  startX: 0, // 记录滑动起始位置,手指触碰到屏幕时 x 轴的位置
  moveDistance: 0, // 记录手指移动距离加上滑动阻力后的移动距离
}

2. touch 事件处理

我们会用到 touchstarttouchmovetouchend 三个事件。

  • touchstart:每次按下时都会初始化 state 状态,并记录上 start.startX 按下时的位置;
  • touchmove:每次移动时都会根据 最新触摸位置 movestart.startX 进行计算,得出滑动的距离 state.moveDistance
  • touchend:当手指离开屏幕后,我们要根据滑动的距离(state.moveDistance),和 右侧操作区域 的宽度(distance)进行比较,来确定是否展示 操作按钮。

具体实现如下:

js 复制代码
sildeOperateContent.addEventListener("touchstart", event => {
  state.moveDistance = 0;
  state.startX = event.targetTouches[0].clientX;
});

sildeOperateContent.addEventListener("touchmove", event => {
  const move = state.startX - event.targetTouches[0].clientX; // 获取左滑的距离
  // move 大于 0,说明手指向左移动了
  if (move > 0) {
    event.preventDefault();
    // 增加滑动阻力,避免 上下滑动 意外 触发 左右滑动 逻辑
    state.moveDistance = Math.pow(move, 0.9); // Math.pow 求一个数的 n 幂次方,这里 n 用 0.9 控制滑动阻力。
    // 控制滑动的最大距离为按钮的宽度
    state.moveDistance = Math.min(state.moveDistance, distance);
  }
});

sildeOperateContent.addEventListener("touchend", () => {
  // 如果滑动结束  滑动距离大于右侧按钮的一半  则出现按钮,否则隐藏按钮
  if (state.moveDistance > distance / 2) {
    state.moveDistance = distance;
  } else {
    // 若滑动距离没有超过按钮的一半,回到初始位置
    state.moveDistance = 0;
  }
});

3. 实现元素位置移动

现在,我们通过 touch 事件拿到了要滑动的距离,由于我们采用原生 JS 来实现,并没有框架的数据驱动视图更新,我们需要手动操作 DOM 来实现元素位置移动。

为了避免在多处编写 DOM 移动操作,这里采用 Proxy 方式对 state.moveDistanceset 变化进行监听,实现视图更新。

最终,我们将 state 改造为如下形式。

js 复制代码
let defaultState = {
  startX: 0, // 记录滑动起始位置,手指触碰到屏幕时 x 轴的位置
  moveDistance: 0, // 记录手指移动距离加上滑动阻力后的移动距离
}

// Proxy 观察滑动距离的变化,从而更新视图
const state = new Proxy(defaultState, {
  set(target, key, value) {
    const oldValue = target[key];
    if (key === "moveDistance" && oldValue !== value) {
      value = Math.floor(value);
      sildeOperateContent.style.cssText = `transition: 300ms; transform: translate(${-value}px, 0)`;
      sildeOperateBtns.style.cssText = `${sildeOperateBtns.style.cssText}; transition: 300ms; transform: translate(${-value}px, 0)`;
    }
    target[key] = value;
    return true;
  }
});

4. 优化交互

当处于 展示操作项 状态时,我们期望再次滑动内容时,仅是恢复为最初隐藏 操作项 的状态,不希望在这个场景下允许滑动,可以设定一个 state.stop 来限制。

js 复制代码
let defaultState = {
  ...
   // 新增交互优化
  stop: false, // 若现在操作按钮处于显示状态,再次进行滑动时让其恢复默认状态,不去改变滑动位置。
}

sildeOperateContent.addEventListener("touchstart", event => {
  if (state.moveDistance === distance) {
    state.stop = true; // 按下时记录状态
  }
  ...
});
sildeOperateContent.addEventListener("touchmove", event => {
  if (state.stop) return; // 移动时判断状态

  ...
});
sildeOperateContent.addEventListener("touchend", () => {
  ...
  state.stop = false; // 松开时重置状态
});

最终效果图如下:

滑动前:

滑动后:

参考

h5 实现向左平滑,出现按钮操作,封装组件,模拟购物车左滑删除

相关推荐
小吕学编程几秒前
ES练习册
java·前端·elasticsearch
Asthenia04128 分钟前
Netty编解码器详解与实战
前端
袁煦丞12 分钟前
每天省2小时!这个网盘神器让我告别云存储混乱(附内网穿透神操作)
前端·程序员·远程工作
一个专注写代码的程序媛1 小时前
vue组件间通信
前端·javascript·vue.js
一笑code1 小时前
美团社招一面
前端·javascript·vue.js
懒懒是个程序员2 小时前
layui时间范围
前端·javascript·layui
NoneCoder2 小时前
HTML响应式网页设计与跨平台适配
前端·html
凯哥19702 小时前
在 Uni-app 做的后台中使用 Howler.js 实现强大的音频播放功能
前端
烛阴2 小时前
面试必考!一招教你区分JavaScript静态函数和普通函数,快收藏!
前端·javascript
GetcharZp2 小时前
xterm.js 终端神器到底有多强?用了才知道!
前端·后端·go