不使用插件与组件库 - 如何简单实现滑动单元格

2,302 阅读7分钟

前言

和我上次的不使用插件与组件库 - 如何简单实现下拉刷新与上滑加载,算是一个系列吧,算是自己思考,然后实现的一种留痕方式。

很多人都在说不要重复造轮子,咱也不造轮子,最起码要有可以造轮子的简单思想,不论是写什么都有一定的用处。

这个东西实现起来没有什么难度,主要是有一些小的弯弯在里面,下面我会细说一下,具体问题。

准备

老样子,听个大概

实现效果

  • 可以左滑动 ?
  • 按钮怎么去写 ?
  • 可以有层次的出现 ?
  • 有点点的弹性效果 ?

动画11.gif

实现思路

来说一下,实现上述的效果有什么方法,我先抛一下愚见。 先给出HTML的结构

  <div class="swipe-cell">
    <div class="swipe">
      <div class="container">
        内容区域
      </div>
      <div class="btn-right">
        存放按钮区域
      </div>
    </div>
  </div>

可以左滑动

  • 左滑动:使用transform: translateX()属性。
  • 滑多少:使用js获取DOM,去拿到每一个按钮的宽度clientWidth,滑动的是总宽度。

按钮怎么去写

  • 按钮的设计:整体存放在一个btn-right的类名盒子里,该盒子是定位在内容的右侧的,因为是流布局的页面模式,div又是块级元素,采用绝对定位,按钮很容易出现在了,可视窗口的右侧了。
  • 按钮的初始化:不论几个按钮都采用left:0放到container盒子的右侧,堆叠在一块,为下面的有层次的出现,做伏笔。

可以有层次的出现

  • 层次的出现:层次的出现不难,前面有了滑动的总距离,每一个按钮的大小,又在同一起点,所以采用一定的比例去移动,跟随上一家按钮的宽度,作为向右移动的最终的距离- 定位的作用:采用left:xxpx,来然按钮分开,第一个按钮是不需要移动的,left:0完美的贴合在container盒子的右侧,剩下了采用js计算一下,移动的比例(例如:第二个按钮,肯定需要在盒子滑出结束,定位在left:(第一个按钮width)px )

滑动的总距离的时间 = 第二个按钮移到最终地点的时间 = 第三个按钮移到最终地点的时间 = .....

对象所需时间需滑动的距离
总距离t按钮的总宽度
第二按钮t第一个按钮的宽度

我们需要得到x2这个距离,上几家按钮的宽度的总宽度.

image.png 假设一:向左滑动,当前总距离滑动的距离为 x1 ,第二按钮移动的距离为 x2

x1x2=总距离第一按钮的宽度\dfrac{x1}{x2} = \dfrac{总距离}{第一按钮的宽度}

验证:x1滑动为总距离时,x2滑动了第一按钮的宽度。

得出:

x2=x1×第一按钮的宽度总距离 {x2} = \dfrac{x1 × 第一按钮的宽度 }{总距离}

我们能轻易的拿到x2的移动,与x1移动的关系了。

假设二:向右滑动,当前总距离滑动的距离为 -x1 ,第二按钮移动的距离为 x2

总距离x1x2=总距离第一按钮的宽度\dfrac{总距离 - x1}{x2} = \dfrac{总距离}{第一按钮的宽度}

验证: 当X1=0为滑动时,X2第二按钮需要移动的距离是第一按钮的宽度。

得出:

x2=(总距离x1)×第一按钮的宽度总距离 {x2} = \dfrac{(总距离 - x1) × 第一按钮的宽度 }{总距离}

拿到x2的移动,与x1移动的关系了。

有点点的弹性效果

  • 弹性效果:都是有点溢出,在让其回归正常的一个过程动画。

开始

以上述的思路,开始先构建html与css,全部代码,最下面了,注意这是分析.

html的结构

<div class="swipe-cell">
    <div class="swipe">
      <div class="container">
        内容
      </div>
      <div class="btn-right">
        <div class="btn1 btn">标记已读</div>
        <div class="btn2 btn">删除</div>
      </div>
    </div>
  </div>

css的上色

/* 页面的处理 */
*{
  box-sizing: border-box;
}
body{
  margin: 0;
}
/* 滑动处的处理 */
.swipe-cell{
 position: relative;
 overflow: hidden;
}
.swipe-cell .swipe{
  transform: translateX(0px); /* 初始滑动的距离 */
  background-color: #eee;
}
/* 内容的处理 */
.container{
  display: flex;
  padding: 15px;
}
/* 按钮的处理 */
.btn-right{
  position: absolute;
  right: 0;
  top: 0;
  transform: translateX(100%);
  height: 100%;
}
.btn{
  position: absolute;
  padding: 20px;
  color: #fff;
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
  transition-duration: 0.25s;
  transition-property: transform;
  transition-timing-function: cubic-bezier(.18,.89,.32,1)
}
.btn1{
  background-color: hotpink;
  left: 0;
  flex-wrap: nowrap;
  white-space: nowrap;
}
.btn2{
  background-color: #6a97eb; 
  left: 0;
  white-space: nowrap;
}

js的运作

js的初始化

// 获取有用的节点
let swipeNode = document.querySelector(".swipe");
swipeNode.addEventListener("touchmove",touchMoveFuc,false) // 手指移动
swipeNode.addEventListener("touchend",touchEndFuc,false) // 手指结束触摸
let swipeContainer = swipeNode.querySelector(".container");
swipeContainer.addEventListener("touchstart",touchContainerStartFuc,false); // 手指开始触摸
let btnRightNode = item.querySelectorAll(".btn-right .btn");
//初始化数据
let startX = 0; // 初始用户点击的位置
let scrollDistance = 0; // 手指滑动的距离,上面说的x1
let swipeState = false; // false是没有滑动
let swipeContainerTimeId = null; // 定时 手指持续点击,就结束滑动状态的计时id
let scrollBtn = [];
let btnBlockTotalWidth = 0;
btnRightNode.forEach(itemNode =>{
// 节点 + 上几个节点的宽度 , 是为了计算上面的提到了一个比例
scrollBtn.push([itemNode,btnBlockTotalWidth])
btnBlockTotalWidth += itemNode.clientWidth;
})

js的监听事件,开始触摸

  function touchContainerStartFuc(e){
    startX = e.changedTouches[0].clientX; // 记录第一次触摸的
    item.style.transitionDuration = "0s"; 
    // 如果没有滑动swipeState=false,自然就只记录了,一次点击
    // 如果没有滑动swipeState=true,自然就记录了,在规定的内如果没有移动就返回到滑动起始的地点
    swipeContainerTimeId = setTimeout(()=>{
      if(swipeState){
        item.style.transitionDuration = "0.25s"; // 添加返回的动画时间
        item.style.transform  = `translateX(0px)`;
        scrollDistance = 0;
        clearTimeout(swipeContainerTimeId);
      }
    },500)
  }

js的监听事件,移动手指

    // 持续触摸中
  function touchMoveFuc(e) {
    // 你在规定的时间内移动,那么我就清除掉在开始触摸事件中留下的定时
    clearTimeout(swipeContainerTimeId);
    // 获取滑动距离
    scrollDistance = startX - e.changedTouches[0].clientX;
    
    if(!swipeState){
      //正常拉出
      if( scrollDistance>0){
        //填充动态拉动效果
        if(scrollDistance - btnBlockTotalWidth < 0){
          // 拉出在正常范围内部
          item.style.transform = `translateX(-${scrollDistance}px)`
           // width是上一个块的,产生叠加的效果
           // 使其同时结束,则width1+width2与width2走出了相同时间,width1+width2的长度与scrollDistance一样
  
           for (let i = 0; i < scrollBtn.length - 1; i++) {
              //记录这个btn前面的宽度,scrollDistance滑行到了btnBlockTotalWidt总宽,那么就是left前面宽度那么远
              // 简单说,这个是百分比滑动的策略
              scrollBtn[i+1][0].style.left = `${ (scrollDistance*scrollBtn[i+1][1])/btnBlockTotalWidth}px`
           }
        }else if(scrollDistance - btnBlockTotalWidth < 50){
          // 多拉出一点,产生布局一种弹性效果
          item.style.transform = `translateX(-${scrollDistance}px)`
          btnRightNode.forEach(itemNode =>{
            itemNode.style.paddingRight = `${20 + 50}px`
           })
           // 多拉出来一点 25的px对吧
          for (let i = 0; i < scrollBtn.length - 1; i++) {
            scrollBtn[i+1][0].style.left = `${ (scrollDistance*scrollBtn[i+1][1])/(btnBlockTotalWidth+25)}px`
          }
        }
      }
    }else{
      //swipeState= true表示,已经拉出来了,可能下面要来回拉动了
      scrollDistance = startX - e.changedTouches[0].clientX;
      //滑出来的距离是 btnBlockTotalWidth
      if(btnBlockTotalWidth + scrollDistance > 0){
        if(scrollDistance < 0){
        // scrollDistance是负数了,+就是-上滑动的scrollDistance,没有毛病,是上面分析的
          item.style.transform = `translateX(${-(btnBlockTotalWidth+scrollDistance)}px)`
          for (let i = 0; i < scrollBtn.length - 1; i++) {
            scrollBtn[i+1][0].style.left = `${((btnBlockTotalWidth+scrollDistance)*scrollBtn[i+1][1])/(btnBlockTotalWidth)}px`
          }
        }else if(scrollDistance < 50){
          // 多拉出一点,产生布局一种弹性效果
          item.style.transform = `translateX(-${scrollDistance+btnBlockTotalWidth}px)`
          btnRightNode.forEach(itemNode =>{
            itemNode.style.paddingRight = `${20 + 50}px`
           })
          for (let i = 0; i < scrollBtn.length - 1; i++) {
            scrollBtn[i+1][0].style.left = `${((btnBlockTotalWidth+scrollDistance)*scrollBtn[i+1][1])/(btnBlockTotalWidth+25)}px`
          }
        }
      
      }
    }
  }

js监听事件,结束触摸

 function touchEndFuc(e){
    // 触摸结束
    item.style.transitionDuration = "0.25s"; // 添加返回的动画时间
    // 滑动一半以上,
    // 还要swipeState = false
    // 滑动的距离还要大于10,因为避免一开始触摸就松开,导致一些问题
    if(btnBlockTotalWidth < scrollDistance*3 && !swipeState && scrollDistance>10){
      console.log("触摸结束,已经滑出");
      swipeState = true; //判定已经划出去了
      item.style.transform  = `translateX(${-btnBlockTotalWidth}px)`;
      //展开,展开的距离就是离定位点的距离
      for (let i = 0; i < scrollBtn.length - 1; i++) {
        scrollBtn[i+1][0].style.left = `${(scrollBtn[i+1][1])}px`
      }
    }else{
      // 不符合标准,就初始化,重新开始吧
      swipeState = false;
      item.style.transform  = `translateX(0px)`;
      scrollDistance = 0;
    }
  }

完整代码

HTML

<div class="swipe-cell">
    <div class="swipe">
      <div class="container">
        <img src="./1.jpg" alt="道长">
        <div class="info">
          <div>红拂仙子</div>
          <span>红拂仙子成为弃子,李化元密见令狐老祖有所图谋</span> 
        </div>
      </div>
      <div class="btn-right">
        <div class="btn1 btn">标记已读</div>
        <div class="btn2 btn">删除</div>
      </div>
    </div>
  </div>
  <div class="swipe-cell">
    <div class="swipe">
      <div class="container">
        <img src="./2.jpg" alt="道长">
        <div class="info">
          <div>李化元</div>
          <span>李化元,黄枫谷结丹修士,为人小气,三阳之体,修炼功法为真阳决,洞府叫绿波洞,韩立的师父。</span>
        </div>
      </div>
      <div class="btn-right">
        <div class="btn1 btn">标记已读</div>
        <div class="btn2 btn">删除</div>
        <div class="btn3 btn">黑名单</div>
      </div>
    </div>
  </div>

CSS

*{
  box-sizing: border-box;
}
body{
  margin: 0;
}
.swipe-cell{
 position: relative;
 overflow: hidden;
}
.swipe-cell .swipe{
  transform: translateX(0px);
  background-color: #eee;
}
.container{
  display: flex;
  padding: 15px;
}

.container img{
  height: 64px;
  width: 64px;
  border-radius: 50%;
  margin-right: 8px;
}
.container .info{
  flex-direction: column;
  align-self: center;
  font-size: 16px;
}
.container .info span{
  display: inline-block;
  font-size: 12px;
  margin-top: 5px;
}
.btn-right{
  position: absolute;
  right: 0;
  top: 0;
  transform: translateX(100%);
  height: 100%;
}
.btn{
  position: absolute;
  padding: 20px;
  color: #fff;
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
  transition-duration: 0.25s;
  transition-property: transform;
  transition-timing-function: cubic-bezier(.18,.89,.32,1)
}
.btn1{
  background-color: hotpink;
  left: 0;
  flex-wrap: nowrap;
  white-space: nowrap;
}
.btn2{
  background-color: #6a97eb; 
  left: 0;
  white-space: nowrap;
}
.btn3{
  background-color: #3a993f; 
  left: 0;
  white-space: nowrap;
}

JavaScript

// 为了更好的移植性选择js
//右拉的弹性效果是怎么实现的
let swipeAllNode = document.querySelectorAll(".swipe");
swipeAllNode.forEach(item =>{
  item.addEventListener("touchmove",touchMoveFuc,false)
  item.addEventListener("touchend",touchEndFuc,false)
  let swipeContainer = item.querySelector(".container");
  swipeContainer.addEventListener("touchstart",touchContainerStartFuc,false);
  let btnRightNode = item.querySelectorAll(".btn-right .btn");
  let startX = 0; //初始用户点击的位置
  let scrollDistance = 0; //用户滑动的距离
  let scrollBtn = [];
  let btnBlockTotalWidth = 0;
  let swipeState = false;//false是没有滑动
  let swipeContainerTimeId = null;
  btnRightNode.forEach(itemNode =>{
    // 节点 + 上几个节点的宽度
    scrollBtn.push([itemNode,btnBlockTotalWidth])
    btnBlockTotalWidth += itemNode.clientWidth;
  })

  function touchContainerStartFuc(e){
    startX = e.changedTouches[0].clientX;
    item.style.transitionDuration = "0s"; 
    swipeContainerTimeId = setTimeout(()=>{
      if(swipeState){
        item.style.transitionDuration = "0.25s"; // 添加返回的动画时间
        item.style.transform  = `translateX(0px)`;
        scrollDistance = 0;
        clearTimeout(swipeContainerTimeId);
      }
    },500)
  }
  
  function touchMoveFuc(e) {
    // 持续触摸中
    clearTimeout(swipeContainerTimeId);
    scrollDistance = startX - e.changedTouches[0].clientX;
    if(!swipeState){
      //正常拉出
      if( scrollDistance>0){
        //填充动态拉动效果
        if(scrollDistance - btnBlockTotalWidth < 0){
          // 拉出在正常范围内部
          item.style.transform = `translateX(-${scrollDistance}px)`
           // width是上一个块的,产生叠加的效果
           // 使其同时结束,则width1+width2与width2走出了相同时间,width1+width2的长度与scrollDistance一样
  
           for (let i = 0; i < scrollBtn.length - 1; i++) {
              //记录这个btn前面的宽度,scrollDistance滑行到了btnBlockTotalWidt总宽,那么就是left前面宽度那么远
              // 简单说,这个是百分比滑动的策略
              scrollBtn[i+1][0].style.left = `${ (scrollDistance*scrollBtn[i+1][1])/btnBlockTotalWidth}px`
           }
        }else if(scrollDistance - btnBlockTotalWidth < 50){
          // 多拉出一点,产生布局一种弹性效果
          item.style.transform = `translateX(-${scrollDistance}px)`
          btnRightNode.forEach(itemNode =>{
            itemNode.style.paddingRight = `${20 + 50}px`
           })
          for (let i = 0; i < scrollBtn.length - 1; i++) {
            scrollBtn[i+1][0].style.left = `${ (scrollDistance*scrollBtn[i+1][1])/(btnBlockTotalWidth+25)}px`
          }
        }
      }
    }else{
      //来回拉动
      scrollDistance = startX - e.changedTouches[0].clientX;
      //滑出来的距离是 btnBlockTotalWidth
      if(btnBlockTotalWidth + scrollDistance > 0){
        if(scrollDistance < 0){
          item.style.transform = `translateX(${-(btnBlockTotalWidth+scrollDistance)}px)`
          for (let i = 0; i < scrollBtn.length - 1; i++) {
            scrollBtn[i+1][0].style.left = `${((btnBlockTotalWidth+scrollDistance)*scrollBtn[i+1][1])/(btnBlockTotalWidth)}px`
          }
        }else if(scrollDistance < 50){
          // 多拉出一点,产生布局一种弹性效果
          item.style.transform = `translateX(-${scrollDistance+btnBlockTotalWidth}px)`
          btnRightNode.forEach(itemNode =>{
            itemNode.style.paddingRight = `${20 + 50}px`
           })
          for (let i = 0; i < scrollBtn.length - 1; i++) {
            scrollBtn[i+1][0].style.left = `${((btnBlockTotalWidth+scrollDistance)*scrollBtn[i+1][1])/(btnBlockTotalWidth+25)}px`
          }
        }
      
      }
    }
  }
  
  function touchEndFuc(e){
    // 触摸结束
    item.style.transitionDuration = "0.25s"; // 添加返回的动画时间
    // 滑动一半以上,
    // 还要swipeState = false
    // 滑动的距离还要大于10,因为避免一开始触摸就松开,导致一些问题
    if(btnBlockTotalWidth < scrollDistance*3 && !swipeState && scrollDistance>10){
      console.log("触摸结束,已经滑出");
      swipeState = true; //判定已经划出去了
      item.style.transform  = `translateX(${-btnBlockTotalWidth}px)`;
      //展开,展开的距离就是离定位点的距离
      for (let i = 0; i < scrollBtn.length - 1; i++) {
        scrollBtn[i+1][0].style.left = `${(scrollBtn[i+1][1])}px`
      }
    }else{
      // 不符合标准,就初始化,重新开始吧
      swipeState = false;
      item.style.transform  = `translateX(0px)`;
      scrollDistance = 0;
    }
  }
})