Skip to content

Commit c5b3c5f

Browse files
fix: 🐛 修复Sticky在h5和App端缓慢拖动时存在几率始终固定在顶部的问题 (#350)
1 parent dae3ffc commit c5b3c5f

File tree

4 files changed

+156
-130
lines changed

4 files changed

+156
-130
lines changed

src/pages/tabs/Index.vue

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<wd-tab :title="`标签${item}`">
88
<view class="content">
99
内容{{ tab1 + 1 }}
10-
<wd-button @click="tab1 < 3 ? tab1++ : (tab1 = 0)">下一个</wd-button>
10+
<view><wd-button @click="tab1 < 3 ? tab1++ : (tab1 = 0)">下一个</wd-button></view>
1111
</view>
1212
</wd-tab>
1313
</block>
@@ -53,21 +53,21 @@
5353
</wd-tabs>
5454
</demo-block>
5555

56-
<demo-block title="手势滑动" transparent>
57-
<wd-tabs v-model="tab5" swipeable @change="handleChange">
56+
<demo-block title="切换动画" transparent>
57+
<wd-tabs v-model="tab8" animated @change="handleChange">
5858
<block v-for="item in 4" :key="item">
5959
<wd-tab :title="`标签${item}`">
60-
<view class="content">内容{{ tab5 + 1 }}</view>
60+
<view class="content">内容{{ tab8 + 1 }}</view>
6161
</wd-tab>
6262
</block>
6363
</wd-tabs>
6464
</demo-block>
6565

66-
<demo-block title="切换动画" transparent>
67-
<wd-tabs v-model="tab8" animated @change="handleChange">
66+
<demo-block title="手势滑动" transparent>
67+
<wd-tabs v-model="tab5" swipeable animated @change="handleChange">
6868
<block v-for="item in 4" :key="item">
6969
<wd-tab :title="`标签${item}`">
70-
<view class="content">内容{{ tab8 + 1 }}</view>
70+
<view class="content">内容{{ tab5 + 1 }}</view>
7171
</wd-tab>
7272
</block>
7373
</wd-tabs>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* @Author: weisheng
3+
* @Date: 2024-04-08 22:34:01
4+
* @LastEditTime: 2024-06-01 16:04:56
5+
* @LastEditors: weisheng
6+
* @Description:
7+
* @FilePath: /wot-design-uni/src/uni_modules/wot-design-uni/components/wd-sticky-box/types.ts
8+
* 记得注释
9+
*/
10+
import type { ComponentInternalInstance, InjectionKey } from 'vue'
11+
12+
export type stickyBoxProvide = {
13+
boxStyle: {
14+
height: number // 高度
15+
width: number // 宽度
16+
}
17+
observerForChild: (child: ComponentInternalInstance) => void // 监听子组件
18+
}
19+
20+
export const STICKY_BOX_KEY: InjectionKey<stickyBoxProvide> = Symbol('wd-sticky-box')

src/uni_modules/wot-design-uni/components/wd-sticky-box/wd-sticky-box.vue

Lines changed: 58 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<template>
22
<view style="position: relative">
33
<view :class="`wd-sticky-box ${props.customClass}`" :style="customStyle" :id="styckyBoxId">
4-
<wd-resize @resize="resizeHandler">
4+
<wd-resize @resize="handleResize">
55
<slot />
66
</wd-resize>
77
</view>
@@ -20,38 +20,43 @@ export default {
2020
</script>
2121

2222
<script lang="ts" setup>
23-
import { getCurrentInstance, onBeforeMount, provide, ref } from 'vue'
23+
import { getCurrentInstance, onBeforeMount, reactive, ref } from 'vue'
2424
import { getRect, uuid } from '../common/util'
2525
import { baseProps } from '../common/props'
26+
import { STICKY_BOX_KEY } from './types'
27+
import { useChildren } from '../composables/useChildren'
2628
2729
const props = defineProps(baseProps)
2830
2931
const styckyBoxId = ref<string>(`wd-sticky-box${uuid()}`)
3032
3133
const observerMap = ref<Map<any, any>>(new Map())
32-
const height = ref<number>(0)
33-
const width = ref<number>(0)
3434
35-
const stickyList: any[] = [] // 子元素sticky列表
35+
const boxStyle = reactive({
36+
height: 0,
37+
width: 0
38+
})
3639
3740
const { proxy } = getCurrentInstance() as any
38-
const instance = getCurrentInstance() as any
3941
40-
provide('box-height', height)
41-
provide('box-width', width)
42-
provide('observerForChild', observerForChild)
42+
const { children: stickyList, linkChildren } = useChildren(STICKY_BOX_KEY)
43+
linkChildren({
44+
boxStyle: boxStyle,
45+
observerForChild
46+
})
4347
4448
onBeforeMount(() => {
4549
observerMap.value = new Map()
4650
})
4751
4852
/**
49-
* @description wd-sticky-box 尺寸发生变化时,重新监听所有的viewport
53+
* 容器大小变化后重新监听sticky组件与box组件的交叉状态
54+
* @param detail
5055
*/
51-
function resizeHandler(detail: any) {
56+
function handleResize(detail: any) {
5257
// 相对的容器大小改变后,同步设置 wd-sticky-box 的大小
53-
width.value = detail.width
54-
height.value = detail.height
58+
boxStyle.width = detail.width
59+
boxStyle.height = detail.height
5560
// wd-sticky-box 大小变化时,重新监听所有吸顶元素
5661
const temp = observerMap.value
5762
observerMap.value = new Map()
@@ -67,8 +72,8 @@ function resizeHandler(detail: any) {
6772
temp.clear()
6873
}
6974
/**
70-
* @description 删除 wd-sticky 废弃的监听器
71-
* @param child
75+
* 删除对指定sticky的监听
76+
* @param child 指定的子组件
7277
*/
7378
function deleteObserver(child: any) {
7479
const observer = observerMap.value.get(child.$.uid)
@@ -77,63 +82,70 @@ function deleteObserver(child: any) {
7782
observerMap.value.delete(child.$.uid)
7883
}
7984
/**
80-
* @description 为 wd-sticky 创建监听器
81-
* @param child
85+
* 针对指定sticky添加监听
86+
* @param child 指定的子组件
8287
*/
8388
function createObserver(child: any) {
84-
const observer = uni.createIntersectionObserver(instance)
89+
const observer = uni.createIntersectionObserver(proxy, { thresholds: [0, 0.5] })
8590
observerMap.value.set(child.$.uid, observer)
8691
return observer
8792
}
8893
/**
89-
* @description 为单个 wd-sticky 监听 viewport
90-
* @param child sticky
94+
* 监听子组件
95+
* @param child 子组件
9196
*/
9297
function observerForChild(child: any) {
93-
const hasChild = stickyList.find((sticky) => {
94-
return sticky.$.uid === child.$.uid
95-
})
96-
if (!hasChild) {
97-
stickyList.push(child)
98-
}
9998
deleteObserver(child)
10099
const observer = createObserver(child)
101-
102100
const exposed = child.$.exposed
103-
const offset = exposed.height.value + exposed.offsetTop
101+
let offset = exposed.stickyState.height + exposed.offsetTop
102+
// #ifdef H5
103+
// H5端,导航栏为普通元素,需要将组件移动到导航栏的下边沿
104+
// H5的导航栏高度为44px
105+
offset = offset + 44
106+
// #endif
104107
105-
// 如果 wd-sticky 比 wd-sticky-box还大,"相对吸顶"无任何意义,此时强制吸顶元素回归其占位符
106-
if (height.value <= exposed.height.value) {
108+
if (boxStyle.height <= exposed.stickyState.height) {
107109
exposed.setPosition(false, 'absolute', 0)
108110
}
109111
observer.relativeToViewport({ top: -offset }).observe(`#${styckyBoxId.value}`, (result) => {
110-
scrollHandler(exposed, result)
112+
handleRelativeTo(exposed, result)
111113
})
114+
// 当子组件默认处于边界外且永远不会进入边界内时,需要手动调用一次
112115
getRect(`#${styckyBoxId.value}`, false, proxy)
113116
.then((res) => {
114-
// 当 wd-sticky-box 位于 viewport 外部时不会触发 observe,此时根据位置手动修复位置。
115-
if (Number(res.bottom) <= offset) scrollHandler(exposed, { boundingClientRect: res })
117+
// #ifdef H5
118+
// H5端,查询节点信息未计算导航栏高度
119+
res.bottom = Number(res.bottom) + 44
120+
// #endif
121+
if (Number(res.bottom) <= offset) handleRelativeTo(exposed, { boundingClientRect: res })
116122
})
117123
.catch((res) => {
118124
console.log(res)
119125
})
120126
}
121127
/**
122-
* @description 为子节点监听 viewport,处理子节点的相对吸顶逻辑
128+
* 监听容器组件
123129
* @param {Object} exposed wd-sticky实例暴露出的事件
124-
* @param {Object} boundingClientRect 目标节点各个边在viewport中的坐标
130+
* @param {Object} boundingClientRect 边界信息
125131
*/
126-
function scrollHandler(exposed: any, { boundingClientRect }: any) {
127-
const offset = exposed.height.value + exposed.offsetTop
128-
if (boundingClientRect.bottom <= offset) {
129-
// 父元素即将被吸顶元素遮盖,将吸顶元素固定到父元素底部
130-
exposed.setPosition(true, 'absolute', boundingClientRect.height - exposed.height.value)
131-
} else if (boundingClientRect.top <= offset && boundingClientRect.bottom > offset) {
132-
// wd-sticky 已经完全呈现了 viewport 中了,
133-
// 此时没有必要再相对 wd-sticky-box 吸顶了
134-
if (exposed.state.value === 'normal') return
135-
// 顶元素开始遮盖不住父元素了,将吸顶元素恢复到吸顶模式
136-
exposed.setPosition(false, 'fixed', exposed.offsetTop)
132+
function handleRelativeTo(exposed: any, { boundingClientRect }: any) {
133+
let childOffsetTop = exposed.offsetTop
134+
// #ifdef H5
135+
// H5端,导航栏为普通元素,需要将组件移动到导航栏的下边沿
136+
// H5的导航栏高度为44px
137+
childOffsetTop = childOffsetTop + 44
138+
// #endif
139+
const offset = exposed.stickyState.height + childOffsetTop
140+
let isAbsolute = boundingClientRect.bottom <= offset
141+
// #ifdef H5 || APP-PLUS
142+
isAbsolute = boundingClientRect.bottom < offset
143+
// #endif
144+
if (isAbsolute) {
145+
exposed.setPosition(true, 'absolute', boundingClientRect.height - exposed.stickyState.height)
146+
} else if (boundingClientRect.top <= offset && !isAbsolute) {
147+
if (exposed.stickyState.state === 'normal') return
148+
exposed.setPosition(false, 'fixed', childOffsetTop)
137149
}
138150
}
139151
</script>

0 commit comments

Comments
 (0)