文件
OmniCard/mpuniapp/pages/template/draggable-half-modal/draggable-half-modal.uvue

255 行
8.0 KiB
Plaintext

<template>
<view ref="pageRef" class="page">
<text class="tip">半屏弹窗,演示了弹出层内scroll-view滚动到顶时由滚变拖。本效果是通过监听TouchEvent实现,当半屏窗口移动时禁用scroll-view的滚动,避免两者的冲突。</text>
<button class="bottomButton" @click="switchHalfScreen(true)">打开弹窗</button>
<view ref="halfScreenRef" class="halfScreen" @touchstart="onHalfTouchStart" @touchmove="onHalfTouchMove"
@touchend="onHalfTouchEnd">
<view class="halfTitle">半屏弹窗标题</view>
<scroll-view ref="halfScrollRef" class="halfScroll" bounces="true" :direction="scrollDirection">
<view v-for="(item,index) in list" :key="index" class="item">
half screen content-{{item}}
</view>
</scroll-view>
</view>
</view>
</template>
<script setup lang="uts">
/**
* @description: 支持从底部弹出、可拖拽下滑关闭的半屏弹窗,并处理弹窗拖拽与内部滚动的手势协调。
* @logic: 点击按钮后弹出半屏窗口;当内部 scroll-view 滚动到顶部时,继续下滑会切换为拖拽半屏窗口,松手后根据位移和速度决定回弹或关闭。
* @optimization: 通过在拖拽过程中动态切换内部 scroll-view 的滚动方向,避免拖拽弹窗与内容滚动同时生效造成手势冲突。
*/
const list = ref(['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15'])
let totalHeight = 0 //总高度
let halfMove = false //是否Move,响应TouchMove
let halfScreenY = 0 //响应TouchMove的起始点Y坐标
let halfOffset = 0 //偏移的位置,translateY
let halfHeight = 0 //高度
let lastY = 0 //上次
let lastY2 = 0 //
let bAnimation = false //是否动画
const pageRef = ref<UniElement | null>(null)
const halfScreenRef = ref<UniElement | null>(null)
const halfScrollRef = ref<UniScrollViewElement | null>(null)
const scrollDirection = ref('vertical')
function onHalfTouchStart(event : TouchEvent) {
halfScreenRef.value?.style?.setProperty('transition-duration', 0);
}
function onHalfTouchMove(e : TouchEvent) {
if (bAnimation) {//容错处理
return;
}
let top : number = halfScrollRef.value?.scrollTop ?? 0;
let p = e.touches[0];
lastY2 = lastY;
lastY = p.screenY;
if (top <= 0.01 || halfMove) {
if (halfScreenY == 0) {
halfScreenY = p.screenY;
}
let offset = p.screenY - halfScreenY;
if (offset > 0) {//向下滚动
halfMove = true;
// #ifdef APP
//halfScrollRef.value?.setAttribute('scroll-y', 'false');
halfScrollRef.value?.setAttribute('direction', 'none');
// #endif
// #ifdef WEB || MP
scrollDirection.value = 'none';
// #endif
halfScreenRef.value?.style?.setProperty('transform', 'translateY(' + offset.toFixed(2) + 'px)');
halfOffset = offset;
} else if (halfOffset > 0) {//向上滚动
offset = halfScreenY - p.screenY;
if (offset > halfOffset) {
offset = 0;
halfMove = false;
}
// #ifdef APP
//halfScrollRef.value?.setAttribute('scroll-y', 'true');
halfScrollRef.value?.setAttribute('direction', 'vertical');
// #endif
// #ifdef WEB || MP
scrollDirection.value = 'vertical';
// #endif
halfScreenRef.value?.style?.setProperty('transform', 'translateY(' + offset.toFixed(2) + 'px)');
halfOffset = offset;
}
}
// #ifdef WEB
e.preventDefault();
// #endif
}
function switchHalfScreen(show : boolean) {
if (show && ('visible' == halfScreenRef.value?.style?.getPropertyValue('visibility'))) {//容错处理
console.log('quick click button!!!');
return;
}
halfMove = false;
// #ifdef APP
//halfScrollRef.value?.setAttribute('scroll-y', 'true');
halfScrollRef.value?.setAttribute('direction', 'vertical');
// #endif
// #ifdef WEB || MP
scrollDirection.value = 'vertical';
// #endif
halfScreenY = 0;
halfOffset = 0;
let top = totalHeight;
let time = 300;
if (show) {
top = totalHeight * 30 / 100; //计算显示的位置
halfScreenRef.value?.style?.setProperty('visibility', 'visible');
halfScreenRef.value?.style?.setProperty('transition-timing-function', 'ease-in-out');
} else {
halfScreenRef.value?.style?.setProperty('transition-timing-function', 'linear');
time *= (halfHeight / totalHeight); //计算关闭动画时间
}
halfScreenRef.value?.style?.setProperty('transition-duration', time.toFixed(0)+'ms');
halfScreenRef.value?.style?.setProperty('transition-property', 'top');
halfScreenRef.value?.style?.setProperty('top', top.toFixed(2)+'px');
setTimeout(() => {
if (!show) {
halfScreenRef.value?.style?.setProperty('visibility', 'hidden');
halfScreenRef.value?.style?.setProperty('transition-duration', 0);
halfScreenRef.value?.style?.setProperty('transform', '');
}
halfScreenRef.value?.style?.setProperty('transition-property', 'none');
bAnimation = false;
}, time)
bAnimation = true;
}
function resumeHalfScreen() {
let time = 300;//(500*halfOffset/halfHeight).toFixed(0); //回弹动画时间
halfScreenRef.value?.style?.setProperty('transition-duration', time.toFixed(0)+'ms');
halfScreenRef.value?.style?.setProperty('transition-timing-function', 'ease-in-out');
halfScreenRef.value?.style?.setProperty('transition-property', 'transform');
halfScreenRef.value?.style?.setProperty('transform', 'translateY(0px)');
halfMove = false;
// #ifdef APP
//halfScrollRef.value?.setAttribute('scroll-y', 'true');
halfScrollRef.value?.setAttribute('direction', 'vertical');
// #endif
// #ifdef WEB || MP
scrollDirection.value = 'vertical';
// #endif
halfScreenY = 0;
halfOffset = 0;
setTimeout(() => {
bAnimation = false;
halfScreenRef.value?.style?.setProperty('transition-property', 'none');
}, time)
bAnimation = true;
}
function onHalfTouchEnd(event : TouchEvent) {
halfScreenY = 0;
if (bAnimation) {//容错处理
return;
}
let top : number = halfScrollRef.value?.scrollTop ?? 0;
let bHide = (halfHeight - halfOffset) < halfHeight / 4;
if (bHide) {
bHide = lastY2 > 0 && lastY2 <= lastY;
} else if (top <= 0.01) {
bHide = (lastY - lastY2) > 3; //向下滑动计算加速度判断是否关闭,简单处理未考虑时间
}
if (bHide) {
switchHalfScreen(false);
} else if (halfOffset > 0) {
resumeHalfScreen();
}
}
onReady(() => {
halfScreenRef.value!.getBoundingClientRectAsync()!.then((rect: DOMRect) => {
halfHeight = rect.height
});
pageRef.value!.getBoundingClientRectAsync()!.then((rect: DOMRect) => {
totalHeight = rect.height
halfScreenRef.value?.style?.setProperty('top', totalHeight.toFixed(2)+'px');
});
})
onResize(() => {
halfScreenRef.value?.getBoundingClientRectAsync()!.then((rect: DOMRect) => {
halfHeight = rect.height
});
totalHeight = uni.getWindowInfo().windowHeight;
halfScreenRef.value?.style?.setProperty('top', totalHeight.toFixed(2)+'px');
halfScreenRef.value?.style?.setProperty('visibility', 'hidden');
})
// #ifndef MP
onBackPress((): boolean => {
if('visible' == halfScreenRef.value?.style?.getPropertyValue('visibility')){
switchHalfScreen(false);
return true;
}
return false;
})
// #endif
</script>
<style>
/* #ifdef MP */
page {
overflow: hidden
}
/* #endif */
.page {
flex: 1;
background-color: darkgrey;
}
.tip {
margin: 10px
}
.bottomButton {
position: absolute;
width: 100%;
bottom: 0px;
/* #ifdef APP */
padding-bottom: env(safe-area-inset-bottom, 0px);
/* #endif */
}
.halfScreen {
position: absolute;
top: 100%;
width: 100%;
height: 70%;
transition-timing-function: ease-in-out;
/*ease ease-in ease-out ease-in-out linear step-start step-end*/
transition-property: top;
transition-duration: 0ms;
visibility: hidden;
}
.halfTitle {
align-items: center;
justify-content: center;
height: 48px;
background-color: ghostwhite;
border-radius: 10px 10px 0 0;
}
.halfScroll {
background-color: white;
flex: 1;
}
.item {
height: 100px;
}
</style>