vue 实现微信浮标效果
微信的浮窗,大伙应该都用过,当我们正在阅读一篇公众号文章时,突然需要处理微信消息,点击浮窗,在微信上会有个浮标,点击浮标可以回到文章。
我们今天打算撸一个类似微信的浮标组件,我们期望组件有以下功能
- 支持拖拽
- 支持左右吸附
- 支持页面上下滑动时隐藏
效果预览
拖拽事件
浮标的核心功能的就是拖拽,对鼠标或移动端的触摸的事件来说,有三个阶段,鼠标或手指接触到元素时,鼠标或手指在移动的过程,鼠标或手指离开元素。这个三个阶段对应的事件名称如下
mouse: { start: 'mousedown', move: 'mousemove', s: 'mouseup' }, touch: { start: 'touchstart', move: 'touchmove', s: 'touchend' }
元素定位
滑动容器我们采用绝对定位,通过设置 和 left 属性来改变元素的位置,那我们怎么获取到新的 和 left 呢?
我们先看狼蚁网站SEO优化这张图
黄色区域是拖拽的元素,蓝色的点就是鼠标或手指触摸的位置,在元素移动的过程中,这些值也会随着发生改变,那么我们只要计算出新的触摸位置和最初触摸位置的横坐标和竖坐标的变化,就可以算出移动后的 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) ]; }
拖拽的前中后
有了上面的基础,我们分析下拖拽的三个阶段我们需要做哪些工作
- 触摸元素,即开始拖拽,将当前元素的 left 和触摸点的 pageX pageY 用对象存储起来,然后监听移动和结束事件
- 元素拖拽过程,计算当前的 pageX pageY 与 初始的 pageX pageY 的差值,算出当前的 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', s: 'mouseup' }, touch: { start: 'touchstart', move: 'touchmove', s: '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 : number = 0; // 元素的 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, : 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. = 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. = this.; 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.s, 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. - tmpDeltaY; this.rawLeft = mouseClickPosition.left - tmpDeltaX; this.$emit('dragging', this.left, this.); } / 监听滑动结束 / private handleUp(e: TouchEvent | MouseEvent) { this.rawTop = this.; this.rawLeft = this.left; if (this.dragging) { this.dragging = false; this.$emit('drags', this.left, this.); } // 左右吸附 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, : 0, }; } / 元素位置 / private get style() { return { position: 'absolute', : this. + '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. = newTop; return; } const left = this.left; const = this.; if (bounds.minTop !== null && newTop < bounds.minTop) { newTop = bounds.minTop; } else if (bounds.maxTop !== null && bounds.maxTop < newTop) { newTop = bounds.maxTop; } this. = newTop; } @Watch('rawLeft') private rawLeftChange(newLeft: number) { const bounds = this.bounds; if (bounds.maxTop === 0) { this.left = newLeft; return; } const left = this.left; const = this.; 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: .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 实现微信浮标效果,希望对大家有所帮助,如果大家有任何疑问欢迎给我留言,长沙网络推广会及时回复大家的!
编程语言
- 如何快速学会编程 如何快速学会ug编程
- 免费学编程的app 推荐12个免费学编程的好网站
- 电脑怎么编程:电脑怎么编程网咯游戏菜单图标
- 如何写代码新手教学 如何写代码新手教学手机
- 基础编程入门教程视频 基础编程入门教程视频华
- 编程演示:编程演示浦丰投针过程
- 乐高编程加盟 乐高积木编程加盟
- 跟我学plc编程 plc编程自学入门视频教程
- ug编程成航林总 ug编程实战视频
- 孩子学编程的好处和坏处
- 初学者学编程该从哪里开始 新手学编程从哪里入
- 慢走丝编程 慢走丝编程难学吗
- 国内十强少儿编程机构 中国少儿编程机构十强有
- 成人计算机速成培训班 成人计算机速成培训班办
- 孩子学编程网上课程哪家好 儿童学编程比较好的
- 代码编程教学入门软件 代码编程教程