js前端无滚动条滑动组件实现

18 篇文章 0 订阅
2 篇文章 0 订阅

应用背景

  
一般在日常web页面开发中,经常会遇到滑动区域的操作,pc端开发滚动区域时大都会采用滚动条的形式,而移动端则大多时候滚动条时多余的。
当然,可以想方设法把滚动条隐藏,但是有更多关于滚动限制或者封装组件逻辑时,不是很方便处理。
  
故本文通过css的transform来滑动区域,使用transition来实现滑动过渡。其中包含弹性滑动,快速滑动和慢速滑动,横向滑动,纵向滑动等方案实现。

技术实现

  
css布局采用外层一个壳子,内层一个绝对定位的待滑动区域即可。滑动时计算好滑动距离,利用css transform来滑动区域,使用transition来实现滑动过渡。

transition: transform 100ms cubic-bezier(0.165, 0.84, 0.44, 1) 0s;
transform: translateX();

  
另外,在滚动区域需要三个事件触发函数,分别是touchstart,touchmove和touchend。还需要几个计算滚动长度的参数。

1.滑动开始记录滑动位置startX,滑动过程中叠加滑动距离moveX并且赋值给transform: translateX()。下一次滑动时基于上一次滑动,故需要allMove累计滑动距离。

2.弹性滑动则是滑动时允许超出一段距离,滑动结束后则减去超出这段距离。即maxUpBounce - maxUp 或者 maxDownBounce - maxDown即可。

3.快速滑动和慢速滑动则是通过计算滑动结束时的速度来计算,速度高,则在实际滑动距离的基础上加上多余滑动距离值。当然,这个多余滑动距离值和速度呈线性相关。

4.使用MutationObserver监听滑动内部dom高度的变化,可以监听到异步请求数据导致的dom更新。

5.按照单位距离滚动,比如列表项里,按照列表高度或者宽度单位滚动,可以实现列表项的选择。

startX: 0,//鼠标开始滑动的距离
moveX: 0,//鼠标移动的距离
allMove: 0,//鼠标累计移动的距离

maxUpBounce: 0,//最大向上反弹距离
maxDownBounce: 0,//最大向下反弹的距离

maxUp: 0,//最大向右滑动的距离
maxDown: 0,//最大向左滑动的距离

moveTime: 0,//滑动时间
moveDistance: 0,//记录移动距离用于计算滑动结束时的速度

  
本次基于vue实现,其余实现方式异曲同工。另外项目里封装了两个示例,滑动是很多组件需要用到的,比如移动端日期选择,表格滚动查看等等。抛砖引玉,抛砖引玉,仅限于提供思路,可根据自己的需求进行更改有需要的请移步github v-mobile-rolling

代码

<template>

  <!-- 纵向滚动时,利用MutationObserver观测DOM变化动态获取高度-->
  <div class="v-m-box" ref="VMBox" :style="{height: horizontal ? heightDom : heightHorizontal+'px'}">

    <!--纵向滚动需要定宽,横向滚动需要定高-->
    <div class="v-m-roll" :class="horizontal ? '' : 'width100'" ref="VMRoll" @touchstart="handleTouchStart"
         @touchmove="handleTouchMove"
         @touchend="handleTouchStop">

      <slot></slot>

    </div>

  </div>

</template>

<script>
  export default {
    name: "v-mobile-rolling",
    props: {
      //是否横向滚动
      horizontal: {
        type: Boolean,
        default: true
      },
      //纵向滚动,故需要设置定高
      heightHorizontal:{
        type:Number,
        default:200
      },
      //是否滚动后按照单位距离停留,多退少补
      rollByUnit: {
        type: String,
        default: ''
      },
      //快速滑动时补充滑动的距离
      distanceMore:{
        type:Number,
        default: 500
      },
      //最大向左滑动的距离
      maxDown: {
        type:Number,
        default:50
      },
      //初始滑动到某个位置
      initPosition:{
        type:Number,
        default:0
      }
    },
    data() {
      return {

        isRolling: false,//是否滚动,当内容填充不满时,不滚动

        startX: 0,//鼠标开始滑动的距离
        moveX: 0,//鼠标移动的距离
        allMove: 0,//鼠标累计移动的距离

        maxUpBounce: 0,//最大向上反弹距离
        maxDownBounce: 0,//最大向下反弹的距离

        maxUp: 0,//最大向右滑动的距离
        maxDown: 0,//最大向左滑动的距离

        moveTime: 0,//滑动时间
        moveDistance: 0,//记录移动距离用于计算滑动结束时的速度

        offsetDir: 'offsetWidth',//offset方向
        clientDir: 'clientX',//滑动方向距离
        translateDir: 'translateX',//动画方向

        heightDom:0,//初始化元素高度,动态监听

        observer:null//观测DOM改变
      }
    },

    created() {

      //垂直滚动还是水平滚动
      if (this.horizontal === false) {

        this.offsetDir = 'offsetHeight';
        this.clientDir = 'clientY';
        this.translateDir = 'translateY';

      }

    },

    beforeDestroy() {
      // 停止观测
      this.observer.disconnect();
    },

    mounted() {

      //获取滚动窗口的大小和滚动内容的大小
      this.$nextTick(() => {

        //利用MutationObserver来监听dom填充以及该变,动态获取内部元素高度
        this.heightDom = this.$refs['VMRoll'].offsetHeight+'px';

        //设置observer的配置选项
        let config = { attributes: true, childList: true, subtree: true };

        //创建一个observer示例与回调函数相关联,监测dom变化,比如通过异步请求滚动区域内的数据
        this.observer = new MutationObserver(()=>{
          this.heightDom = this.$refs['VMRoll'].offsetHeight+'px';
        });

        //使用配置文件对目标节点进行观测
        this.observer.observe(this.$refs['VMRoll'], config);

        //检测内容是否大于父元素,不大于则不滚动
        this.isRolling = (this.$refs['VMRoll'][this.offsetDir] - this.$refs['VMBox'][this.offsetDir]) > 0

        if (this.isRolling) {

          this.maxDownBounce = -(this.$refs['VMRoll'][this.offsetDir] - this.$refs['VMBox'][this.offsetDir]);
          this.maxUp = -(this.$refs['VMRoll'][this.offsetDir] - this.$refs['VMBox'][this.offsetDir] + this.maxDown);

        }

        //默认滚动到某个位置
        this.$refs['VMRoll'].style.transition = "transform 100ms cubic-bezier(.165, .84, .44, 1) 0s";
        this.$refs['VMRoll'].style.transform = `${this.translateDir}(${this.initPosition}px)`;

      })

    },
    methods: {

      //滚动是按单位长度滚动,不足的则补上滚长度
      getUnitValue(num, dir) {

        if (this.rollByUnit === '' || !(/(^[1-9]\d*$)/.test(this.rollByUnit))) {
          return num;
        }

        let val = Math.abs(num % this.rollByUnit);

        if (dir > 0) {
          return num + val;
        } else {
          return num - (this.rollByUnit - val);
        }

      },

      //开始滚动触发
      handleTouchStart(event) {

        if (this.horizontal === false){
          event.preventDefault();
        }

        if (!this.isRolling) {
          return;
        }

        //记录开始位置
        this.startX = event.touches[0][this.clientDir];

        //记录开始的时间
        this.moveTime = new Date().getTime();

        //记录移动距离用于计算滑动结束时的速度
        this.moveDistance = this.startX;

        //开始滑动时触发回调
        this.$emit('touchStart', event);

      },

      //滚动中触发
      handleTouchMove(event) {
        if (this.horizontal === false){
          event.preventDefault();
        }

        if (!this.isRolling) {
          return;
        }

        //滑动时移动了多少距离
        this.moveX = event.touches[0][this.clientDir] - this.startX;

        //当前鼠标累计移动了多少距离
        this.$refs['VMRoll'].style.transition = "none";

        let disMove = this.allMove + this.moveX;

        //如果当前鼠标累计移动了的距离大于最大向右可移动的距离,则不能再向右移动
        if (disMove > this.maxDown) {
          disMove = this.maxDown;
        }

        // 当累计移动了的距离小于设定的最大向左距离,则不能向左滑动
        else if (disMove < this.maxUp) {
          disMove = this.maxUp;
        }

        //利用动画实现滑动效果
        this.$refs['VMRoll'].style.transition = "transform 100ms cubic-bezier(.165, .84, .44, 1) 0s";
        this.$refs['VMRoll'].style.transform = `${this.translateDir}(${disMove}px)`;

        //计算时间戳,大于300属于慢速滑动,小于300则属于快速滑动
        if (new Date().getTime() - this.moveTime > 300) {
          this.moveTime = new Date().getTime();
          this.moveDistance = event.touches[0][this.clientDir];
        }

        //滑动过程中触发回调
        this.$emit('touchMove', event);

      },

      //滚动停止时触发
      handleTouchStop(event) {
        if (this.horizontal === false){
          event.preventDefault();
        }

        if (!this.isRolling) {
          return;
        }

        //算出滑动开始到滑动结束的距离,往左边滑动为负数,往右边滑动为正数
        let m = event.changedTouches[0][this.clientDir] - this.startX;

        //累计距离
        this.allMove = this.allMove + m;

        //计算滑动速度
        let v = Math.abs((event.changedTouches[0][this.clientDir] - this.moveDistance)
        / (new Date().getTime() - this.moveTime))
        console.log(v)

        //经测试,滑动速度大于0.6计算为快滑动
        if (m > 0) { //往右边滑动时,加上多余滑动的距离,距离随着速度梯度变化
          this.allMove = this.allMove + Math.abs(this.distanceMore * v);
        } else if(m < 0) { //往左边滑动时,减去多余滑动的距离,距离随着速度梯度变化
          this.allMove = this.allMove - Math.abs(this.distanceMore * v);
        }

        //是否按照规定的单位长度的倍数滑动,不足补齐,多了减去
        this.allMove = this.getUnitValue(this.allMove, m > 0 ? 1 : -1);

        // 让两次滑动的距离不超过设置的边界值
        if (this.allMove >= this.maxUpBounce) {

          this.allMove = this.maxUpBounce;

        } else if (this.allMove < this.maxDownBounce) {

          this.allMove = this.maxDownBounce;

        }

        //利用动画实现滑动效果
        this.$refs['VMRoll'].style.transition = "transform 800ms cubic-bezier(.165, .84, .44, 1) 0s";
        this.$refs['VMRoll'].style.transform = `${this.translateDir}(${this.allMove}px)`;

        //滑动结束触发回调
        this.$emit('touchEnd', event,this.allMove);

      }
    }
  }
</script>

<style lang="scss" scoped>
  .v-m-box {
    width: 100%;
    height: 100%;
    overflow: hidden;
    position: relative;

    .v-m-roll {
      position: absolute;
      left: 0;
      top: 0;
    }

    .width100{
      width: 100%;
    }

  }
</style>

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值