优点:拖动响应快
缺点:动画有点奇怪
TypeScript
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
interface Props {
/**
* 头部高度(px)
* 默认 50
*/
headHeight?: number;
/**
* 内容高度(px)
* 默认 300
*/
contentHeight?: number;
/**
* 锚点,单位px
* 默认为[50, 200]
*/
anchors?: number[];
}
const props = withDefaults(defineProps<Props>(), {
headHeight: 50,
// contentHeight: 300,
// anchors: () => [50, 200],
contentHeight: 600,
anchors: () => [100, 300],
});
// 屏幕显示区域高度
const maxAnchorsHeight = computed(() => {
return Math.max(...props.anchors);
});
// 拖动区域
const dragAreaHeight = computed(() => {
return props.contentHeight + props.headHeight + maxAnchorsHeight.value;
});
// 偏移offsetY
const offsetY = ref(0);
// 当前拖动的y
let curY = 0;
// 结束定时器
let dragEndTimer: any = null;
onMounted(() => {
offsetY.value = getOffsetYByAnchor(Math.min(...props.anchors));
});
const getOffsetYByAnchor = (anchor: number): number => {
const anchorHeight = maxAnchorsHeight.value;
return anchorHeight - anchor;
};
// 管理拖动结束
const handleDragEnd = () => {
console.log("拖动结束");
dragEndTimer = null;
if (curY === offsetY.value) return;
const anchor = findNearestAnchor(getOffsetYByAnchor(curY));
// 这里的锚点是相对于屏幕的,而offsetY是相对于拖动区域的,需要转换
const result = getOffsetYByAnchor(anchor);
offsetY.value = result === offsetY.value ? result - 0.01 : result;
console.log("最终位置", offsetY.value);
};
// 鼠标松开事件
const handleTouchEnd = () => {
if (dragEndTimer) clearTimeout(dragEndTimer);
dragEndTimer = setTimeout(handleDragEnd, 100);
dragEndTimer = null;
};
// 拖动中事件
const handleTouchMove = (e: any) => {
curY = e.detail.y;
if (!dragEndTimer) return;
// 如果有结束定时器,说明已经拖动结束了,在做惯性动画
// 在做惯性动画,重新开启定时器,一直没有拖动为直
clearTimeout(dragEndTimer);
dragEndTimer = setTimeout(handleDragEnd, 100);
console.log("重新开启定时器");
};
// 计算最近的锚点(用于可能的自动吸附)
const findNearestAnchor = (currentY: number): number => {
return props.anchors.reduce((prev, curr) => {
return Math.abs(curr - currentY) < Math.abs(prev - currentY) ? curr : prev;
});
};
</script>
<template>
<view class="floating-panel">
<movable-area
class="floating-panel__drag-area"
:style="{
bottom: maxAnchorsHeight - dragAreaHeight + 'px',
height: `${dragAreaHeight}px`,
}"
>
<movable-view
class="floating-panel__movable-view"
:style="{
height: `${props.contentHeight + props.headHeight}px`,
}"
:y="offsetY"
direction="vertical"
@touchend="handleTouchEnd"
@change="handleTouchMove"
>
<view>
<slot name="head">
<view
class="floating-panel__head"
:style="{
height: `${props.headHeight}px`,
}"
></view>
</slot>
<view
class="floating-panel__content"
:style="{
height: `${props.contentHeight}px`,
}"
>
<slot></slot>
</view>
</view>
</movable-view>
</movable-area>
</view>
</template>
<style lang="scss" scoped>
.floating-panel {
position: fixed;
bottom: 0;
left: 0;
z-index: 20;
color: #fff;
width: 100%;
.floating-panel__drag-area {
position: absolute;
left: 0;
bottom: 200px;
width: 100%;
.floating-panel__movable-view {
width: 100%;
background-color: pink;
}
}
}
</style>