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实现自定义风格的滑动条实现代码
Apr 26 Javascript
JavaScript中json对象和string对象之间相互转化
Dec 26 Javascript
jquery的冒泡事件的阻止与允许(三种实现方法)
Feb 01 Javascript
深入了解javascript中的prototype与继承
Apr 14 Javascript
jQuery实现向下滑出的平滑下拉菜单效果
Aug 21 Javascript
jquery验证邮箱格式是否正确实例讲解
Nov 16 Javascript
提高JavaScript执行效率的23个实用技巧
Mar 01 Javascript
Vue.js实现输入框绑定的实例代码
Aug 24 Javascript
微信小程序返回多级页面的实现方法
Oct 27 Javascript
JS实现的简单下拉框联动功能示例
May 11 Javascript
图文讲解vue的v-if使用方法
Feb 11 Javascript
vue中watch和computed为什么能监听到数据的改变以及不同之处
Dec 27 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
php中数字、字符与对象判断函数用法实例
2014/11/26 PHP
phpStudy 2016 使用教程详解(支持PHP7)
2017/10/18 PHP
Laravel相关的一些故障解决
2020/08/19 PHP
Javascript YUI 读码日记之 YAHOO.util.Dom - Part.3
2008/03/22 Javascript
javascript文件中引用依赖的js文件的方法
2014/03/17 Javascript
单击和双击事件的冲突处理示例代码
2014/04/03 Javascript
原生javascript模仿win8等待提示圆圈进度条
2014/04/24 Javascript
javascript学习笔记(五)原型和原型链详解
2014/10/08 Javascript
js实现可折叠展开的手风琴菜单效果
2015/09/07 Javascript
不得不分享的JavaScript常用方法函数集(上)
2015/12/23 Javascript
jQuery中通过ajax的get()函数读取页面的方法
2016/02/29 Javascript
深入浅析JavaScript中数据共享和数据传递
2016/04/25 Javascript
Nodejs进阶:如何将图片转成datauri嵌入到网页中去实例
2016/11/21 NodeJs
JavaScript正则替换HTML标签功能示例
2017/03/02 Javascript
jquery.param()实现数组或对象的序列化方法
2018/10/08 jQuery
微信小程序如何调用json数据接口并解析
2019/06/29 Javascript
[02:20]DOTA2中文配音宣传片
2013/05/22 DOTA
[04:29]2014DOTA2国际邀请赛 主赛事第三日TOPPLAY
2014/07/21 DOTA
[01:08]DOTA2次级职业联赛 - Shield战队宣传片
2014/12/01 DOTA
深入探究Python中变量的拷贝和作用域问题
2015/05/05 Python
Python中的random()方法的使用介绍
2015/05/15 Python
Python中tell()方法的使用详解
2015/05/24 Python
Python3连接MySQL(pymysql)模拟转账实现代码
2016/05/24 Python
Flask框架的学习指南之开发环境搭建
2016/11/20 Python
Python操作SQLite数据库的方法详解【导入,创建,游标,增删改查等】
2017/07/11 Python
浅谈python 里面的单下划线与双下划线的区别
2017/12/01 Python
解决python大批量读写.doc文件的问题
2018/05/08 Python
django将图片上传数据库后在前端显式的方法
2018/05/25 Python
Python基于pandas实现json格式转换成dataframe的方法
2018/06/22 Python
使用NumPy读取MNIST数据的实现代码示例
2019/11/20 Python
python中return的返回和执行实例
2019/12/24 Python
Spanx塑身衣官网:美国知名内衣品牌
2017/01/11 全球购物
Lowe’s加拿大:家居装修、翻新和五金店
2019/12/06 全球购物
劳动竞赛活动总结
2014/05/05 职场文书
社会学专业求职信
2014/07/17 职场文书
教师党员学习群众路线心得体会
2014/11/04 职场文书