Vue實現浮動按鈕組件 - 頁面滾動時自動隱藏 - 可拖拽

效果圖

說明

本文可能有點囉嗦了...css

組件難點

  • 如何監聽滾動完成事件
  • 移動端如何監聽拖拽事件

前置條件

爲了充分發揮vue的特性,咱們不該該經過ref來直接操做dom,而是應該經過修改數據項從而讓vue自動更新dom。所以,咱們這樣編寫templatehtml

<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的,其餘的樣式很簡單,你們自由發揮。vue

初始化位置

首次進入頁面時,按鈕應該處於一個初始位置。咱們在created鉤子中進行初始化。html5

created(){
      this.left = document.documentElement.clientWidth - 50;
      this.top = document.documentElement.clientHeight*0.8;
    },
複製代碼

監聽滾動

爲了可以讓這個浮動按鈕可以在頁面滾動時隱藏,第一步要作的就是監聽頁面滾動事件。chrome

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

嗯,別忘了取消註冊。瀏覽器

beforeDestroy(){
      window.removeEventListener('scroll', this.handleScrollStart);
    },
複製代碼

這樣就可以讓組件在頁面滾動時往右再移動25像素的距離。 but!我尚未寫動畫誒...less

過渡動畫

嗯,我固然不會使用js寫動畫了,咱們在css.ys-float-btn中加上transition: all 0.3s; 過渡動畫就搞定了。dom

滾動何時完成呢?

監聽到scroll事件只是第一步,那麼何時scroll事件纔會中止呢?瀏覽器並無爲咱們準備這樣一個事件,咱們須要手動去實現它。思路其實也很簡單,當一個時間週期內頁面的scrollTop不變就說明頁面滾動中止了。 因此咱們須要在data函數裏返回一個timer對象,用來存儲咱們的定時器。像這樣:函數

data(){
      return{
        timer:null,
        currentTop:0
      }
    }
複製代碼

改造一下handleScrollStart方法。 觸發scroll的時候清掉當前的計時器(若是存在),並從新計時測試

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方法

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

若是如今的滾動高度等於以前的滾動高度,說明頁面沒有繼續滾動了。將left調整爲初始位置。

關於拖拽我踩過的坑

爲了實現組件的拖拽功能,我最早想到的就是html5爲咱們提供的drag方法。所以像這樣,爲咱們的template增長這樣的代碼。

<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取消了移動端模式,而後發現拖拽監聽方法執行了。

這真是,無力吐槽。 記筆記了:移動端沒法使用drag來進行組件的拖拽操做

移動端拖拽

那麼移動端如何實現拖拽效果呢?瞭解到移動端有touch事件。touchclick事件觸發的前後順序以下所示:

touchstart => touchmove => touchend => click。

這裏咱們須要爲組件註冊監聽以上touch事件,怎麼拿到具體的dom呢? vue爲咱們提供了ref屬性。

在這裏插入圖片描述
咱們給 template最外層的 div加上 ref

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

爲了確保組件已經成功掛載,咱們在nextTick中進行事件註冊。如今mounted鉤子方法長這樣:

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

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

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

        });
      });
    },
複製代碼

在對組件進行拖拽的過程當中,應當不須要組件的過分動畫的,因此咱們在touchstart中取消過分動畫。

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.top = touch.clientY;
          }
        });
複製代碼

可能有同窗看了上面的代碼以後已經看出來所疏漏的地方了,上述代碼彷佛可以讓組件跟隨手指移動了,可是還差了點。由於並非組件中心跟隨手指在移動。咱們微調一下:

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;
          }
        });
複製代碼

拖拽結束之後,判斷在頁面的稍左仍是稍右,從新調整組件的位置並從新設置過分動畫。

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方法。

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);
        }
      }
複製代碼

重構

剛剛噼裏啪啦一頓敲鍵盤終於把這個組件寫完啦,這樣是否是就完事大吉了呢?不,固然不。咱們爲何要寫組件呢?不就是爲了重用嗎,如今這個組件裏充斥着各類沒有標明意義的數字和重複代碼,是時候重構一下了。 開發組件一般是數據先行,如今咱們回過頭來看一下哪些數據須要預約義。

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定義:

data(){
      return{
        timer:null,
        currentTop:0,
        clientWidth:0,
        clientHeight:0,
        left:0,
        top:0,
      }
    }
複製代碼

所以,在組件建立的時候咱們須要爲這些數據作預處理! 如今created長這樣:

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;
    },
複製代碼

... 就到這裏吧,後面的都差很少了....

完整源碼

<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>

複製代碼
相關文章
相關標籤/搜索