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

Zipple發表於2018-12-26

效果圖

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

說明

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

元件難點

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

前置條件

為了充分發揮vue的特性,我們不應該通過ref來直接操作dom,而是應該通過修改資料項從而讓vue自動更新dom。因此,我們這樣編寫template

<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的,其他的樣式很簡單,大家自由發揮。

初始化位置

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

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

監聽滾動

為了能夠讓這個浮動按鈕能夠在頁面滾動時隱藏,第一步要做的就是監聽頁面滾動事件。

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

嗯,別忘了取消註冊。

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

這樣就能夠讓元件在頁面滾動時往右再移動25畫素的距離。 but!我還沒有寫動畫誒...

過渡動畫

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

滾動什麼時候完成呢?

監聽到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>

複製程式碼

相關文章