Vue.js实现浮动按钮组件 – 页面滚动时自动隐藏 – 可拖拽

2018-12-2713:36:48WEB前端开发Comments5,818 views字数 7502阅读模式

Vue.js实现浮动按钮组件 – 页面滚动时自动隐藏 – 可拖拽文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9088.html

组件难点

  • 如何监听滚动完成事件
  • 移动端如何监听拖拽事件

前置条件

为了充分发挥vue的特性,我们不应该通过ref来直接操作dom,而是应该通过修改数据项从而让vue自动更新dom。因此,我们这样编写template文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9088.html

<template>
  <div class="ys-float-btn" :style="{'left':left+'px','top':top+'px'}"> 
    <slot name="icon"></slot>
    <p>{{text}}</p>
  </div>
</template>
复制代码

当然.ys-float-btn肯定是position:fixed的,其他的样式很简单,大家自由发挥。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9088.html

初始化位置

首次进入页面时,按钮应该处于一个初始位置。我们在created钩子中进行初始化。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9088.html

    created(){
      this.left = document.documentElement.clientWidth - 50;
      this.top = document.documentElement.clientHeight*0.8;
    },
复制代码

监听滚动

为了能够让这个浮动按钮能够在页面滚动时隐藏,第一步要做的就是监听页面滚动事件。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9088.html

mounted(){
  window.addEventListener('scroll', this.handleScrollStart);
},
methods:{
   handleScrollStart(){
     this.left = document.documentElement.clientWidth - 25;
  }
}
复制代码

嗯,别忘了取消注册。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9088.html

    beforeDestroy(){
      window.removeEventListener('scroll', this.handleScrollStart);
    },
复制代码

这样就能够让组件在页面滚动时往右再移动25像素的距离。 but!我还没有写动画诶...文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9088.html

过渡动画

嗯,我当然不会使用js写动画了,我们在css.ys-float-btn中加上transition: all 0.3s; 过渡动画就搞定了。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9088.html

滚动什么时候完成呢?

监听到scroll事件只是第一步,那么什么时候scroll事件才会停止呢?浏览器并没有为我们准备这样一个事件,我们需要手动去实现它。思路其实也很简单,当一个时间周期内页面的scrollTop不变就说明页面滚动停止了。 所以我们需要在data函数里返回一个timer对象,用来存储我们的定时器。像这样:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9088.html

 data(){
      return{
        timer:null,
        currentTop:0
      }
    }
复制代码

改造一下handleScrollStart方法。 触发scroll的时候清掉当前的计时器(如果存在),并重新计时文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9088.html

      handleScrollStart(){
        this.timer&&clearTimeout(this.timer);
        this.timer = setTimeout(()=>{
          this.handleScrollEnd();
        },300);
        this.currentTop = document.documentElement.scrollTop || document.body.scrollTop;
        this.left = document.documentElement.clientWidth - 25;
      },
复制代码

现在增加了一个回调handleScrollEnd方法文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9088.html

      handleScrollEnd(){
        let scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
        if(scrollTop === this.currentTop){
           this.left = document.documentElement.clientWidth - 50;
          clearTimeout(this.timer);
        }
      }
复制代码

如果现在的滚动高度等于之前的滚动高度,说明页面没有继续滚动了。将left调整为初始位置。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9088.html

关于拖拽我踩过的坑

为了实现组件的拖拽功能,我最先想到的就是html5为我们提供的drag方法。因此像这样,为我们的template增加这样的代码。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9088.html

  <div class="ys-float-btn" :style="{'width':itemWidth+'px','height':itemHeight+'px','left':left+'px','top':top+'px'}"
  :draggable ='true' @dragstart="onDragStart" @dragover.prevent = "onDragOver"  @dragenter="onDragEnter" @dragend="onDragEnd">
    <slot name="icon"></slot>
    <p>{{text}}</p>
  </div>
复制代码

结果在测试的时候就是没有效果,设置的四个监听方法一个都没有执行。迷茫了好久,后来在自己找bug期间无意将chrome取消了移动端模式,然后发现拖拽监听方法执行了。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9088.html

这真是,无力吐槽。 记笔记了:移动端无法使用drag来进行组件的拖拽操作文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9088.html

移动端拖拽

那么移动端如何实现拖拽效果呢?了解到移动端有touch事件。touchclick事件触发的先后顺序如下所示:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9088.html

touchstart => touchmove => touchend => click。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9088.html

这里我们需要为组件注册监听以上touch事件,怎么拿到具体的dom呢? vue为我们提供了ref属性。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9088.html

Vue.js实现浮动按钮组件 – 页面滚动时自动隐藏 – 可拖拽

我们给template最外层的div加上ref文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9088.html

  <div class="ys-float-btn" :style="{'left':left+'px','top':top+'px'}"
       ref="div">
    <slot name="icon"></slot>
    <p>{{text}}</p>
  </div>
复制代码

为了确保组件已经成功挂载,我们在nextTick中进行事件注册。现在mounted钩子方法长这样:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9088.html

    mounted(){
      window.addEventListener('scroll', this.handleScrollStart);
      this.$nextTick(()=>{
        const div = this.$refs.div;
        div.addEventListener("touchstart",()=>{

        });
        div.addEventListener("touchmove",(e)=>{

        });
        div.addEventListener("touchend",()=>{

        });
      });
    },
复制代码

在对组件进行拖拽的过程中,应当不需要组件的过度动画的,所以我们在touchstart中取消过度动画。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9088.html

        div.addEventListener("touchstart",()=>{
             div.style.transition = 'none';
        });
复制代码

在拖拽的过程中,组件应该跟随手指的移动而移动。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9088.html

 div.addEventListener("touchmove",(e)=>{
          if (e.targetTouches.length === 1) {//一根手指
            let touch = event.targetTouches[0];
            this.left = touch.clientX;
            this.top = touch.clientY;
          }
        });
复制代码

可能有同学看了上面的代码之后已经看出来所疏漏的地方了,上述代码似乎能够让组件跟随手指移动了,但是还差了点。因为并不是组件中心跟随手指在移动。我们微调一下:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9088.html

 div.addEventListener("touchmove",(e)=>{
          if (e.targetTouches.length === 1) {
            let touch = event.targetTouches[0];
            this.left = touch.clientX - 25;//组件的宽度是50
            this.top = touch.clientY - 25;
          }
        });
复制代码

拖拽结束以后,判断在页面的稍左还是稍右,重新调整组件的位置并重新设置过度动画。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9088.html

div.addEventListener("touchend",()=>{
          div.style.transition = 'all 0.3s';
           if(this.left>document.documentElement.clientWidth/2){
             this.left = document.documentElement.clientWidth - 50;
           }else{
             this.left = 0;
           }
        });
复制代码

写到这里是不是就完了呢? 我们好像漏了点什么。 对了,页面滚动时没有判断组件在左边还是在右边,当时统一当成右边在处理了。 现在修改handleScrollStart和handleScrollEnd方法。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9088.html

      handleScrollStart(){
        this.timer&&clearTimeout(this.timer);
        this.timer = setTimeout(()=>{
          this.handleScrollEnd();
        },300);
        this.currentTop = document.documentElement.scrollTop || document.body.scrollTop;
        if(this.left>document.documentElement.clientWidth/2){
          this.left = document.documentElement.clientWidth - 25;
        }else{
          this.left = -25;
        }
      },
      handleScrollEnd(){
        let scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
        if(scrollTop === this.currentTop){
          if(this.left>document.documentElement.clientWidth/2){
            this.left = document.documentElement.clientWidth - 50;
          }else{
            this.left = 0;
          }
          clearTimeout(this.timer);
        }
      }
复制代码

重构

刚刚噼里啪啦一顿敲键盘终于把这个组件写完啦,这样是不是就完事大吉了呢?不,当然不。我们为什么要写组件呢?不就是为了重用吗,现在这个组件里充斥着各种没有标明意义的数字和重复代码,是时候重构一下了。 开发组件通常是数据先行,现在我们回过头来看一下哪些数据需要预定义。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9088.html

props:{
      text:{
        type:String,
        default:"默认文字"
      },
      itemWidth:{
        type:Number,
        default:60
      },
      itemHeight:{
        type:Number,
        default:60
      },
      gapWidth:{
        type:Number,
        default:10
      },
      coefficientHeight:{
        type:Number,
        default:0.8
      }
    }
复制代码

我们需要组件的宽高和间隔(与页面边界的间隔),额对了,还有那个视口的宽度!我们在前文中多次使用document.documentElement.clientWidth 不知道你们有没有看烦,我反正是写烦了.... 组件内部用的数据我们用data定义:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9088.html

data(){
      return{
        timer:null,
        currentTop:0,
        clientWidth:0,
        clientHeight:0,
        left:0,
        top:0,
      }
    }
复制代码

因此,在组件创建的时候我们需要为这些数据做预处理! 现在created长这样:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9088.html

    created(){
      this.clientWidth = document.documentElement.clientWidth;
      this.clientHeight = document.documentElement.clientHeight;
      this.left = this.clientWidth - this.itemWidth - this.gapWidth;
      this.top = this.clientHeight*this.coefficientHeight;
    },
复制代码

... 就到这里吧,后面的都差不多了....文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9088.html

完整源码

<template>
  <div class="ys-float-btn" :style="{'width':itemWidth+'px','height':itemHeight+'px','left':left+'px','top':top+'px'}"
       ref="div"
       @click ="onBtnClicked">
    <slot name="icon"></slot>
    <p>{{text}}</p>
  </div>
</template>

<script>
  export default {
    name: "FloatImgBtn",
    props:{
      text:{
        type:String,
        default:"默认文字"
      },
      itemWidth:{
        type:Number,
        default:60
      },
      itemHeight:{
        type:Number,
        default:60
      },
      gapWidth:{
        type:Number,
        default:10
      },
      coefficientHeight:{
        type:Number,
        default:0.8
      }
    },
    created(){
      this.clientWidth = document.documentElement.clientWidth;
      this.clientHeight = document.documentElement.clientHeight;
      this.left = this.clientWidth - this.itemWidth - this.gapWidth;
      this.top = this.clientHeight*this.coefficientHeight;
    },
    mounted(){
      window.addEventListener('scroll', this.handleScrollStart);
      this.$nextTick(()=>{
        const div = this.$refs.div;
        div.addEventListener("touchstart",()=>{
          div.style.transition = 'none';
        });
        div.addEventListener("touchmove",(e)=>{
          if (e.targetTouches.length === 1) {
            let touch = event.targetTouches[0];
            this.left = touch.clientX - this.itemWidth/2;
            this.top = touch.clientY - this.itemHeight/2;
          }
        });
        div.addEventListener("touchend",()=>{
          div.style.transition = 'all 0.3s';
           if(this.left>this.clientWidth/2){
             this.left = this.clientWidth - this.itemWidth - this.gapWidth;
           }else{
             this.left = this.gapWidth;
           }
        });

      });
    },
    beforeDestroy(){
      window.removeEventListener('scroll', this.handleScrollStart);
    },
    methods:{
      onBtnClicked(){
        this.$emit("onFloatBtnClicked");
      },
      handleScrollStart(){
        this.timer&&clearTimeout(this.timer);
        this.timer = setTimeout(()=>{
          this.handleScrollEnd();
        },300);
        this.currentTop = document.documentElement.scrollTop || document.body.scrollTop;
        if(this.left>this.clientWidth/2){
          this.left = this.clientWidth - this.itemWidth/2;
        }else{
          this.left = -this.itemWidth/2;
        }
      },
      handleScrollEnd(){
        let scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
        if(scrollTop === this.currentTop){
          if(this.left>this.clientWidth/2){
            this.left = this.clientWidth - this.itemWidth - this.gapWidth;
          }else{
            this.left = this.gapWidth;
          }
          clearTimeout(this.timer);
        }
      }
    },
    data(){
      return{
        timer:null,
        currentTop:0,
        clientWidth:0,
        clientHeight:0,
        left:0,
        top:0,
      }
    }
  }
</script>

<style lang="less" scoped>
  .ys-float-btn{
    background:rgb(255,255,255);
    box-shadow:0 2px 10px 0 rgba(0,0,0,0.1);
    border-radius:50%;
    color: #666666;
    z-index: 20;
    transition: all 0.3s;

    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;

    position: fixed;
    bottom: 20vw;

    img{
      width: 50%;
      height: 50%;
      object-fit: contain;
      margin-bottom: 3px;
    }

    p{
      font-size:7px;
    }
  }
</style>

作者:Zipple
链接:https://juejin.im/post/5c237c5ce51d453603630837
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9088.html

文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9088.html
  • 本站内容整理自互联网,仅提供信息存储空间服务,以方便学习之用。如对文章、图片、字体等版权有疑问,请在下方留言,管理员看到后,将第一时间进行处理。
  • 转载请务必保留本文链接:https://www.cainiaoxueyuan.com/gcs/9088.html

Comment

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定