vue移动端手把手教你封装一个可移动悬浮窗、可移动打开扇形悬浮按钮组件

news2024/9/21 16:38:42

目录

概要

功能设计

技术细节-API回顾

@touchstart 事件

@touchmove事件

完整的代码实现(悬浮窗)

运行效果

进阶封装——可移动扇形展开悬浮按钮

实现效果演示

需求分析

代码实现


概要

悬浮窗、悬浮按钮是项目中常见的一种交互设计,特别是在移动端上有着广泛的使用,可以进行一些重要信息展示或者提供便捷的交互操作,比如手机网速内存信息显示的悬浮窗,联系客服悬浮按钮,录制视频的开始或结束悬浮按钮,快速到页面顶部或者底部悬浮按钮等。本文将手把手教你封装一个可移动的悬浮框组件,利用悬浮窗在进阶封装一个可展开可移动的扇形悬浮按钮组件。本案例将以移动端为背景采用uniapp技术实现一个三端(H5、小程序、APP)通用的悬浮组件。

功能设计

  • 悬浮窗在页面可视区域内根据手指触摸自由移动
  • 悬浮窗各边缘跟可视区域边界相切,不能移出屏幕外
  • 悬浮窗样式布局可自定义(按钮、长条风格等),内容可自定义(展示内容),初始化位置可自定义
  • H5、小程序、APP三端通用

      


技术实现分析

    1.如何实现悬浮可移动,并禁止移出屏幕

     答:首先悬浮框使用fixed布局,通过监听touchmove触摸移动事件实时获取当前位置坐标(x,y)动态设置组件的top和left值,组件就能跟着手触摸移动而移动。禁止移出屏幕需要结合组件本身宽高、屏幕宽高、计算出最大可移动距离,限制 top和left值范围。

<template>
	<view class="supenpopup"
		:style="{top:`${elTop}px`,left:`${elLeft}px`,width:`${width}rpx`,height:`${height}rpx`,zIndex}"
		@touchmove.prevent.stop="onTouchMove" @touchstart="onTouchStart">
      <!-- 插槽-自定义内容 -->
		<slot></slot>
	</view>
</template>
data() {
			return {
				elTop: 0, //组件距离顶部距离
				elLeft: 0, //组件距离左边距离
				windowHeight: 0, //窗口高度
				windowWidth: 0, //窗口宽度
				rate: 0, //px和rpx换算比例
				windowTop: 0, //窗口距离屏幕顶部距离
				startX: 0, //开始移动触摸点x坐标,相对页面左上角
				startY: 0, //开始移动点触摸点y坐标,,相对页面左上角
				startTop: 0, //悬浮框顶部距离顶部距离,小程序相对页面顶部,h5和app相对屏幕顶部
				startLeft: 0, //悬浮窗左边距离页面左边距离
			}
		},

	//开始移动
			onTouchStart(e) {
			   //记录开始时候触摸点坐标
				this.startX = e.touches[0].clientX;
				this.startY = e.touches[0].clientY
				
				//记录移动前组件位置
				this.startTop = this.elTop
				this.startLeft = this.elLeft
			},
			//移动中
			onTouchMove(e) {
				let x = e.touches[0].clientX;
				let y = e.touches[0].clientY;
				//忽略触摸屏幕最左边外面
				if (x < 0) {
					x = 0;
				}
                
				//当前组件距离左边位置=开始位置(x轴)+(当前触摸点x坐标-开始移动触摸点x坐标)
				let elLeft = this.startLeft + (x - this.startX);

				//悬浮窗右边限制移出屏幕外 this.rate单位换算比例,windowWidth单位px,width单位rpx
				//屏幕宽度-组件宽度=组件最大向左可移动距离(elLeft)
				//可移动范围elLeft值限制在0~(this.windowWidth - this.width / this.rate)范围内
				elLeft = Math.min(elLeft, this.windowWidth - this.width / this.rate)
				this.elLeft = elLeft > 0 ? elLeft : 0

				//忽略触摸屏幕最顶部外面
				if (y < 0) {
					y = 0;
				}
				let elTop = this.startTop + (y - this.startY);

				//悬浮窗限制移出屏幕底部
				//可移动范围elTop值限制在this.windowTop~(this.windowHeight - this.height / this.rate + this.windowTop)范围内
				elTop = Math.min(elTop, this.windowHeight - this.height / this.rate + this.windowTop)
				//悬浮窗限制移到导航栏上或移出屏幕顶部
				this.elTop = Math.max(elTop, this.windowTop)

			},

     

2.悬浮窗自定义

     答:通过组件的prop属性定义宽高、起始位置top和left值以及层级(z-index)可按需传入设置,内容样式(按钮风格、方形等)通过插槽保留自定义入口

	props: {
			//组件高,单位rpx
			height: {
				type: [String, Number],
				default: 100
			},
			//组件宽,单位rpx
			width: {
				type: [String, Number],
				default: 100
			},
			//起始位置距离顶部距离,单位rpx,auto:自动设置,将位于页面最顶端
			top: {
				type: [String, Number],
				default: 'auto'
			},
			//起始位置距离左边距离,单位rpx,auto:自动设置,将位于页面最左边
			left: {
				type: [String, Number],
				default: 'auto'
			},
			//层级
			zIndex: {
				type: Number,
				default: 999
			}
		},

     3.多端兼容性

     答:fixed布局在不同端相对基点位置是不一样的,H5、APP基点是在屏幕左上角(导航栏上)而小程序是页面左上角(导航栏下面)。而不管哪端touchmove获取到的坐标(clientX,clientY)相对基点是页面左上角,所以在设置H5、APP端的top值时候需要在加上导航栏底部到屏幕顶部距离而这个距离我们可以通过uni.getSystemInfo返回的windowTop字段获取(小程序该值为0),所以动态设置top值时候可以=组件当前位置+windowTop

fixed布局,top=60rpx在不同端的效果

           

	created() {
			uni.getSystemInfo({
				success: res => {
					this.windowWidth = res.windowWidth;//页面可视区域宽度
					this.rate = 750 / this.windowWidth;//rpx和px转换比例
					this.windowHeight = res.windowHeight;//页面可是区域高度
					this.windowTop = res.windowTop;//页面距离窗口顶部距离
					//设置初始位置,APP端和H5将基于屏幕最顶部定位,而小程序windowTop为0基于页面顶部
					this.elTop = this.top === 'auto' ? this.windowTop : this.top / this.rate;
					this.elLeft = this.left === 'auto' ? 0 : this.left / this.rate;
				},
			});
		},

技术细节-API回顾

@touchstart 事件

手指开始触摸元素触发

element.touchstart(options: Object): Promise<void>

options 字段定义如下:

字段类型说明
touchesarray触摸事件,当前停留在屏幕中的触摸点信息的数组
changedTouchesarray触摸事件,当前变化的触摸点信息的数组

touches对象定义如下:

字段类型说明
identifierNumber触摸点的标识符
pageX, pageYNumber距离文档左上角的距离,文档的左上角为原点 ,横向为X轴,纵向为Y轴
clientX, clientYNumber距离页面可显示区域(屏幕除去导航条)左上角距离,横向为X轴,纵向为Y轴

@touchmove事件

手指在屏幕上滑动时候连续触发

element.touchmove(options: Object): Promise<void>

options 字段同 touchstart。

两个事件都会冒泡,取消冒泡可通过修饰符设置例如:@touchmove.prevent.stop

完整的代码实现(悬浮窗)

 suspenPopup.vue 

组件文件:

<!-- 悬浮窗 -->
<template>
	<view class="supenpopup"
		:style="{top:`${elTop}px`,left:`${elLeft}px`,width:`${width}rpx`,height:`${height}rpx`,zIndex}"
		@touchmove.prevent.stop="onTouchMove" @touchstart="onTouchStart">
		<!-- 插槽-自定义内容 -->
		<slot></slot>
	</view>
</template>

<script>
	export default {
		props: {
			//组件高,单位rpx
			height: {
				type: [String, Number],
				default: 100
			},
			//组件宽,单位rpx
			width: {
				type: [String, Number],
				default: 100
			},
			//起始位置距离顶部距离,单位rpx,auto:自动设置,将位于页面最顶端
			top: {
				type: [String, Number],
				default: 'auto'
			},
			//起始位置距离左边距离,单位rpx,auto:自动设置,将位于页面最左边
			left: {
				type: [String, Number],
				default: 'auto'
			},
			//层级
			zIndex: {
				type: Number,
				default: 999
			}
		},

		data() {
			return {
				elTop: 0, //组件距离顶部距离
				elLeft: 0, //组件距离左边距离
				windowHeight: 0, //窗口高度
				windowWidth: 0, //窗口宽度
				rate: 0, //px和rpx换算比例
				windowTop: 0, //窗口距离屏幕顶部距离
				startX: 0, //开始移动触摸点x坐标,相对页面左上角
				startY: 0, //开始移动点触摸点y坐标,,相对页面左上角
				startTop: 0, //悬浮框顶部距离顶部距离,小程序相对页面顶部,h5和app相对屏幕顶部
				startLeft: 0, //悬浮窗左边距离页面左边距离
			}
		},
		created() {
			uni.getSystemInfo({
				success: res => {
					this.windowWidth = res.windowWidth;//页面可视区域宽度
					this.rate = 750 / this.windowWidth;//rpx和px转换比例
					this.windowHeight = res.windowHeight;//页面可是区域高度
					this.windowTop = res.windowTop;//页面距离窗口顶部距离
					//设置初始位置,APP端和H5将基于屏幕最顶部定位,而小程序windowTop为0基于页面顶部
					this.elTop = this.top === 'auto' ? this.windowTop : this.top / this.rate;
					this.elLeft = this.left === 'auto' ? 0 : this.left / this.rate;
				},
			});
		},
		methods: {
			//开始移动
			onTouchStart(e) {
			   //记录开始时候触摸点坐标
				this.startX = e.touches[0].clientX;
				this.startY = e.touches[0].clientY
				
				//记录移动前组件位置
				this.startTop = this.elTop
				this.startLeft = this.elLeft
			},
			//移动中
			onTouchMove(e) {
				let x = e.touches[0].clientX;
				let y = e.touches[0].clientY;
				//忽略触摸屏幕最左边外面
				if (x < 0) {
					x = 0;
				}
                
				//当前组件距离左边位置=开始位置(x轴)+(当前触摸点x坐标-开始移动触摸点x坐标)
				let elLeft = this.startLeft + (x - this.startX);

				//悬浮窗右边限制移出屏幕外 this.rate单位换算比例,windowWidth单位px,width单位rpx
				//屏幕宽度-组件宽度=组件最大向左可移动距离(elLeft)
				//可移动范围elLeft值限制在0~(this.windowWidth - this.width / this.rate)范围内
				elLeft = Math.min(elLeft, this.windowWidth - this.width / this.rate)
				this.elLeft = elLeft > 0 ? elLeft : 0

				//忽略触摸屏幕最顶部外面
				if (y < 0) {
					y = 0;
				}
				let elTop = this.startTop + (y - this.startY);

				//悬浮窗限制移出屏幕底部
				//可移动范围elTop值限制在this.windowTop~(this.windowHeight - this.height / this.rate + this.windowTop)范围内
				elTop = Math.min(elTop, this.windowHeight - this.height / this.rate + this.windowTop)
				//悬浮窗限制移到导航栏上或移出屏幕顶部
				this.elTop = Math.max(elTop, this.windowTop)

			},


		},

	}
</script>

<style lang="scss" scoped>
	.supenpopup {
		position: fixed;
		z-index: 999;
	}
</style>

index.vue

页面调用:


<template>
	<view>
         <!-- 悬浮窗 -->
		<suspenPopup top="auto" left="40" height="150" width="650">
			<view class="supen-popup">我是可移动的悬浮窗</view>
		</suspenPopup>
	</view>
</template>

<script>
	import suspenPopup from './component/suspenPopup.vue'
	export default {
		components: {
			suspenPopup,
		},


	}
</script>
<style lang="scss" scoped>
	.supen-popup {
		display: flex;
		justify-content: center;
		align-items: center;
		border-radius: 20rpx;
		box-sizing: border-box;
		background: white;
		box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
		border: 1px solid #aaa;
		font-size: 24rpx;
		width: 100%;
		height: 100%;
	}
</style>

运行效果

进阶封装——可移动扇形展开悬浮按钮

实现效果演示

 

需求分析

可以看到在未展开状态下就是一个悬浮窗,我们可以在之前封装好的悬浮窗基础上进行内容自定义。

整个组件一共由4个部分组成,中心按钮、顶部按钮、底部按钮、左边按钮,当未展开时候4个按钮重叠在一起中心按钮在最上面,

当点击中心按钮时候中心按钮不移动动只旋转一定角度,其他三个按钮类似发射效果,同时分别沿着不同角度做平移运动并且自身也在旋转,而再次点击中心按钮关闭时候跟打开是个逆过程。

扇形展开效果实际上就是一个简单的动画特效,以顶部按钮分析它的动画,移动轨迹分解为往上移动y距离,往左移动x距离,整个过程发射角度是不变的大约30-45度之间。实现这个动画我们很容易想到css属性transition,在配合绝对或者相对布局设置top和left即可

代码实现

<template>
	<suspenPopup >
		<!-- top按钮 -->
		<view :class="['button','top',btnClass]">
			<image class="icon" src="/static/top.png" mode="widthFix"></image>
		</view>
		<!-- bottom按钮 -->
		<view :class="['button','bottom',btnClass]">
			<image  class="icon" src="/static/bottom.png" mode="widthFix"></image>
		</view>
		<!-- left按钮 -->
		<view :class="['button ','left',btnClass]">
			<image  class="icon" src="/static/left.png" mode="widthFix"></image>
		</view>
		<!-- 中心按钮 -->
		<view :class="['button','center',btnClass]" @click="handleOpen">
			<image  class="icon" src="/static/center.png" mode="widthFix"></image>
		</view>
	</suspenPopup>
</template>

<script>
	import suspenPopup from './suspenPopup.vue' //悬浮窗组件
	export default {
		components: {
			suspenPopup
		},
		data() {
			return {
				isOpen: null, //是否打开
			}
		},
		computed: {
			//中心按钮class
			btnClass() {
				return this.isOpen === true ? 'open' : this.isOpen === false ? 'close' : null
			}
		},
		methods: {
			handleOpen() {
				this.isOpen = !this.isOpen
			}

		}
	}
</script>

<style lang="scss" scoped>
	.button {
		border-radius: 50%;
		position: absolute;
		left: 0;
		top: 0;
		height: 80rpx;
		width: 80rpx;
		background: rgb(235, 155, 50);
		display: flex;
		flex-direction: column;
		justify-content: center;
		align-items: center;
		z-index: 99999;
		overflow: hidden;
		transition: all 0.5s ease;
		font-size: 24rpx;
		.icon{
			width: 100%;
		}

		/**中心按钮**/
		&.center {

			&.open {
				transform: rotate(315deg);

			}

			&.close {
				transform: rotate(0deg);

			}
		}

		/**顶部按钮**/
		&.top {
			opacity: 0;

			&.open {
				top: -100rpx;
				left: -120rpx;
				transform: rotate(360deg);
				opacity: 1
			}

			&.close {
				top: 0rpx;
				left: 0rpx;
				transform: rotate(360deg);
				opacity: 0
			}
		}

		/**底部按钮**/
		&.bottom {
			opacity: 0;

			&.open {
				top: 100rpx;
				left: -120rpx;
				transform: rotate(360deg);
				opacity: 1
			}

			&.close {
				top: 0rpx;
				left: 0rpx;
				transform: rotate(360deg);
				opacity: 0
			}
		}

		/**左边按钮**/
		&.left {
			opacity: 0;

			&.open {
				top: 0rpx;
				left: -180rpx;
				transform: rotate(360deg);
				opacity: 1
			}

			&.close {
				top: 0rpx;
				left: 0rpx;
				transform: rotate(360deg);
				opacity: 0
			}
		}
	}
</style>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/659708.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux国产操作系统,UCA-系统工程师学习必备技能,文件管理和查找、内容查找、归档的再学习

复习和巩固Linux的基础操作&#xff0c;对文件管理和查找、内容查找、归档以及管道和输入输出重定向进行再学习。 目录 1.文件管理 1. 1.head命令 1.2. tail命令 1.3. more/less命令 1.4. wc 统计命令 1.5. sort 排序命令 1.6. uniq 去重命令 1.7. paste合并命令 2.文…

基于matlab对现代相控阵系统中常用的子阵列进行建模分析(附源码)

一、前言 本示例说明如何使用相控阵系统工具箱对现代相控阵系统中常用的子阵列进行建模并进行分析。 相控阵天线与传统碟形天线相比具有许多优势。相控阵天线的元件更容易制造;整个系统受组件故障的影响较小;最重要的是&#xff0c;可以向不同方向进行电子扫描。 但是&#xff…

耗时半个月,终于把十几个大厂的python面试题整理成了PDF合集(基础+高级+web+数据库+爬虫)

大家好&#xff0c;最近有不少小伙伴在后台留言&#xff0c;近期的面试越来越难了&#xff0c;要背的越来越多了&#xff0c;考察得越来越细&#xff0c;明摆着就是想让我们徒手造航母嘛&#xff01;实在是太为难我们这些程序员了。 这不&#xff0c;为了帮大家节约时间&#…

JVM垃圾回收与双亲委派模型

观前提示:本篇博客演示使用的 IDEA 版本为2021.3.3版本,使用的是Java8(又名jdk1.8) 前端使用VSCode(Visual Studio Code1.78.2) 电脑使用的操作系统版本为 Windows 10 目录 1. 什么是 JVM 2. jvm 发展史 2.1 Sun Classic VM 2.2 Exact VM 2.3 HotSpot VM 2.4 JRockit …

Docker部署——将jar包打成docker镜像并启动容器

在代码编写完成即将部署的时候&#xff0c;如果采用docker容器的方法&#xff0c;需要将jar包打成docker镜像并通过镜像将容器启动起来。具体的步骤如下。 一、首先下载java镜像 先使用docker search java命令进行搜索。 然而在拉取镜像的时候要注意不能直接去选择pull java ,…

在线DDL操作踩坑记录

官方地址&#xff1a;GitHub - github/gh-ost: GitHubs Online Schema-migration Tool for MySQL 使用ghost方式在线对mysql表进行ddl ghost原理&#xff1a; 要对表A进行DDL&#xff0c;在主库建立一个ghost表 A1在表A1上进行alter操作伪装成一个mysql的从库&#xff0c;监…

Java集合框架:队列、Queue和Deque详解

目录 一、普通队列 1. 概念 2. Queue&#xff08;Java集合框架的接口&#xff09; 3. Queue中的方法 4. 方法使用演示 5. 队列的模拟实现 6. 顺序普通队列的缺点&#xff1a; 二、循环队列 1. 循环队列也是一种数据结构。基于上述队列的缺点&#xff0c;此时就有了循环…

为什么我不建议你入行网络安全,因为99.9%的人都绕不过这三个坎

前言 我一个朋友老赵&#xff0c;老赵在一家大型互联网公司做高级网络安全工程师&#xff0c;从实习生到工程师整整呆了六年。去年他们公司为了缩减成本&#xff0c;做了裁员&#xff0c;他也在其中&#xff0c;取而代之的是一个只有三年工作经验的 “新人” … 老赵想着&…

Windows10下安装Oracle19c提示“无法将 **\** 安装用户添加到 ** 组“解决办法

问题描述 操作系统&#xff1a;window10 数据库版本&#xff1a;Oracle19c 本机在安装Oracle19c提示无法将 ZHOUQUAN\zhouquan 安装用户添加到 %2% 组。 问题原因 根据安装的对话框中的日志&#xff0c;找到并打开 日志报错信息&#xff1a; 信息: WindowsSecurityExcep…

时序预测 | Matlab实现INFO-ELM向量加权算法优化极限学习机时间序列预测

时序预测 | Matlab实现INFO-ELM向量加权算法优化极限学习机时间序列预测 目录 时序预测 | Matlab实现INFO-ELM向量加权算法优化极限学习机时间序列预测效果一览基本介绍程序设计学习总结参考资料 效果一览 基本介绍 Matlab实现INFO-ELM向量加权算法优化极限学习机时间序列预测 …

skywalking 源码

源码核心是SkyWalkingAgent 找到一堆插件&#xff0c;来对符合条件的类来代理 通过AbstractClassEnhancePluginDefine.define方法来。 如果有很多版本的插件&#xff0c;spring有2.0版本,3.0版本,4.0版。 具体使用哪个版本&#xff0c;看被增加的类使用的是哪个版本的spring …

vue基础--计算商品的总价格

计算商品的总价格&#xff1a; 1、在 父组件中 通过计算属性 动态把总价格计算出来&#xff0c; 2、通过 父向子传值&#xff0c;通过自定义属性&#xff0c;把值传给 子组件 父组件&#xff1a; 1、使用计算属性computed 计算总价格&#xff1a; 1.1、先用filter 过滤出 数…

Unity UGUI5——图集

一、Drawcall ​ 字面理解 DrawCall&#xff0c;就是绘制呼叫的意思&#xff0c;表示 CPU&#xff08;中央处理器&#xff09;通知 GPU&#xff08;图形处理器-显卡&#xff09; &#xff08;一&#xff09;DrawCall 概念 就是 CPU &#xff08;处理器&#xff09;准备好渲染…

基于Web的停车场管理系统(Java)

目录 一、系统介绍 1.开发的环境 2.本系统实现的功能 3.数据库用到的表 4.工程截图 二、系统展示 1、登录页面 2、首页 3、系统信息管理模块 4、车位信息管理模块 5、IC卡信息管理模块 ​编辑6、固定车主停车管理模块 7、临时车主停车管理模块 8、系统功能操作模块 …

unity3d:小地图UV,UGUIshader毒圈挖孔,缩圈

运行效果 场景中缩圈 小地图中挖孔 大地图中挖孔 小地图 方案1使用Mask 给了一个方形的mask组件&#xff0c;然后根据玩家位置计算出地图左下角的位置进行移动。这种实现方式虽然简单&#xff0c;但是会有两个问题&#xff1a; 1.Overdraw特别大&#xff0c;几乎很多时候会有…

【LLMs 入门实战 】Vicuna 模型学习与实战

UC伯克利学者联手CMU、斯坦福等&#xff0c;再次推出一个全新模型70亿/130亿参数的Vicuna&#xff0c;俗称「小羊驼」&#xff0c;小羊驼号称能达到GPT-4的90%性能。 欢迎使用小羊驼&#x1f999;环境搭建权重下载下载 Vicuna Weight下载 LLAMA Weight构建真正的 working weigh…

Lattice Planner从入门到放弃

Lattice Planner相关背景和更正式的公式推导可以直接参考其原始论文《Optimal Trajectory Generation for Dynamic Street Scenarios in a Frent Frame》&#xff08;ICRA 2010&#xff09;&#xff0c;本文侧重于Lattic planner理论和代码的结合。 1. Lattice Planner基本流程…

2023年6月GESP能力等级认证C++一级真题

2023-06 GESP一级真题 题数&#xff1a;27 分数&#xff1a;100 测试时长&#xff1a;60min 一、选择题(每题 2 分&#xff0c;共 30 分) 1.以下不属于计算机输入设备的有 (B ) 。&#xff08;2分&#xff09; A、键盘 B、音箱 C、鼠标 D、传感器 答案解析&#xff1…

如果你正在做AI测试,那么这十点你必须注意

AI是一个已经进入人类日常生活的新技术时代&#xff0c;例如Siri&#xff0c;Alexa语音接口等。通过大数据和数据科学实现数据存储的进步&#xff0c;使用户能够进行快速分析和数据检索。机器学习是一个新领域&#xff0c;机 AI是一个已经进入人类日常生活的新技术时代&#x…

今天我们来说说MySQL的缓存机制

原文链接&#xff1a;http://www.ibearzmblog.com/#/technology/info?id5770c555acd4302f81d86976c06e2319 前言 当我们向数据库服务器发送一条SQL的时候&#xff0c;但数据库收到后就会执行&#xff0c;但是如果在短时间内都执行同一条SQL&#xff0c;如果每次数据库都会执…