vue 实现微信浮标效果


Posted in Javascript onSeptember 01, 2019

微信的浮窗,大伙应该都用过,当我们正在阅读一篇公众号文章时,突然需要处理微信消息,点击浮窗,在微信上会有个浮标,点击浮标可以再次回到文章。

我们今天打算撸一个类似微信的浮标组件,我们期望组件有以下功能

  • 支持拖拽
  • 支持左右吸附
  • 支持页面上下滑动时隐藏

效果预览

vue 实现微信浮标效果 

拖拽事件

浮标的核心功能的就是拖拽,对鼠标或移动端的触摸的事件来说,有三个阶段,鼠标或手指接触到元素时,鼠标或手指在移动的过程,鼠标或手指离开元素。这个三个阶段对应的事件名称如下:

mouse: {
  start: 'mousedown',
  move: 'mousemove',
  stop: 'mouseup'
},
touch: {
  start: 'touchstart',
  move: 'touchmove',
  stop: 'touchend'
}

元素定位

滑动容器我们采用绝对定位,通过设置 top 和 left 属性来改变元素的位置,那我们怎么获取到新的 top 和 left 呢?

我们先看下面这张图

vue 实现微信浮标效果 

黄色区域是拖拽的元素,蓝色的点就是鼠标或手指触摸的位置,在元素移动的过程中,这些值也会随着发生改变,那么我们只要计算出新的触摸位置和最初触摸位置的横坐标和竖坐标的变化,就可以算出移动后的 top left ,因为拖拽的元素不随着页面滚动而变化,所以我们采用 pageX pageY 这两个值。用公式简单描述就是;

newTop = initTop + (currentPageY - initPageY)
newLeft = initLeft + (currentPageX - initPageX)

拖拽区域

拖拽区域默认是在拖拽元素的父级元素内,所以我们需要计算出父级元素的宽高。这里有一点需要注意,如果父级的宽高是由异步事件来改变的,那么获取的时候就会不准确,这种情况就需要改变下布局。

private getParentSize() {
  const style = window.getComputedStyle(
    this.$el.parentNode as Element,
    null
  );

  return [
    parseInt(style.getPropertyValue('width'), 10),
    parseInt(style.getPropertyValue('height'), 10)
  ];

}

拖拽的前中后

有了上面的基础,我们分析下拖拽的三个阶段我们需要做哪些工作

  • 触摸元素,即开始拖拽,将当前元素的 top left 和触摸点的 pageX pageY 用对象存储起来,然后监听移动和结束事件
  • 元素拖拽过程,计算当前的 pageX pageY 与 初始的 pageX pageY 的差值,算出当前的 top left ,更新元素的位置
  • 拖拽结束,重置初始值

左右吸附

在手指离开后,若元素偏向某一侧,便吸附在该侧的边上,那么在拖拽事件结束后,根据元素的X轴中心的与父级元素的X轴中心点做比较,就可知道往左还是往右移动

页面上下滑动时隐藏

使用 watch 监听父级容器的滑动事件,获取 scrollTop ,当 scrollTop 的值不在发生变化的时候,就说明页面滑动结束了,在变化前和结束时设置 left 即可。

若无法监听父级容器滑动事件,那么可以将监听事件放到外层组件,将 scrollTop 传入拖拽组件也是可以的。

代码实现

组件用的是 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; // 是否开启吸附左右两侧
  @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; // 监听事件
  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)
    ];
  }
  /**
   * 滑动区域计算
   */
  private calcDragLimits() {
    return {
      minLeft: 0,
      maxLeft: Math.floor(this.parentWidth - this.width),
      minTop: 0,
      maxTop: Math.floor(this.parentHeight - this.height),
    };
  }
  /**
   * 监听滑动开始
   */
  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;
    // 监听移动事件 结束事件
    dom.addEvent(document.documentElement, this.eventsFor.move, this.move);
    dom.addEvent(
      document.documentElement,
      this.eventsFor.stop,
      this.handleUp
    );
  }
  
  /**
   * 监听拖拽过程
   */
  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);
  }
  /**
   * 监听滑动结束
   */
  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();
  }
  /**
   * 重置初始数据
   */
  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') // 监听 props.scrollTop 
  @Watch('parentScrollTop') // 监听父级组件
  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.ts
export 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 实现微信浮标效果,希望对大家有所帮助,如果大家有任何疑问欢迎给我留言,小编会及时回复大家的!

Javascript 相关文章推荐
JQUERY THICKBOX弹出层插件
Aug 30 Javascript
改善你的jQuery的25个步骤 千倍级效率提升
Feb 11 Javascript
自己动手制作jquery插件之自动添加删除行的实现
Oct 13 Javascript
javascript遍历控件实例详细解析
Jan 10 Javascript
使用jQuery实现图片遮罩半透明坠落遮挡
Mar 16 Javascript
初步使用Node连接Mysql数据库
Mar 03 Javascript
js改变css样式的三种方法推荐
Jun 28 Javascript
JavaScript闭包和范围实例详解
Dec 19 Javascript
详解在vue-cli项目中使用mockjs(请求数据删除数据)
Oct 23 Javascript
Angular2中监听数据更新的方法
Aug 31 Javascript
React中使用外部样式的3种方式(小结)
May 28 Javascript
如何让微信小程序页面之间的通信不再变困难
Jun 03 Javascript
微信小程序获取位置展示地图并标注信息的实例代码
Sep 01 #Javascript
JS实现可用滑块滑动的缓动图代码
Sep 01 #Javascript
vue动态子组件的两种实现方式
Sep 01 #Javascript
Vue CLI项目 axios模块前后端交互的使用(类似ajax提交)
Sep 01 #Javascript
简单分析js中的this的原理
Aug 31 #Javascript
微信小程序image图片加载完成监听
Aug 31 #Javascript
JS实现使用POST方式发送请求
Aug 30 #Javascript
You might like
全国FM电台频率大全 - 2 天津市
2020/03/11 无线电
php 动态多文件上传
2009/01/18 PHP
php中模拟POST传递数据的两种方法分享
2011/09/16 PHP
浅析php创建者模式
2014/11/25 PHP
基于PHP微信红包的算法探讨
2016/07/21 PHP
PHP与以太坊交互详解
2018/08/24 PHP
微信公众平台开发教程①获取用户Openid及个人信息图文详解
2019/04/10 PHP
PHP中str_split()函数的用法讲解
2019/04/11 PHP
tp5(thinkPHP5框架)使用DB实现批量删除功能示例
2019/05/28 PHP
基于Jquery的将DropDownlist的选中值赋给label的实现代码
2011/05/06 Javascript
javascript算法题 求任意一个1-9位不重复的N位数在该组合中的大小排列序号
2012/07/21 Javascript
js获取php变量的实现代码
2013/08/10 Javascript
函数式 JavaScript(一)简介
2014/07/07 Javascript
JavaScript中使用stopPropagation函数停止事件传播例子
2014/08/27 Javascript
原生javascript 学习之js变量全面了解
2016/07/14 Javascript
javascript 判断页面访问方式电脑或者移动端
2016/09/19 Javascript
BootStrap Table 获取同行不同列元素的方法
2016/12/19 Javascript
Vue Element使用icon图标教程详解(第三方)
2018/02/07 Javascript
Angular动态绑定样式及改变UI框架样式的方法小结
2018/09/03 Javascript
利用JavaScript的Map提升性能的方法详解
2019/08/14 Javascript
微信小程序订阅消息(java后端实现)开发
2020/06/01 Javascript
[50:02]完美世界DOTA2联赛循环赛 Magma vs IO BO2第一场 11.01
2020/11/02 DOTA
Python的subprocess模块总结
2014/11/07 Python
Python基于pillow判断图片完整性的方法
2016/09/18 Python
python操作mysql代码总结
2018/06/01 Python
用Python逐行分析文件方法
2019/01/28 Python
python3 实现口罩抽签的功能
2020/03/11 Python
python opencv pytesseract 验证码识别的实现
2020/08/28 Python
eVitamins日本:在线购买折扣维生素、补品和草药
2019/04/04 全球购物
一些Unix笔试题和面试题
2013/01/22 面试题
诚信承诺书范文
2014/03/27 职场文书
行政文员实习自我鉴定范文
2014/09/14 职场文书
房产公证委托书范本
2014/09/20 职场文书
限期整改通知书
2015/04/22 职场文书
科级干部培训心得体会
2016/01/06 职场文书
python基于turtle绘制几何图形
2021/06/15 Python