飞道的博客

vue 弹窗 惯性滚动 加速滚动

352人阅读  评论(0)

惯性滚动组件

新建文件 components/scroll-viwe

<template>
	<div v-if="visiable">
		<div class="mapbox-result-scroll-hidden">
			<div class="mapbox-result-wrap" ref="resultWrap">
				<div class="mapbox-result-body" id="resultBody" ref="resultBody" @touchstart="touchStart" @touchmove="touchMove" @touchend="touchEnd" :style="resultBodyStyle">
					<div v-html="thmltext"></div>
				</div>
			</div>
		</div>
	</div>
</template>
<script>
let offset = 50; // 最大溢出值
let cur = 0; // 列表滑动位置
let isDown = false;
let vy = 0; // 滑动的力度
let fl = 150; // 弹力,值越大,到底或到顶后,可以继续拉的越远
let isInTransition = false; // 是否在滚动中

export default {
     
	name: 'scroll-viwe',
	props: {
     
		thmltext: {
     
			type: String,
			default: ``
		},
		closable: {
     
			type: Boolean,
			default: true
		}
	},
	data() {
     
		return {
     
			resultPanelStatus: 'normal', //'normal'、'top'
			startY: 0, // 开始触摸屏幕的点,会变动,用于滑动计算
			sY: 0, // 开始触摸屏幕点,不会变动,用于判断是否是点击还是滑动
			endY: 0, // 离开屏幕的点
			moveY: 0, // 滑动时的距离
			disY: 0, // 移动距离
			slideEffect: '', // 滑动效果
			timer: null,
			resultBodyStyle: '',
			top: false,
			startTime: '', // 初始点击时的时间戳
			oh: 0, // 列表的高度
			ch: 0 // 容器的高度
		};
	},
	mounted() {
     
		// this.$refs.resultWrap.style.height = `${this.defaultHeight - 50}px`;
	},
	computed: {
     
		visiable() {
     
			this.resultPanelStatus = 'normal';
			this.slideEffect = `transform: translateY(-${
       this.defaultHeight}px); transition: all .3s`;
			return true;
		}
	},
	methods: {
     
		/**
		 * 根据手指来滚动,会触发click延时300ms的问题,导致关闭结果列表面板时,立即点击另一个poi结果列表,导致此时的scroll滑动到上一次的位置,且滑动时也会移到上一次滑动停止的位置
		 */
		touchStart(ev) {
     
			ev = ev || event;
			if (isInTransition || ev.targetTouches.length > 1) return;
			// ev.preventDefault();
			this.startY = ev.targetTouches[0].clientY; // 点击的初始位置
			this.sY = ev.targetTouches[0].clientY; // 点击的初始位置, 点击时使用
			clearInterval(this.timer); // 清除定时器
			vy = 0;
			this.disY = ev.targetTouches[0].clientY - cur; // 计算点击位置与列表当前位置的差值,列表位置初始值为0
			this.startTime = ev.timeStamp;
			/**
			 * overflow:hidden 导致scrollHeight和clientHeight 相等         解决:把容器高度写死, 结果列表大于3,则为204,否则内容高度即为容器高度
			 */
			this.oh = this.$refs.resultWrap.scrollHeight; // 内容的高度
			this.ch = 400; // 容器的高度
			// console.log("this.$refs.resultWrap.style: ", this.$refs.resultWrap.style);
			isDown = true;
		},

		touchMove(ev) {
     
			ev = ev || event;
			if (ev.targetTouches.length > 1) return;
			if (Math.abs(ev.targetTouches[0].clientY - this.sY) < 5) return;
			if (isDown) {
     
				if (ev.timeStamp - this.startTime > 40) {
     
					// 如果是慢速滑动,则不会产生力度,内容是根据手指一动的
					this.startTime = ev.timeStamp; // 慢速滑动不会产生力度,所以需要实时更新时间戳
					cur = ev.targetTouches[0].clientY - this.disY; // 内容位置应为手指当前位置减去手指点击时与内容位置的差值
					if (cur > 0) {
     
						// 如果内容位置大于0, 即手指向下滑动并到顶时
						cur *= fl / (fl + cur); // 弹力模拟公式: 位置 *= 弹力 / (弹力 + 位置)
					} else if (cur < this.ch - this.oh) {
     
						// 如果内容位置小于容器高度减内容高度(因为需要负数,所以反过来减),即向上滑动到最底部时
						// 当列表滑动到最底部时,curPos的值其实是等于容器高度减列表高度的,假设窗口高度为10,列表为30,此时curPos为-20,但这里判断是小于,所以当curPos < -20时才会触发
						cur += this.oh - this.ch; // 列表位置加等于列表高度减容器高度(这是与上面不同,这里是正减,得到一个正数),这里curPos为负数,加上一个正数,这里curPos为负数,加上一个正数,延用上面的假设,此时 cur = -21 + (30-10=20) = -1 ,所以这里算的是溢出数
						cur = (cur * fl) / (fl - cur) - this.oh + this.ch; // 然后给溢出数带入弹力,延用上面的假设,这里为   cur = -1 * 150 /(150 - -1 = 151)~= -0.99 再减去 30  等于 -30.99  再加上容器高度 -30.99+10=-20.99  ,这也是公式,要死记。。
					}
					this.setPos(cur);
				}
				vy = ev.targetTouches[0].clientY - this.startY; // 记录本次移动后,与前一次手指位置的滑动的距离,快速滑动时才有效,慢速滑动时差值为 1 或 0,vy可以理解为滑动的力度
				this.startY = ev.targetTouches[0].clientY; // 更新前一次位置为现在的位置,以备下一次比较
			}
			// let maxHeight = this.total < 3 ? 0 : (this.$refs.resultBody.offsetHeight - this.defaultHeight);
		},

		touchEnd(ev) {
     
			ev = ev || event;
			if (ev.changedTouches.length > 1) return;
			if (Math.abs(ev.changedTouches[0].clientY - this.sY) < 5) return;
			this.mleave(ev);
		},

		setPos(y) {
     
			// 列表y轴位置,移动列表
			this.resultBodyStyle = `transform: translateY(${
       y}px) translateZ(0);`;
		},

		ease(target) {
     
			isInTransition = true;
			let that = this;
			this.timer = setInterval(function() {
     
				// 回弹算法为 当前位置 减 目标位置 取2个百分点 递减
				cur -= (cur - target) * 0.2;
				if (Math.abs(cur - target) < 1) {
     
					// 减到当前位置与目标位置相差小于1之后直接归位
					cur = target;
					clearInterval(that.timer);
					isInTransition = false;
				}
				that.setPos(cur);
			}, 20);
		},

		mleave(ev) {
     
			if (isDown) {
     
				isDown = false;
				let friction = ((vy >> 31) * 2 + 1) * 0.5, // 根据力度套用公式计算出惯性大小
					that = this,
					_oh = this.$refs.resultWrap.scrollHeight - this.$refs.resultWrap.clientHeight;
				// _oh = this.$refs.resultWrap.scrollHeight - (this.total > 3 ? 204 : this.$refs.resultWrap.clientHeight);
				this.timer = setInterval(function() {
     
					vy -= friction; // 力度按惯性大小递减
					cur += vy; // 转换为额外的滑动距离
					that.setPos(cur); // 滑动列表

					if (-cur - _oh > offset) {
     
						// 如果列表底部超出
						clearInterval(that.timer);
						that.ease(-_oh); // 回弹
						return;
					}
					if (cur > offset) {
     
						// 如果列表顶部超出
						clearInterval(that.timer);
						that.ease(0); // 回弹
						return;
					}
					if (Math.abs(vy) < 1) {
     
						// 如果力度减小到小于1了,再做超出回弹
						clearInterval(that.timer);
						if (cur > 0) {
     
							that.ease(0);
							return;
						}
						if (-cur > _oh) {
     
							that.ease(-_oh);
							return;
						}
					}
				}, 20);
			}
		},

		normal() {
     
			this.slideEffect = `transform: translateY(${
       -this.defaultHeight}px); transition: all .5s;`;
			this.resultPanelStatus = 'normal';
		},

		clickItem(_index) {
     
			let len = this.$refs.resultBody.children.length;
			for (let i = 0; i < len; i++) {
     
				if (i === _index) {
     
					this.$refs.resultBody.children[i].style.background = '#F0F0F0';
				} else {
     
					this.$refs.resultBody.children[i].style.background = 'white';
				}
			}
		},

		close(ev) {
     
			// click事件会和touchestart事件冲突
			this.normal();
			this.resultBodyStyle = 'transform: translateY(0) translateZ(0);';
			cur = 0;
			// this.$store.state.resultPanel.show = false;
			this.$emit('on-cancel');
		}
	}
};
</script>

<style type="text/less" scoped>

.mapbox-result-scroll-hidden {
     
	overflow: hidden;
	width: 100%;
	margin-top: 30px;
	padding: 0 20px;
	font-size: 22px;
	font-family: Source Han Sans CN;
	font-weight: 400;
	color: #666666;
	line-height: 34px;
}

.mapbox-result-wrap {
     
	/*-webkit-overflow-scrolling: touch;*/
	/*overflow-scrolling: touch;*/
	position: relative;
	overflow-y: hidden; /* 设置overflow-y为hidden,以避免原生的scroll影响根据手势滑动计算滚动距离 */
	height: 500px;
	background: transparent;
	width: calc(100% + 17px);
	/*解决安卓滑动页面时出现空白*/
	-webkit-backface-visibility: hidden;
	-webkit-transform: translate3d(0, 0, 0);
}

.mapbox-result-wrap::-webkit-scrollbar {
     
	display: none;
}

.mapbox-result-body {
     
	/*position: absolute;*/
	/*transform: translateY(0);*/
	/*transition: transform .5s;*/
	width: 100%;
}
</style>

弹窗基础组件

新建文件 components/zwy-popup

<template>
	<div v-show="ishide" @touchmove.stop.prevent>
		<!-- 遮罩 -->
		<div class="mask" :style="maskStyle"></div>
		<!-- 内容 -->
		<div class="tip" :style="tipStyle"><slot></slot></div>
	</div>
</template>

<script>
export default {
     
	props: {
     
		// 控制弹窗显隐
		ishide: {
     
			type: Boolean,
			required: true
		},
		// 设置弹窗层级
		zindex: {
     
			type: Number,
			default: 99
		},
		// 设置遮罩透明度
		opacity: {
     
			type: Number,
			default: 0.6
		},
		// 设置内容区宽度
		width: {
     
			type: String,
			default: '100%'
		},
		// 设置内容区高度
		height: {
     
			type: String,
			default: '400px'
		},
		// 设置内容区圆角
		radius: {
     
			type: String
		},
		// 设置内容区底色
		bgcolor: {
     
			type: String,
			default: 'transparent'
		}
	},
	computed: {
     
		// 遮罩样式
		maskStyle() {
     
			return `
					z-index:${
       this.zindex};
					background:rgba(0,0,0,${
       this.opacity});
				`;
		},
		// 内容样式
		tipStyle() {
     
			return `
					width:${
       this.width};
					min-height:${
       this.height};
					z-index:${
       this.zindex + 1};
					border-radius:${
       this.radius};
					background-color:${
       this.bgcolor};
				`;
				
		}
	}
};
</script>

<style scoped>
.mask {
     
	width: 100%;
	height: 100vh;
	background: rgba(0, 0, 0, 0.2);
	position: fixed;
	left: 0;
	top: 0;
	z-index: 100000;
	display: block;
}

.tip {
     
	position: fixed;
	left: 50%;
	top: 50%;
	transform: translate(-50%, -50%);
	 display: flex;
	 flex-direction: column;
	 justify-content: center;
	 align-items: center;
}
</style>

弹窗业务组件

<template>
	<div class="exothecium">
		<zwyPopup :ishide="ishide">
			<div class="pup-box" >
				<div class="close-btn" @click="closeClick"></div>
				<div class="pup-tc" ></div>
				<div class="pup-box-nir" style="padding-top: 26px;">
					<p class="benefits-title">活动规则</p>
					<scrollViwe :thmltext="info.actRules"></scrollViwe>
				</div>
			</div>
		</zwyPopup>
	</div>
</template>
<script>
import zwyPopup from '@/components/zwy-popup.vue';
import scrollViwe from '@/components/scroll-viwe.vue';

export default {
     
	name: 'coupon-pup',
	components: {
     
		zwyPopup,
		scrollViwe
	},
	data() {
     
		return {
     
			checked: false,
		};
	},
	props: {
     
		ishide: {
     
			type: Boolean,
			default: false
		},
		info: {
     
			type: Object,
			default: {
     }
		},
		titleStatus: {
     
			type: Boolean,
			default: false
		}
	},
	mounted: function() {
     
	},

	methods: {
     
		closeClick() {
     
			this.$emit('closeClick');
		},
	}
};
</script>
<style scoped>
.pup-box {
     
	width: 84%;
	min-height: 400px;
	background: linear-gradient(0deg, #ff5959, #ff6969);
	border-radius: 14px;
	position: relative;
}

.close-btn {
     
	position: absolute;
	right: 0;
	top: -90px;
	width: 72px;
	height: 72px;
	background: url('../../../assets/reduction/close-btn.png') no-repeat;
	background-size: 100% 100%;
}

.pup-banner {
     
	width: 100%;
	height: 180px;
	background: url('../../../assets/reduction/banner.png') no-repeat;
	background-size: 100% 100%;
	position: absolute;
	bottom: -230px;
	left: 0;
}



.benefits-title {
     
	margin: 0;
	text-align: center;
	font-size: 48px;
	font-family: Source Han Sans CN;
	font-weight: 400;
	color: #fc575a;
	line-height: 30px;
}

.coupons-btn {
     
	width: 68%;
	height: 80px;
	font-size: 32px;
	font-family: Source Han Sans CN;
	font-weight: 500;
	color: #b56408;
	line-height: 60px;
	background: url('../../../assets/reduction/btn1.png') no-repeat;
	background-size: 100% 100%;
	text-align: center;
	margin-left: 16%;
}

.pup-tc {
     
	position: absolute;
	top: 60px;
	width: 100%;
	height: 260px;
	background: url('../../../assets/reduction/pup-tc.png') no-repeat;
	background-size: 100% 100%;
	pointer-events: none;
}

.pup-box-nir {
     
	background: #feffff;
	width: calc(100% - 40px);
	min-height: 400px;
	margin: 20px;
	box-shadow: 0px -6px 10px 0px rgba(0, 0, 0, 0.1);
	border-radius: 14px;
}

.rules-box {
     
	width: calc(100% - 40px);
	/* min-height: 400px; */
	height: 400px;
	overflow: hidden;
	/* 	overflow-y: scroll;
	overflow-scrolling: touch;
	  -webkit-overflow-scrolling: touch; */
	margin: 20px;
	box-sizing: border-box;
	margin-top: 50px;

	font-size: 22px;
	font-family: Source Han Sans CN;
	font-weight: 400;
	color: #666666;
	line-height: 34px;
}

.rules-box::-webkit-scrollbar {
     
	display: none;
}

</style>

组件使用

<template>
	<div class="exothecium">
		<div class="rules" @click="rulesClick">弹窗按钮</div>
		<couponPup :ishide="ishide" @closeClick="closeClick"></couponPup>
	</div>
</template>
<script>
import couponPup from '../components/coupon-pup.vue';

export default {
     
	name: 'reduction_lj',
	components: {
     
		couponPup
	},
	data() {
     
		return {
     
			ishide: false
		};
	},
	mounted: function() {
     },
	methods: {
     
		closeClick() {
     
			this.ishide = false;
		}
	}
};
</script>
<style scoped>
* {
     
	touch-action: pan-y;
}

html,
body {
     
	overflow: hidden;
}
.exothecium {
     
	width: 100%;
	height: 100vh;
	background: #999;
	background-size: 100% 100%;
	padding-top: 42px;
	/* position: relative; */
}

.rules{
     
	width: 200px;
	height: 80px;
	background-color: aqua;
}
</style>


转载:https://blog.csdn.net/qq_43869822/article/details/128483497
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场