The vue mobile terminal teaches you to package a movable floating window and a movable fan-shaped floating button component

overview

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

feature design

  • The floating window moves freely in the visible area of ​​the page according to the finger touch
  • The edges of the floating window are tangent to the boundary of the visible area and cannot be moved out of the screen
  • The floating window style layout can be customized (button, strip style, etc.), the content can be customized (display content), and the initialization position can be customized
  • H5、小程序、APP三端通用

      


Technology Realization Analysis

   1. How to make the suspension moveable and prohibit it from moving out of the screen

     Answer: First, the floating frame uses a fixed layout. By monitoring the touchmove event, the current position coordinates (x, y) are obtained in real time and the top and left values ​​of the component are dynamically set, so that the component can move with the touch of the hand. Prohibiting moving out of the screen needs to combine the width and height of the component itself, the width and height of the screen, calculate the maximum movable distance, and limit the range of top and left values.

<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. Customize the floating window

     Answer: Define the width and height, the top and left values ​​of the starting position, and the layer (z-index) through the prop attribute of the component, and the settings can be passed in as needed. The content style (button style, square, etc.) retains the custom entry through the slot

	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. Multi-terminal compatibility

     Answer: The position of the fixed layout is different relative to the base point at different ends. The H5、APP基点是在屏幕左上角(导航栏上)而小程序是页面左上角(导航栏下面)。而不管哪端relative base point of the coordinates (clientX, clientY) obtained by touchmove is the upper left corner of the page, so in the settingsH5、APP端的top值时候需要在加上导航栏底部到屏幕顶部距离而这个距离我们可以通过uni.getSystemInfo返回的windowTop字段获取(小程序该值为0),所以动态设置top值时候可以=组件当前位置+windowTop

fixed layout, the effect of top=60rpx at different ends

           

	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;
				},
			});
		},

Technical Details - API Review

@touchstart event

Triggered when a finger starts touching an element

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

The options field is defined as follows:

field type illustrate
touches array Touch event, an array of touch point information currently on the screen
changedTouches array Touch event, an array of currently changing touch point information

The touches object is defined as follows:

field type illustrate
identifier Number Identifier of the touch point
pageX, pageY Number The distance from the upper left corner of the document, the upper left corner of the document is the origin, the horizontal axis is the X axis, and the vertical axis is the Y axis
clientX, clientY Number The distance from the upper left corner of the displayable area of ​​the page (the screen excludes the navigation bar), the horizontal axis is the X axis, and the vertical axis is the Y axis

@touchmove event

Triggered continuously when the finger slides on the screen

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

The options field is the same as touchstart.

Both events will bubble, and the cancellation of the bubble can be set by a modifier, for example: @touchmove.prevent.stop

Complete code implementation (floating window)

 suspenPopup.vue 

Component file:

<!-- 悬浮窗 -->
<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.view

Page call:


<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>

running result

Advanced packaging - movable fan-shaped expansion floating button

Realize the effect demonstration

 

demand analysis

It can be seen that it is a floating window in the unexpanded state, and we can customize the content based on the previously packaged floating window.

The whole component consists of 4 parts, the center button, the top button, the bottom button, and the left button. When they are not expanded, the 4 buttons overlap together and the center button is on the top.

When the center button is clicked, the center button does not move and only rotates at a certain angle. The other three buttons are similar to the launch effect. At the same time, they perform translational motions along different angles and rotate themselves. When the center button is clicked again to close, it is the reverse process of opening. .

The fan-shaped expansion effect is actually a simple animation special effect. Use the top button to analyze its animation. The movement trajectory is decomposed into moving up the y distance and moving the left x distance. between. To achieve this animation, we can easily think of the CSS attribute transition, and set top and left in conjunction with absolute or relative layout.

Code

<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>

Guess you like

Origin blog.csdn.net/sd1sd2/article/details/131270798