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 相关文章推荐
彪哥1.1(智能表格)提供下载
Sep 07 Javascript
再谈IE中Flash控件的自动激活 ObjectWrap
Mar 09 Javascript
解决遍历时Array.indexOf产生的性能问题
Jul 03 Javascript
无缝滚动js代码通俗易懂(自写)
Jun 19 Javascript
解决jquery无法找到其他父级子集问题的方法
May 10 Javascript
详解Angular的数据显示优化处理
Dec 26 Javascript
js实现鼠标左右移动,图片也跟着移动效果
Jan 25 Javascript
详解Angularjs 如何自定义Img的ng-load 事件
Feb 15 Javascript
利用require.js与angular搭建spa应用的方法实例
Jul 19 Javascript
AngularJS 打开新的标签页实现代码
Sep 07 Javascript
微信小程序实现同一页面取值的方法分析
Apr 30 Javascript
原生JS实现拖拽功能
Dec 16 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
echo(),print(),print_r()之间的区别?
2006/11/19 PHP
PHP中echo和print的区别
2014/08/28 PHP
php分页函数完整实例代码
2014/09/22 PHP
PHP实现指定字段的多维数组排序函数分享
2015/03/09 PHP
php转换颜色为其反色的方法
2015/04/27 PHP
PHP MVC框架路由学习笔记
2016/03/02 PHP
Yii框架引用插件和ckeditor中body与P标签去除的方法
2017/01/19 PHP
PHP5.5新特性之yield理解与用法实例分析
2019/01/11 PHP
由JavaScript技术实现的web小游戏(不含网游)
2010/06/12 Javascript
Ajax提交与传统表单提交的区别说明
2014/02/07 Javascript
JavaScript中window.showModalDialog()用法详解
2014/12/18 Javascript
情人节单身的我是如何在敲完代码之后收到12束玫瑰的(javascript)
2015/08/21 Javascript
JS实现密码框根据焦点的获取与失去控制文字的消失与显示效果
2015/11/26 Javascript
JS实现刷新父页面不弹出提示框的方法
2016/06/22 Javascript
浅谈Vue的基本应用
2016/12/27 Javascript
ES6学习教程之对象字面量详解
2017/10/09 Javascript
实现图片首尾平滑轮播(JS原生方法—节流)
2017/10/17 Javascript
使用Vue.js和Element-UI做一个简单登录页面的实例
2018/02/23 Javascript
生产制造追溯系统之在线打印功能
2019/06/03 Javascript
vue-quill-editor插入图片路径太长问题解决方法
2021/01/08 Vue.js
Python库urllib与urllib2主要区别分析
2014/07/13 Python
Python中logging模块的用法实例
2014/09/29 Python
python Pygame的具体使用讲解
2017/11/03 Python
rabbitmq(中间消息代理)在python中的使用详解
2017/12/14 Python
pyqt5 使用cv2 显示图片,摄像头的实例
2019/06/27 Python
完美解决pyinstaller打包报错找不到依赖pypiwin32或pywin32-ctypes的错误
2020/04/01 Python
jupyter notebook 实现matplotlib图动态刷新
2020/04/22 Python
通过自学python能找到工作吗
2020/06/21 Python
CSS改变网页中鼠标选中文字背景颜色例子
2014/04/23 HTML / CSS
《放小鸟》教学反思
2014/04/20 职场文书
幼儿教师暑期培训方案
2014/08/27 职场文书
迎新生欢迎词
2015/01/23 职场文书
求职信:求职应该注意的问题
2019/04/24 职场文书
高中议论文(范文2篇)
2019/08/19 职场文书
JS异步堆栈追踪之为什么await胜过Promise
2021/04/28 Javascript
VUE解决跨域问题Access to XMLHttpRequest at
2022/05/06 Vue.js