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 实现向左平滑,出现按钮操作,封装组件,模拟购物车左滑删除

相关推荐
OEC小胖胖22 分钟前
Vue 3 中 onUnload 和 onPageScroll 使用详解
前端·javascript·vue.js·前端框架·web
秋田君1 小时前
uniapp中使用Mescroll实现下拉刷新与上拉加载项目实战
javascript·uni-app
川石教育2 小时前
Vue前端开发-slot传参
前端·vue.js·前端框架·前端开发·slot组件
确实菜,真的爱2 小时前
vue3 element plus 把表格数据导出到excel
javascript·vue.js·excel
一舍予2 小时前
nuxt3项目搭建相关
开发语言·javascript·vue.js·nuxt
新时代的弩力2 小时前
【Cesium】--viewer,entity,dataSource
前端·javascript·vue.js
无恃而安2 小时前
localStorage缓存 接口 配置
javascript·vue.js·缓存
余道各努力,千里自同风2 小时前
HTML5 视频 Vedio 标签详解
前端·音视频·html5
_大菜鸟_3 小时前
修改element-ui-时间框date 将文字月份改为数字
javascript·vue.js·ui
尽兴-3 小时前
Vue 中父子组件间的参数传递与方法调用
前端·javascript·vue.js·父子组件·参数传递·组件调用