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 相关文章推荐
浅谈tudou土豆网首页图片延迟加载的效果
Jun 23 Javascript
javascript动态的改变IFrame的高度实现自动伸展
Oct 12 Javascript
关于页面嵌入swf覆盖div层的问题的解决方法
Feb 11 Javascript
通用无限极下拉菜单的实现代码
May 31 Javascript
工作中比较实用的JavaScript验证和数据处理的干货(经典)
Aug 03 Javascript
jquery popupDialog 使用 加载jsp页面的方法
Oct 25 Javascript
JS中的phototype详解
Feb 04 Javascript
详解webpack2+React 实例demo
Sep 11 Javascript
Angular5集成eventbus的示例代码
Jul 19 Javascript
vue ssr服务端渲染(小白解惑)
Nov 10 Javascript
微信小程序实现搜索功能
Mar 10 Javascript
javascript全局自定义鼠标右键菜单
Dec 08 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
桌面中心(二)数据库写入
2006/10/09 PHP
用php实现的获取网页中的图片并保存到本地的代码
2010/01/05 PHP
微信公众平台开发关注及取消关注事件的方法
2014/12/23 PHP
使用PHP处理数据库数据如何将数据返回客户端并显示当前状态
2016/02/16 PHP
详解yii2使用多个数据库的案例
2017/06/16 PHP
PHP实现动态删除XML数据的方法示例
2018/03/30 PHP
PHP获取远程http或ftp文件的md5值的方法
2019/04/15 PHP
js css样式操作代码(批量操作)
2009/10/09 Javascript
JQuery扩展插件Validate—6 radio、checkbox、select的验证
2011/09/05 Javascript
JavaScript获取表单内所有元素值的方法
2015/04/02 Javascript
JavaScript实现向setTimeout执行代码传递参数的方法
2015/04/16 Javascript
Javascript基础之数组的使用
2016/05/13 Javascript
JQuery 进入页面默认给已赋值的复选框打钩
2017/03/23 jQuery
详解vue过滤器在v2.0版本用法
2017/06/01 Javascript
基于vue的短信验证码倒计时demo
2017/09/13 Javascript
vue不操作dom实现图片轮播的示例代码
2019/12/18 Javascript
[02:27]DOTA2英雄基础教程 莱恩
2014/01/17 DOTA
[01:09]DOTA2次级职业联赛 - 99战队宣传片
2014/12/01 DOTA
python字符串,数值计算
2016/10/05 Python
python用fsolve、leastsq对非线性方程组求解
2018/12/15 Python
VSCode Python开发环境配置的详细步骤
2019/02/22 Python
python基于pdfminer库提取pdf文字代码实例
2019/08/15 Python
Python3 合并二叉树的实现
2019/09/30 Python
pycharm中导入模块错误时提示Try to run this command from the system terminal
2020/03/26 Python
如何基于Django实现上下文章跳转
2020/09/16 Python
英国顶级水晶珠宝零售商之一:Tresor Paris
2019/04/27 全球购物
如何通过 CSS 写出火焰效果
2021/03/24 HTML / CSS
数控技术与应用毕业生自荐信
2013/09/24 职场文书
护理专业自我鉴定
2014/01/30 职场文书
祖国在我心中演讲稿200字
2014/08/28 职场文书
护士年终工作总结不会写?各科护士模板总结
2020/01/02 职场文书
Canvas跟随鼠标炫彩小球的实现
2021/04/11 Javascript
Python3 类型标注支持操作
2021/06/02 Python
PyQt5结合QtDesigner实现文本框读写操作
2021/06/11 Python
JavaScript组合继承详解
2021/11/07 Javascript
MySQL数据管理操作示例讲解
2022/12/24 MySQL