H5移动端实现左右滑屏切换页面

时间:2024-10-09 07:40:33

项目中需要实现的一个需求是顶部有一个tab选择框,点选某一个tab的时候切换页面,并且支持手势滑动,左滑右滑可以同点选tab一样切换页面。
根据项目中选用的ui组件cube-ui为基准查看了一下可实现的方案,比如可以直接用swipe或者是slide实现,但根据之前的实现方案来看,多少都会有些问题,尤其是在页面嵌套了很多层的垂直和水平滚动的情况下,会让滚动很不流畅。
于是选用了移动端的touch事件,整个功能就是监听页面touchstart,touchmove,touchend这三个事件来做的。
首先页面结构如下:

<div class="home-main flex-1" @='touchStart' 		    @="touchMove" @='touchEnd'>
     <transition :name="transitionName" mode="out-in">
        <keep-alive>
          <router-view/>
        </keep-alive>
      </transition>
</div>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

router-view里面就是需要进行切换的页面,页面切换不能太生硬需要加入动画,并且左滑右滑的动画效果是不一样的。
那么接下来就是滑动样式:

 .slide-right-enter-active {
      transition: all .8s ease;
    }
    .slide-right-leave-active {
      transition: all .3s cubic-bezier(1.0, 0.5, 0.8, 1.0);
    }
    .slide-right-enter {
      opacity: 0;
      transform: translateX(-100%);
    }
    .slide-right-leave-to {
      opacity: 0;
      transform: translateX(100%);
    }
    .slide-left-enter-active {
      transition: all .8s ease;
    }
    .slide-left-leave-active {
      transition: all .3s cubic-bezier(1.0, 0.5, 0.8, 1.0);
    }
    .slide-left-enter {
      opacity: 0;
      transform: translateX(100%);
    }
    .slide-left-leave-to {
      opacity: 0;
      transform: translateX(-100%);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

然后就是主要实现方法了,首先data里面声明touch事件需要用到的数据:

 data () {
 	transitionName: '', // 过度动画名称 滑动时才有过度动画
    touch: {}, // 保存着起始位置x1和变化的位置x2
    currentDistance: 0, // 上一个touch事件完成后,已滑动距离。实际在这个设计里,因为我们手指离开后, 页面不会停留在中间,不是滑过去切换路由,就是滑回去恢复原样。所以这个变量并没有什么卵用,但是如果要*即停即走*,这个变量不可少。
    totalDiff: 0 // 总滑动距离
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

然后touchStart方法很简单,只需要记录一下手指初始接触页面的位置即可:

touchStart (ev) {
      let touch = [0]
      .x1 = 
      .y1 = 
    }
  • 1
  • 2
  • 3
  • 4
  • 5

左滑右滑肯定是手指在页面滑动了一定的距离才触发,所以滑动过程中也需要记录下位置,touchMove方法:

touchMove (ev) {
      let touch = [0]
      .x2 = 
      .y2 = 
      let w = (.x2 - .x1)
      let h = (.y2 - .y1)
      // 部分安卓手机可能会有兼容问题
      if (w > h) {
        ()
        // ()
      }
       = .x2 - .x1 // 差值,表示滑动过程中手指移动的距离
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

因为vue中的click300延迟以及touchstart可能和click的各种兼容问题,这里需要判断处理下是否左滑,并且将页面的默认滑动先禁止。
然后在touchend中将手指离开的坐标记录下,同时将路由切换到滑动页面:

// TODO:此方法复用性差 需要再次封装
    touchEnd (ev) {
      let touch = [0]
      .x2 = 
      .y2 = 
      let diff = .x2 - .x1 // 手指触摸结束位置的水平移动差值
      let diffY = .y2 - .y1 // 竖直方向的差值
       = diff + 
      // 正切得到弧度转换为角度
      let angel = Math.atan2((diffY), ()) * 180 / () // 90为垂直滑动 需要一定差值
      if () { // 这个参数是为了处理页面banner的滑动,水平滑动和手势的冲突
        if (angel < 45 &&  < -20 && this.$ !== 'homeVideo') { // 小于为左滑 左滑到右边(视频)
           = 'slide-left'
          this.$('/home/video')
           = '视频'
        } else if (angel < 30 &&  > 20 && this.$ !== 'homeFound') { // 大于为右滑 右滑到左边(发现)
           = 'slide-right'
          this.$('/home/Found')
           = '发现'
        }
      }
      this.$('setTouchStatus', true)
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

在结束滑动的时候判断下是左滑还是右滑,并且允许一定的角度差,因为人的手指是比较粗的,而且滑动的时候并不是水平或者竖直的,存在一定的偏移,所以要控制一个偏移距离和角度,让滑动顺畅一些,而不是滑动着就切换页面了,以上偏移角度和长度只是项目测试的可接受数据,并不是标准的值。
至此,页面的左滑右滑切换就完成了,但是随之问题也出现了,比如页面头部有个banner,中间内容部分还有可以水平滚动的区块,我在滑动banner,或者滑动水平区块的时候,页面直接切换了,这并不是我想要的结果。
虽然touch事件是添加到外层容器元素上面的,但touch事件是可以冒泡的,所以现在的思路就是阻止右水平滑动的几个元素冒泡不就完事了,然后我就一顿添加vue的修饰符,@简直不要太轻松,事实证明我还是太年轻,添加之后并没有什么用,反而让页面的滚动行为异常,再接再厉,@让事件捕获,然后以防万一我再阻止冒泡stopPropagation,果然也是没什么用,页面的各种滚动变得奇奇怪怪,要么滚不动要么一起滚,总之就是不能正常 。
然后换个思路,我可以在水平滚动那几个元素的时候让绑定的touch事件不触发,从而让它自己滚动,页面也不会切换,nice。于是有了以下方法:

preventTouch (theNode) {
      let that = this
      let flag = false // 是否滚动
      const scrollNode = (theNode) // 有滚动的面板
      let dur = 0 // 触摸时间,太短不触发
      let dateStart, dateEnd
      ('touchstart', handler, { passive: false })
      ('touchmove', handler, { passive: false })
      ('touchend', handler, { passive: false })
      ('scroll', handler, { passive: false })
      function handler (e) {
        switch () {
          case 'touchstart':
            flag = false
            dateStart = new Date()
            that.$('setTouchStatus', false)
            break
          case 'touchmove':
            dateEnd = new Date()
            dur = dateEnd - dateStart
            if (flag || dur < 300) { // 表示是滚动完成后的那个touch不触发/间隔太短
              // () // 阻止冒泡 可能会导致滑动不流畅
              that.$('setTouchStatus', false)
            }
            break
          case 'touchend':
            dateEnd = new Date()
            dur = dateEnd - dateStart
            if (flag || dur < 300) { // 表示是滚动完成后的那个touch不触发/间隔太短
              // () // 阻止冒泡
              that.$('setTouchStatus', true) // 区块横向滑动完毕将状态置为可滑动
            }
            break
          case 'scroll':
            if (!flag) {
              flag = true
            }
            break
        }
      }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

添加一个全局状态canTouch,如果是在页面可以水平滚动的区块,那么touchEnd事件就不执行页面的切换和动画,也就是说,并不去干涉页面的滚动行为,该滚还是让他滚,也不禁止冒泡和默认行为,这样也免去了滚动异常,然后在各区块独立水平滚动的同时,在这些元素范围外可以左右滑动切换。
后续还需要重新封装一下这些方法,看看有没有更优雅的解决方案,同时touch也可以封装成指令,方便使用。