微信的浮窗,大伙應該都用過,當我們正在閱讀一篇公眾號文章時,突然需要處理微信消息,點擊浮窗,在微信上會有個浮標,點擊浮標可以再次回到文章。
我們今天打算擼一個類似微信的浮標組件,我們期望組件有以下功能
效果預覽
 
拖拽事件
浮標的核心功能的就是拖拽,對鼠標或移動端的觸摸的事件來說,有三個階段,鼠標或手指接觸到元素時,鼠標或手指在移動的過程,鼠標或手指離開元素。這個三個階段對應的事件名稱如下:
mouse: {  start: 'mousedown',  move: 'mousemove',  stop: 'mouseup'},touch: {  start: 'touchstart',  move: 'touchmove',  stop: 'touchend'}元素定位
滑動容器我們采用絕對定位,通過設置 top 和 left 屬性來改變元素的位置,那我們怎么獲取到新的 top 和 left 呢?
我們先看下面這張圖
 
黃色區(qū)域是拖拽的元素,藍色的點就是鼠標或手指觸摸的位置,在元素移動的過程中,這些值也會隨著發(fā)生改變,那么我們只要計算出新的觸摸位置和最初觸摸位置的橫坐標和豎坐標的變化,就可以算出移動后的 top left ,因為拖拽的元素不隨著頁面滾動而變化,所以我們采用 pageX pageY 這兩個值。用公式簡單描述就是;
newTop = initTop + (currentPageY - initPageY)newLeft = initLeft + (currentPageX - initPageX)
拖拽區(qū)域
拖拽區(qū)域默認是在拖拽元素的父級元素內(nèi),所以我們需要計算出父級元素的寬高。這里有一點需要注意,如果父級的寬高是由異步事件來改變的,那么獲取的時候就會不準確,這種情況就需要改變下布局。
private getParentSize() {  const style = window.getComputedStyle(    this.$el.parentNode as Element,    null  );  return [    parseInt(style.getPropertyValue('width'), 10),    parseInt(style.getPropertyValue('height'), 10)  ];}拖拽的前中后
有了上面的基礎,我們分析下拖拽的三個階段我們需要做哪些工作
左右吸附
在手指離開后,若元素偏向某一側(cè),便吸附在該側(cè)的邊上,那么在拖拽事件結束后,根據(jù)元素的X軸中心的與父級元素的X軸中心點做比較,就可知道往左還是往右移動
頁面上下滑動時隱藏
使用 watch 監(jiān)聽父級容器的滑動事件,獲取 scrollTop ,當 scrollTop 的值不在發(fā)生變化的時候,就說明頁面滑動結束了,在變化前和結束時設置 left 即可。
若無法監(jiān)聽父級容器滑動事件,那么可以將監(jiān)聽事件放到外層組件,將 scrollTop 傳入拖拽組件也是可以的。
代碼實現(xiàn)
組件用的是 ts 寫的,代碼略長,大伙可以先收藏在看
// draggable.vue<template>  <div class="dra " :class="{'dra-tran':showtran}" :style="style" @mousedown="elementTouchDown" @touchstart="elementTouchDown">    <slot></slot>  </div></template><script lang="ts">import { Component, Prop, Vue, Watch } from 'vue-property-decorator';import dom from './dom';const events = {  mouse: {    start: 'mousedown',    move: 'mousemove',    stop: 'mouseup'  },  touch: {    start: 'touchstart',    move: 'touchmove',    stop: 'touchend'  }};const userSelectNone = {  userSelect: 'none',  MozUserSelect: 'none',  WebkitUserSelect: 'none',  MsUserSelect: 'none'};const userSelectAuto = {  userSelect: 'auto',  MozUserSelect: 'auto',  WebkitUserSelect: 'auto',  MsUserSelect: 'auto'};@Component({  name: 'draggable',})export default class Draggable extends Vue {  @Prop(Number) private width !: number; // 寬  @Prop(Number) private height !: number; // 高  @Prop({ type: Number, default: 0 }) private x!: number; //初始x  @Prop({ type: Number, default: 0 }) private y!: number; //初始y  @Prop({ type: Number, default: 0 }) private scrollTop!: number; // 初始 scrollTop  @Prop({ type: Boolean,default:true}) private draggable !:boolean; // 是否開啟拖拽  @Prop({ type: Boolean,default:true}) private adsorb !:boolean; // 是否開啟吸附左右兩側(cè)  @Prop({ type: Boolean,default:true}) private scrollHide !:boolean; // 是否開啟滑動隱藏  private rawWidth: number = 0;   private rawHeight: number = 0;   private rawLeft: number = 0;   private rawTop: number = 0;  private top: number = 0; // 元素的 top  private left: number = 0; // 元素的 left  private parentWidth: number = 0; // 父級元素寬  private parentHeight: number = 0; // 父級元素高  private eventsFor = events.mouse; // 監(jiān)聽事件  private mouseClickPosition = { // 鼠標點擊的當前位置    mouseX: 0,    mouseY: 0,    left: 0,    top: 0,  };  private bounds = {    minLeft: 0,    maxLeft: 0,    minTop: 0,    maxTop: 0,  };  private dragging: boolean = false;  private showtran: boolean = false;  private preScrollTop: number = 0;  private parentScrollTop: number = 0;  private mounted() {    this.rawWidth = this.width;    this.rawHeight = this.height;    this.rawLeft = this.x;    this.rawTop = this.y;    this.left = this.x;    this.top = this.y;    [this.parentWidth, this.parentHeight] = this.getParentSize();    // 對邊界計算    this.bounds = this.calcDragLimits();    if(this.adsorb){      dom.addEvent(this.$el.parentNode,'scroll',this.listScorll)    }  }  private listScorll(e:any){    this.parentScrollTop = e.target.scrollTop  }  private beforeDestroy(){    dom.removeEvent(document.documentElement, 'touchstart', this.elementTouchDown);    dom.removeEvent(document.documentElement, 'mousedown', this.elementTouchDown);    dom.removeEvent(document.documentElement, 'touchmove', this.move);    dom.removeEvent(document.documentElement, 'mousemove', this.move);    dom.removeEvent(document.documentElement, 'mouseup', this.handleUp);    dom.removeEvent(document.documentElement, 'touchend', this.handleUp);  }  private getParentSize() {    const style = window.getComputedStyle(      this.$el.parentNode as Element,      null    );    return [      parseInt(style.getPropertyValue('width'), 10),      parseInt(style.getPropertyValue('height'), 10)    ];  }  /**   * 滑動區(qū)域計算   */  private calcDragLimits() {    return {      minLeft: 0,      maxLeft: Math.floor(this.parentWidth - this.width),      minTop: 0,      maxTop: Math.floor(this.parentHeight - this.height),    };  }  /**   * 監(jiān)聽滑動開始   */  private elementTouchDown(e: TouchEvent) {    if(this.draggable){      this.eventsFor = events.touch;      this.elementDown(e);    }  }  private elementDown(e: TouchEvent | MouseEvent) {    const target = e.target || e.srcElement;    this.dragging = true;    this.mouseClickPosition.left = this.left;    this.mouseClickPosition.top = this.top;    this.mouseClickPosition.mouseX = (e as TouchEvent).touches      ? (e as TouchEvent).touches[0].pageX      : (e as MouseEvent).pageX;    this.mouseClickPosition.mouseY = (e as TouchEvent).touches      ? (e as TouchEvent).touches[0].pageY      : (e as MouseEvent).pageY;    // 監(jiān)聽移動事件 結束事件    dom.addEvent(document.documentElement, this.eventsFor.move, this.move);    dom.addEvent(      document.documentElement,      this.eventsFor.stop,      this.handleUp    );  }    /**   * 監(jiān)聽拖拽過程   */  private move(e: TouchEvent | MouseEvent) {    if(this.dragging){      this.elementMove(e);    }  }  private elementMove(e: TouchEvent | MouseEvent) {    const mouseClickPosition = this.mouseClickPosition;    const tmpDeltaX = mouseClickPosition.mouseX - ((e as TouchEvent).touches ? (e as TouchEvent).touches[0].pageX : (e as MouseEvent).pageX) || 0;    const tmpDeltaY = mouseClickPosition.mouseY - ((e as TouchEvent).touches ? (e as TouchEvent).touches[0].pageY : (e as MouseEvent).pageY) || 0;    if (!tmpDeltaX && !tmpDeltaY) return;    this.rawTop = mouseClickPosition.top - tmpDeltaY;    this.rawLeft = mouseClickPosition.left - tmpDeltaX;    this.$emit('dragging', this.left, this.top);  }  /**   * 監(jiān)聽滑動結束   */  private handleUp(e: TouchEvent | MouseEvent) {    this.rawTop = this.top;    this.rawLeft = this.left;    if (this.dragging) {      this.dragging = false;      this.$emit('dragstop', this.left, this.top);    }    // 左右吸附    if(this.adsorb){      this.showtran = true      const middleWidth = this.parentWidth / 2;      if((this.left + this.width/2) < middleWidth){        this.left = 0      }else{        this.left = this.bounds.maxLeft - 10      }      setTimeout(() => {        this.showtran = false      }, 400);    }    this.resetBoundsAndMouseState();  }  /**   * 重置初始數(shù)據(jù)   */  private resetBoundsAndMouseState() {    this.mouseClickPosition = {      mouseX: 0,      mouseY: 0,      left: 0,      top: 0,    };  }  /**   * 元素位置   */  private get style() {    return {      position: 'absolute',      top: this.top + 'px',      left: this.left + 'px',      width: this.width + 'px',      height: this.height + 'px',      ...(this.dragging ? userSelectNone : userSelectAuto)    };  }  @Watch('rawTop')  private rawTopChange(newTop: number) {    const bounds = this.bounds;    if (bounds.maxTop === 0) {      this.top = newTop;      return;    }    const left = this.left;    const top = this.top;    if (bounds.minTop !== null && newTop < bounds.minTop) {      newTop = bounds.minTop;    } else if (bounds.maxTop !== null && bounds.maxTop < newTop) {      newTop = bounds.maxTop;    }    this.top = newTop;  }  @Watch('rawLeft')  private rawLeftChange(newLeft: number) {    const bounds = this.bounds;    if (bounds.maxTop === 0) {      this.left = newLeft;      return;    }    const left = this.left;    const top = this.top;    if (bounds.minLeft !== null && newLeft < bounds.minLeft) {      newLeft = bounds.minLeft;    } else if (bounds.maxLeft !== null && bounds.maxLeft < newLeft) {      newLeft = bounds.maxLeft;    }    this.left = newLeft;  }  @Watch('scrollTop') // 監(jiān)聽 props.scrollTop   @Watch('parentScrollTop') // 監(jiān)聽父級組件  private scorllTopChange(newTop:number){    let timer = undefined;    if(this.scrollHide){      clearTimeout(timer);      this.showtran = true;      this.preScrollTop = newTop;      this.left = this.bounds.maxLeft + this.width - 10      timer = setTimeout(()=>{        if(this.preScrollTop === newTop ){          this.left = this.bounds.maxLeft - 10;          setTimeout(()=>{            this.showtran = false;          },300)        }      },200)    }  }} </script><style lang="css" scoped>.dra {  touch-action: none;}.dra-tran {  transition: top .2s ease-out , left .2s ease-out;}</style>// dom.tsexport default {  addEvent(el: any, event: string, handler: any) {    if (!el) {      return;    }    if (el.attachEvent) {      el.attachEvent('on' + event, handler);    } else if (el.addEventListener) {      el.addEventListener(event, handler, true);    } else {      el['on' + event] = handler;    }  },  removeEvent(el: any, event: string, handler: any) {    if (!el) {      return;    }    if (el.detachEvent) {      el.detachEvent('on' + event, handler);    } else if (el.removeEventListener) {      el.removeEventListener(event, handler, true);    } else {      el['on' + event] = null;    }  }};總結
以上所述是小編給大家介紹的vue 實現(xiàn)微信浮標效果,希望對大家有所幫助,如果大家有任何疑問歡迎給我留言,小編會及時回復大家的!
新聞熱點
疑難解答