@tarojs/components-advanced 4.1.12-beta.4 → 4.1.12-beta.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/index.js +1 -0
- package/dist/components/index.js.map +1 -1
- package/dist/components/list/hooks/useItemSizeCache.d.ts +1 -13
- package/dist/components/list/hooks/useItemSizeCache.js +6 -50
- package/dist/components/list/hooks/useItemSizeCache.js.map +1 -1
- package/dist/components/list/hooks/useListNestedScroll.d.ts +18 -0
- package/dist/components/list/hooks/useListNestedScroll.js +61 -0
- package/dist/components/list/hooks/useListNestedScroll.js.map +1 -0
- package/dist/components/list/hooks/useListScrollElementAttach.d.ts +25 -0
- package/dist/components/list/hooks/useListScrollElementAttach.js +88 -0
- package/dist/components/list/hooks/useListScrollElementAttach.js.map +1 -0
- package/dist/components/list/hooks/useListScrollElementAttachWeapp.d.ts +14 -0
- package/dist/components/list/hooks/useListScrollElementAttachWeapp.js +134 -0
- package/dist/components/list/hooks/useListScrollElementAttachWeapp.js.map +1 -0
- package/dist/components/list/hooks/useMeasureStartOffset.d.ts +12 -0
- package/dist/components/list/hooks/useMeasureStartOffset.js +84 -0
- package/dist/components/list/hooks/useMeasureStartOffset.js.map +1 -0
- package/dist/components/list/hooks/useMeasureStartOffsetWeapp.d.ts +14 -0
- package/dist/components/list/hooks/useMeasureStartOffsetWeapp.js +82 -0
- package/dist/components/list/hooks/useMeasureStartOffsetWeapp.js.map +1 -0
- package/dist/components/list/hooks/useRefresher.d.ts +10 -2
- package/dist/components/list/hooks/useRefresher.js +150 -69
- package/dist/components/list/hooks/useRefresher.js.map +1 -1
- package/dist/components/list/hooks/useResizeObserver.d.ts +1 -0
- package/dist/components/list/hooks/useResizeObserver.js +37 -31
- package/dist/components/list/hooks/useResizeObserver.js.map +1 -1
- package/dist/components/list/hooks/useScrollCorrection.d.ts +3 -2
- package/dist/components/list/hooks/useScrollCorrection.js +7 -5
- package/dist/components/list/hooks/useScrollCorrection.js.map +1 -1
- package/dist/components/list/hooks/useScrollParentAutoFind.d.ts +20 -0
- package/dist/components/list/hooks/useScrollParentAutoFind.js +81 -0
- package/dist/components/list/hooks/useScrollParentAutoFind.js.map +1 -0
- package/dist/components/list/index.d.ts +16 -6
- package/dist/components/list/index.js +435 -498
- package/dist/components/list/index.js.map +1 -1
- package/dist/components/list/utils.d.ts +5 -0
- package/dist/components/list/utils.js +17 -1
- package/dist/components/list/utils.js.map +1 -1
- package/dist/components/virtual-list/vue/list.d.ts +10 -10
- package/dist/components/virtual-waterfall/vue/waterfall.d.ts +10 -10
- package/dist/components/water-flow/flow-item.js +6 -4
- package/dist/components/water-flow/flow-item.js.map +1 -1
- package/dist/components/water-flow/flow-section.js +1 -1
- package/dist/components/water-flow/flow-section.js.map +1 -1
- package/dist/components/water-flow/index.d.ts +1 -1
- package/dist/components/water-flow/interface.d.ts +18 -2
- package/dist/components/water-flow/root.d.ts +35 -4
- package/dist/components/water-flow/root.js +114 -42
- package/dist/components/water-flow/root.js.map +1 -1
- package/dist/components/water-flow/section.d.ts +7 -1
- package/dist/components/water-flow/section.js +54 -9
- package/dist/components/water-flow/section.js.map +1 -1
- package/dist/components/water-flow/utils.d.ts +4 -0
- package/dist/components/water-flow/utils.js +5 -1
- package/dist/components/water-flow/utils.js.map +1 -1
- package/dist/components/water-flow/water-flow-node-cache.d.ts +24 -0
- package/dist/components/water-flow/water-flow-node-cache.js +161 -0
- package/dist/components/water-flow/water-flow-node-cache.js.map +1 -0
- package/dist/components/water-flow/water-flow.d.ts +2 -3
- package/dist/components/water-flow/water-flow.js +286 -31
- package/dist/components/water-flow/water-flow.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/scrollElementContext.d.ts +15 -0
- package/dist/utils/scrollElementContext.js +14 -0
- package/dist/utils/scrollElementContext.js.map +1 -0
- package/dist/utils/scrollParent.d.ts +33 -0
- package/dist/utils/scrollParent.js +88 -0
- package/dist/utils/scrollParent.js.map +1 -0
- package/package.json +9 -8
|
@@ -2,17 +2,22 @@ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
|
2
2
|
import { View, ScrollView } from '@tarojs/components';
|
|
3
3
|
import Taro from '@tarojs/taro';
|
|
4
4
|
import React from 'react';
|
|
5
|
+
import '../../utils/index.js';
|
|
6
|
+
import { ScrollElementContextOrFallback } from '../../utils/scrollElementContext.js';
|
|
5
7
|
import { useItemSizeCache } from './hooks/useItemSizeCache.js';
|
|
6
|
-
import {
|
|
8
|
+
import { useListNestedScroll } from './hooks/useListNestedScroll.js';
|
|
9
|
+
import { useListScrollElementAttach } from './hooks/useListScrollElementAttach.js';
|
|
10
|
+
import { useListScrollElementAttachWeapp } from './hooks/useListScrollElementAttachWeapp.js';
|
|
11
|
+
import { useRefresher } from './hooks/useRefresher.js';
|
|
7
12
|
import { useResizeObserver } from './hooks/useResizeObserver.js';
|
|
8
13
|
import { useScrollCorrection } from './hooks/useScrollCorrection.js';
|
|
9
14
|
import { ListItem } from './ListItem.js';
|
|
10
15
|
import { NoMore } from './NoMore.js';
|
|
11
16
|
import { StickyHeader } from './StickyHeader.js';
|
|
12
17
|
import { StickySection } from './StickySection.js';
|
|
13
|
-
import { supportsNativeRefresher } from './utils.js';
|
|
18
|
+
import { isWeapp, createSelectorQueryScoped, isH5, supportsNativeRefresher } from './utils.js';
|
|
19
|
+
import { getScrollViewContextNode } from '../../utils/dom.js';
|
|
14
20
|
|
|
15
|
-
// 工具:累加数组
|
|
16
21
|
function accumulate(arr) {
|
|
17
22
|
const result = [0];
|
|
18
23
|
for (let i = 0; i < arr.length; i++) {
|
|
@@ -20,77 +25,86 @@ function accumulate(arr) {
|
|
|
20
25
|
}
|
|
21
26
|
return result;
|
|
22
27
|
}
|
|
23
|
-
// 检测抖动
|
|
24
28
|
function isShaking(diffList) {
|
|
25
29
|
if (diffList.length < 3)
|
|
26
30
|
return false;
|
|
27
|
-
// 检查是否有连续的正负交替
|
|
28
31
|
const signs = diffList.map(diff => Math.sign(diff));
|
|
29
32
|
let alternations = 0;
|
|
30
33
|
for (let i = 1; i < signs.length; i++) {
|
|
31
|
-
if (signs[i] !== 0 && signs[i] !== signs[i - 1])
|
|
34
|
+
if (signs[i] !== 0 && signs[i] !== signs[i - 1])
|
|
32
35
|
alternations++;
|
|
33
|
-
}
|
|
34
36
|
}
|
|
35
|
-
// 如果交替次数过多,认为是抖动
|
|
36
37
|
return alternations >= 2;
|
|
37
38
|
}
|
|
38
39
|
// 小程序端:判断 item 是否应该执行 SelectorQuery 测量(仅检查是否已测量过)
|
|
39
40
|
// SelectorQuery 是只读查询,不会触发 setData,滚动期间执行是安全的
|
|
40
|
-
// 真正需要延迟的是 sizeCacheVersion bump(由 weappSizeCacheVersionBump 负责)
|
|
41
41
|
function shouldMeasureWeappItem(index, measuredSet) {
|
|
42
42
|
return !measuredSet.has(index);
|
|
43
43
|
}
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
// 避免父层重渲染/remount 导致回顶或空白。
|
|
58
|
-
}
|
|
59
|
-
// 小程序端(定高路径):记录滚动期间的测量变化,用于 flush 时计算滚动修正量
|
|
60
|
-
// 使用 Map 去重:同一个 index 多次测量时,保留 originalOldSize(首次)和 latestNewSize(最新)
|
|
61
|
-
function weappRecordMeasurement(pendingMeasurementsRef, index, oldSize, newSize) {
|
|
62
|
-
const existing = pendingMeasurementsRef.current.get(index);
|
|
63
|
-
if (existing) {
|
|
64
|
-
existing.latestNewSize = newSize;
|
|
44
|
+
// 小程序端暂不外抛 onItemSizeChange,避免父层重渲染导致 List remount 引发回顶或空白
|
|
45
|
+
function weappDeferItemSizeChange(_index, _size, _onItemSizeChange) { }
|
|
46
|
+
/** 从 scroll 选项解析目标偏移量 */
|
|
47
|
+
function resolveScrollTargetOffset(options, isHorizontal) {
|
|
48
|
+
const opts = options !== null && options !== void 0 ? options : {};
|
|
49
|
+
const top = typeof opts.top === 'number' ? opts.top : undefined;
|
|
50
|
+
const left = typeof opts.left === 'number' ? opts.left : undefined;
|
|
51
|
+
let result = 0;
|
|
52
|
+
if (isHorizontal) {
|
|
53
|
+
if (typeof left === 'number')
|
|
54
|
+
result = left;
|
|
55
|
+
else if (typeof top === 'number')
|
|
56
|
+
result = top;
|
|
65
57
|
}
|
|
66
58
|
else {
|
|
67
|
-
|
|
59
|
+
if (typeof top === 'number')
|
|
60
|
+
result = top;
|
|
61
|
+
else if (typeof left === 'number')
|
|
62
|
+
result = left;
|
|
68
63
|
}
|
|
64
|
+
return Number.isFinite(result) ? result : 0;
|
|
69
65
|
}
|
|
70
|
-
//
|
|
66
|
+
// eslint-disable-next-line complexity -- List 多端/多模式逻辑集中,已抽离 useListNestedScroll、useListScrollElementAttach、resolveScrollTargetOffset 等
|
|
71
67
|
const InnerList = (props, ref) => {
|
|
72
|
-
var _a;
|
|
73
|
-
const
|
|
74
|
-
const isWeapp = process.env.TARO_ENV === 'weapp';
|
|
75
|
-
const { stickyHeader = false, space = 0, height = 400, width = '100%', showScrollbar = true, scrollTop: controlledScrollTop, scrollX = false, scrollY = true, onScroll, onScrollToUpper, onScrollToLower, onScrollStart, onScrollEnd, upperThreshold = 0, lowerThreshold = 0, cacheCount = 2, cacheExtent, enableBackToTop, className, style, children, } = props;
|
|
68
|
+
var _a, _b;
|
|
69
|
+
const { stickyHeader = false, space = 0, height = 400, width = '100%', showScrollbar = true, scrollTop: controlledScrollTop, scrollX = false, scrollY = true, onScroll, onScrollToUpper, onScrollToLower, onScrollStart, onScrollEnd, upperThreshold = 50, lowerThreshold = 50, cacheCount = 2, cacheExtent, enableBackToTop, className, style, children, nestedScroll, scrollElement, scrollRef: scrollRefProp, } = props;
|
|
76
70
|
const isHorizontal = scrollX === true;
|
|
71
|
+
const listType = nestedScroll === true ? 'nested' : 'default';
|
|
72
|
+
const { effectiveScrollElement, effectiveStartOffset, effectiveStartOffsetRef, useScrollElementMode, needAutoFind, autoFindStatus, contentWrapperRef, contentId, } = useListNestedScroll(listType, scrollElement, undefined, isHorizontal, props.selectorQueryScope);
|
|
77
73
|
const DEFAULT_ITEM_WIDTH = 120;
|
|
78
74
|
const DEFAULT_ITEM_HEIGHT = 40;
|
|
75
|
+
const defaultItemSize = isHorizontal ? DEFAULT_ITEM_WIDTH : DEFAULT_ITEM_HEIGHT;
|
|
76
|
+
const normalizeSize = React.useCallback((value) => {
|
|
77
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0)
|
|
78
|
+
return null;
|
|
79
|
+
return value;
|
|
80
|
+
}, []);
|
|
81
|
+
const resolveItemSizeByIndex = React.useCallback((index, fallback) => {
|
|
82
|
+
const { itemSize, itemData } = props;
|
|
83
|
+
const numberSize = normalizeSize(itemSize);
|
|
84
|
+
if (numberSize != null)
|
|
85
|
+
return numberSize;
|
|
86
|
+
if (typeof itemSize === 'function') {
|
|
87
|
+
const functionSize = normalizeSize(itemSize(index, itemData));
|
|
88
|
+
if (functionSize != null)
|
|
89
|
+
return functionSize;
|
|
90
|
+
}
|
|
91
|
+
return fallback;
|
|
92
|
+
}, [props.itemSize, props.itemData, normalizeSize]);
|
|
79
93
|
// 滚动状态管理
|
|
80
94
|
const containerRef = React.useRef(null);
|
|
81
95
|
// 生成唯一 List ID(用于小程序 ResizeObserver)
|
|
82
96
|
const listId = React.useMemo(() => `list-${Math.random().toString(36).slice(2, 11)}`, []);
|
|
83
97
|
// 渲染偏移量 - 用于计算应该渲染哪些元素
|
|
84
98
|
const [renderOffset, setRenderOffset] = React.useState(controlledScrollTop !== null && controlledScrollTop !== void 0 ? controlledScrollTop : 0);
|
|
85
|
-
//
|
|
99
|
+
// 程序性滚动用的目标偏移;用户滑动期间不更新,避免与原生滚动冲突
|
|
86
100
|
const [scrollViewOffset, setScrollViewOffset] = React.useState(controlledScrollTop !== null && controlledScrollTop !== void 0 ? controlledScrollTop : 0);
|
|
87
101
|
// 用户正在滑动时不再向 ScrollView 传 scrollTop,让滚动完全由原生接管
|
|
88
102
|
const [isUserScrolling, setIsUserScrolling] = React.useState(false);
|
|
89
|
-
// isUserScrolling 的 ref
|
|
103
|
+
// isUserScrolling 的 ref 镜像,供异步上下文读取最新值
|
|
90
104
|
const isUserScrollingRef = React.useRef(false);
|
|
91
105
|
const initialContainerLength = typeof (isHorizontal ? width : height) === 'number' ? (isHorizontal ? width : height) : 400;
|
|
92
106
|
const [containerLength, setContainerLength] = React.useState(initialContainerLength);
|
|
93
|
-
//
|
|
107
|
+
// 用容器实际尺寸更新视口长度,避免 props 与 CSS 不一致导致底部空白
|
|
94
108
|
React.useEffect(() => {
|
|
95
109
|
const el = containerRef.current;
|
|
96
110
|
if (!el || typeof ResizeObserver === 'undefined')
|
|
@@ -106,31 +120,41 @@ const InnerList = (props, ref) => {
|
|
|
106
120
|
ro.observe(el);
|
|
107
121
|
return () => ro.disconnect();
|
|
108
122
|
}, [isHorizontal]);
|
|
123
|
+
// WeChat 小程序:没有原生 ResizeObserver,用 SelectorQuery 一次性测量容器高度
|
|
124
|
+
React.useEffect(() => {
|
|
125
|
+
if (!isWeapp)
|
|
126
|
+
return;
|
|
127
|
+
Taro.nextTick(() => {
|
|
128
|
+
createSelectorQueryScoped(props.selectorQueryScope)
|
|
129
|
+
.select(`#${listId}`)
|
|
130
|
+
.boundingClientRect((rect) => {
|
|
131
|
+
const measured = isHorizontal ? rect === null || rect === void 0 ? void 0 : rect.width : rect === null || rect === void 0 ? void 0 : rect.height;
|
|
132
|
+
if (typeof measured === 'number' && measured > 0) {
|
|
133
|
+
setContainerLength(measured);
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
.exec();
|
|
137
|
+
});
|
|
138
|
+
}, [isWeapp, isHorizontal, listId]);
|
|
109
139
|
// 滚动追踪相关refs
|
|
110
140
|
const isScrollingRef = React.useRef(false);
|
|
111
141
|
const lastScrollTopRef = React.useRef(controlledScrollTop !== null && controlledScrollTop !== void 0 ? controlledScrollTop : 0);
|
|
112
142
|
const scrollDiffListRef = React.useRef([0, 0, 0]);
|
|
113
143
|
const scrollTimeoutRef = React.useRef(null);
|
|
114
|
-
//
|
|
115
|
-
const lastScrollEventAtRef = React.useRef(Date.now());
|
|
116
|
-
// 小程序端:微动静止检测定时器(仅在存在 pending 测量时启动)
|
|
117
|
-
const settleCheckTimerRef = React.useRef(null);
|
|
118
|
-
const settleLastOffsetRef = React.useRef(controlledScrollTop !== null && controlledScrollTop !== void 0 ? controlledScrollTop : 0);
|
|
119
|
-
const settleStillCountRef = React.useRef(0);
|
|
120
|
-
// 仅程序性滚动(scrollIntoView/修正/初始)时才把 scrollViewOffset 写回 H5 DOM,避免用户拖动结束后误把旧值写回导致卡顿/跳跃(在桌面 Chrome 模拟器下尤为明显)
|
|
144
|
+
// H5:仅程序性滚动时写回 DOM scrollTop,用户滑动结束后不写回避免卡顿
|
|
121
145
|
const programmaticScrollRef = React.useRef(false);
|
|
122
|
-
//
|
|
123
|
-
const programmaticTargetOffsetRef = React.useRef(null);
|
|
124
|
-
const suppressResetUntilRef = React.useRef(0);
|
|
125
|
-
// guard 兜底回补节流,避免异常回顶事件频繁触发重复回补
|
|
126
|
-
const guardHealAtRef = React.useRef(0);
|
|
127
|
-
// 程序性滚动冷却期:在此期间内 handleScroll 不设置同步定时器,避免 scrollIntoView 后被原生 scroll 回调"拉回"
|
|
146
|
+
// 小程序端程序性滚动冷却期:handleScroll 只更新 renderOffset,避免 scrollIntoView 后被原生回调拉回
|
|
128
147
|
const programmaticCooldownRef = React.useRef(false);
|
|
129
148
|
const programmaticCooldownTimerRef = React.useRef(null);
|
|
130
|
-
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
149
|
+
const scrollViewOffsetRef = React.useRef(0);
|
|
150
|
+
// 用户滚动期间挂起的 reflow 标记(用于滚动结束后补触发)
|
|
151
|
+
const pendingWeappReflowRef = React.useRef(false);
|
|
152
|
+
// ref 持有最新版 scheduleWeappDynamicReflow,供 updateRenderOffset 内的 setTimeout 闭包安全访问
|
|
153
|
+
const scheduleWeappDynamicReflowRef = React.useRef(null);
|
|
154
|
+
// 处理渲染偏移量更新。
|
|
155
|
+
// syncToScrollView=true:程序性滚动,立即同步 scrollViewOffset。
|
|
156
|
+
// syncToScrollView=false:用户滑动,仅更新 renderOffset。weapp 采用 recycle-view 策略:不把用户滑动位置同步到 scrollViewOffset,避免「传滞后值拉回」和「从有到无归顶」。
|
|
157
|
+
const updateRenderOffset = React.useCallback((newOffset, syncToScrollView, source) => {
|
|
134
158
|
lastScrollTopRef.current = newOffset;
|
|
135
159
|
isScrollingRef.current = true;
|
|
136
160
|
if (scrollTimeoutRef.current) {
|
|
@@ -140,21 +164,31 @@ const InnerList = (props, ref) => {
|
|
|
140
164
|
if (syncToScrollView) {
|
|
141
165
|
isUserScrollingRef.current = false;
|
|
142
166
|
setIsUserScrolling(false);
|
|
143
|
-
|
|
167
|
+
// 小程序:target===sv 时 ScrollView 认为无变化不滚动→白屏;imperative/scrollIntoView 时先传中间值再 RAF 传 target 强制触发
|
|
168
|
+
// target=0 用 +0.01;target>0 用 -0.01(统一 +0.01 到底部会被 clamp 成同值无效)
|
|
169
|
+
const same = isWeapp && Math.abs(scrollViewOffsetRef.current - newOffset) < 1;
|
|
170
|
+
const needForce = same && (source === 'imperative' || source === 'scrollIntoView');
|
|
171
|
+
if (needForce) {
|
|
172
|
+
const intermediate = newOffset > 0 ? newOffset - 0.01 : 0.01;
|
|
173
|
+
setScrollViewOffset(intermediate);
|
|
174
|
+
programmaticScrollRef.current = true;
|
|
175
|
+
requestAnimationFrame(() => {
|
|
176
|
+
// 第二帧也需要标记为程序性滚动,否则 else 分支不会传 scrollTop
|
|
177
|
+
programmaticScrollRef.current = true;
|
|
178
|
+
setScrollViewOffset(newOffset);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
setScrollViewOffset(newOffset);
|
|
183
|
+
}
|
|
144
184
|
programmaticScrollRef.current = true;
|
|
145
|
-
// 小程序端:程序性滚动开启冷却期,在冷却期内忽略 handleScroll 中的同步逻辑,避免被原生 scroll 回调"拉回"
|
|
146
|
-
// H5 端已稳定,不启用冷却期
|
|
147
185
|
if (isWeapp) {
|
|
148
|
-
programmaticTargetOffsetRef.current = newOffset;
|
|
149
|
-
suppressResetUntilRef.current = Date.now() + 700;
|
|
150
186
|
programmaticCooldownRef.current = true;
|
|
151
187
|
if (programmaticCooldownTimerRef.current) {
|
|
152
188
|
clearTimeout(programmaticCooldownTimerRef.current);
|
|
153
189
|
}
|
|
154
190
|
programmaticCooldownTimerRef.current = setTimeout(() => {
|
|
155
191
|
programmaticCooldownRef.current = false;
|
|
156
|
-
programmaticTargetOffsetRef.current = null;
|
|
157
|
-
suppressResetUntilRef.current = 0;
|
|
158
192
|
}, 500);
|
|
159
193
|
}
|
|
160
194
|
}
|
|
@@ -165,44 +199,59 @@ const InnerList = (props, ref) => {
|
|
|
165
199
|
scrollTimeoutRef.current = setTimeout(() => {
|
|
166
200
|
isScrollingRef.current = false;
|
|
167
201
|
if (!syncToScrollView) {
|
|
168
|
-
// 滚动结束后同步:state 与真实滚动位置一致,下次程序性滚动有正确基准
|
|
169
202
|
isUserScrollingRef.current = false;
|
|
170
|
-
setScrollViewOffset(lastScrollTopRef.current);
|
|
171
203
|
setIsUserScrolling(false);
|
|
204
|
+
// weapp recycle-view 策略:用户滑动结束后不同步 scrollViewOffset,保持 pass 的值不变,避免 从有到无 归顶
|
|
205
|
+
if (!isWeapp) {
|
|
206
|
+
setScrollViewOffset(lastScrollTopRef.current);
|
|
207
|
+
}
|
|
208
|
+
// 滚动结束后触发期间被延迟的 reflow(item 首次测量触发)
|
|
209
|
+
if (isWeapp && pendingWeappReflowRef.current) {
|
|
210
|
+
scheduleWeappDynamicReflowRef.current();
|
|
211
|
+
}
|
|
172
212
|
}
|
|
173
213
|
}, isWeapp ? 200 : 150);
|
|
174
214
|
}, []);
|
|
175
215
|
// 暴露给外部的实例方法:通过 ref.scroll({ top / left }) 进行程序性滚动
|
|
176
216
|
React.useImperativeHandle(ref, () => ({
|
|
177
217
|
scroll(options) {
|
|
178
|
-
const
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (typeof left === 'number') {
|
|
185
|
-
targetOffset = left;
|
|
218
|
+
const targetOffset = resolveScrollTargetOffset(options, isHorizontal);
|
|
219
|
+
const el = effectiveScrollElement === null || effectiveScrollElement === void 0 ? void 0 : effectiveScrollElement.current;
|
|
220
|
+
if (el && isH5) {
|
|
221
|
+
const scrollTarget = targetOffset + effectiveStartOffset;
|
|
222
|
+
if (isHorizontal) {
|
|
223
|
+
el.scrollTo({ left: scrollTarget });
|
|
186
224
|
}
|
|
187
|
-
else
|
|
188
|
-
|
|
225
|
+
else {
|
|
226
|
+
el.scrollTo({ top: scrollTarget });
|
|
189
227
|
}
|
|
228
|
+
updateRenderOffset(targetOffset, false, 'scrollElement');
|
|
190
229
|
}
|
|
191
|
-
else {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
230
|
+
else if (el && isWeapp && useScrollElementMode) {
|
|
231
|
+
// 小程序 scrollElement 模式:需通过 getScrollViewContextNode + node.scrollTo 真正滚动外部 scroll-view
|
|
232
|
+
// updateRenderOffset 必须在 scrollTo 之后调用,否则 getScrollViewContextNode 异步期间会先更新 renderOffset 导致闪一下
|
|
233
|
+
const startOff = effectiveStartOffsetRef.current;
|
|
234
|
+
const scrollTarget = targetOffset + startOff;
|
|
235
|
+
const scrollViewId = el.id || `_ls_${listId}`;
|
|
236
|
+
if (!el.id)
|
|
237
|
+
el.id = scrollViewId;
|
|
238
|
+
getScrollViewContextNode(`#${scrollViewId}`).then((node) => {
|
|
239
|
+
var _a, _b;
|
|
240
|
+
if (isHorizontal) {
|
|
241
|
+
(_a = node === null || node === void 0 ? void 0 : node.scrollTo) === null || _a === void 0 ? void 0 : _a.call(node, { left: scrollTarget, animated: false });
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
(_b = node === null || node === void 0 ? void 0 : node.scrollTo) === null || _b === void 0 ? void 0 : _b.call(node, { top: scrollTarget, animated: false });
|
|
245
|
+
}
|
|
246
|
+
updateRenderOffset(targetOffset, true, 'imperative');
|
|
247
|
+
});
|
|
198
248
|
}
|
|
199
|
-
|
|
200
|
-
targetOffset
|
|
249
|
+
else {
|
|
250
|
+
updateRenderOffset(targetOffset, true, 'imperative');
|
|
201
251
|
}
|
|
202
|
-
updateRenderOffset(targetOffset, true);
|
|
203
252
|
},
|
|
204
|
-
}), [scrollX, updateRenderOffset]);
|
|
205
|
-
// 提取 Refresher
|
|
253
|
+
}), [scrollX, effectiveScrollElement, effectiveStartOffset, effectiveStartOffsetRef, isH5, isWeapp, isHorizontal, listId, useScrollElementMode, updateRenderOffset]);
|
|
254
|
+
// 提取 Refresher 配置(List 属性为 base,Refresher 子组件覆盖)
|
|
206
255
|
const refresherConfig = React.useMemo(() => {
|
|
207
256
|
const listRefresherEnabled = props.refresherEnabled !== false && (props.refresherEnabled === true || props.onRefresherRefresh != null);
|
|
208
257
|
const baseFromList = listRefresherEnabled
|
|
@@ -220,7 +269,6 @@ const InnerList = (props, ref) => {
|
|
|
220
269
|
onRefresherStatusChange: props.onRefresherStatusChange,
|
|
221
270
|
}
|
|
222
271
|
: null;
|
|
223
|
-
// 通过 displayName 检测 Refresher 子组件(保持与 Refresher 组件解耦)
|
|
224
272
|
const isRefresherComponent = (child) => {
|
|
225
273
|
const type = child.type;
|
|
226
274
|
return (type === null || type === void 0 ? void 0 : type.displayName) === 'Refresher' || (type === null || type === void 0 ? void 0 : type.name) === 'Refresher';
|
|
@@ -268,7 +316,8 @@ const InnerList = (props, ref) => {
|
|
|
268
316
|
if (noMoreCount > 1) {
|
|
269
317
|
return;
|
|
270
318
|
}
|
|
271
|
-
|
|
319
|
+
const childProps = child.props;
|
|
320
|
+
config = Object.assign(Object.assign({}, childProps), { visible: childProps.visible !== false });
|
|
272
321
|
}
|
|
273
322
|
});
|
|
274
323
|
// Props 方式转换为配置(优先级低于子组件)
|
|
@@ -282,8 +331,10 @@ const InnerList = (props, ref) => {
|
|
|
282
331
|
}
|
|
283
332
|
return config;
|
|
284
333
|
}, [children, props.showNoMore, props.noMoreText, props.noMoreStyle, props.renderNoMore]);
|
|
285
|
-
//
|
|
286
|
-
const { scrollViewRefresherProps, scrollViewRefresherHandlers, h5RefresherProps, addImperativeTouchListeners, renderRefresherContent, } = useRefresher(refresherConfig, isH5 && refresherConfig && !supportsNativeRefresher ? 0 : renderOffset
|
|
334
|
+
// Refresher 平台适配:H5 用 addImperativeTouchListeners
|
|
335
|
+
const { scrollViewRefresherProps, scrollViewRefresherHandlers, h5RefresherProps, addImperativeTouchListeners, renderRefresherContent, slotHeight: h5RefresherSlotHeight, } = useRefresher(refresherConfig, isH5 && refresherConfig && !supportsNativeRefresher ? 0 : renderOffset, useScrollElementMode
|
|
336
|
+
? (ev) => { var _a, _b; return (_b = (ev.target != null && ((_a = contentWrapperRef.current) === null || _a === void 0 ? void 0 : _a.contains(ev.target)))) !== null && _b !== void 0 ? _b : false; }
|
|
337
|
+
: undefined, !!(isH5 && refresherConfig && !supportsNativeRefresher && refresherConfig.refresherEnabled !== false));
|
|
287
338
|
const refresherTeardownRef = React.useRef(null);
|
|
288
339
|
const addImperativeTouchListenersRef = React.useRef(addImperativeTouchListeners);
|
|
289
340
|
addImperativeTouchListenersRef.current = addImperativeTouchListeners;
|
|
@@ -291,17 +342,16 @@ const InnerList = (props, ref) => {
|
|
|
291
342
|
if (refresherTeardownRef.current)
|
|
292
343
|
refresherTeardownRef.current();
|
|
293
344
|
}, []);
|
|
294
|
-
// H5
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
//
|
|
345
|
+
// H5 下拉刷新顶栏高度:自定义内容由 useRefresher 内 ResizeObserver 测量,否则默认 50
|
|
346
|
+
const refresherHeightForH5 = (isH5 && refresherConfig && !supportsNativeRefresher && refresherConfig.refresherEnabled !== false)
|
|
347
|
+
? h5RefresherSlotHeight
|
|
348
|
+
: 0;
|
|
349
|
+
// 解析分组结构:StickySection、ListItem 为直接子组件,过滤 Refresher/NoMore
|
|
299
350
|
const sections = React.useMemo(() => {
|
|
300
351
|
const result = [];
|
|
301
352
|
const defaultItems = [];
|
|
302
353
|
React.Children.forEach(children, (child, idx) => {
|
|
303
354
|
if (React.isValidElement(child) && child.type === StickySection) {
|
|
304
|
-
// 分组模式
|
|
305
355
|
const sectionProps = child.props;
|
|
306
356
|
let header = null;
|
|
307
357
|
const items = [];
|
|
@@ -314,19 +364,16 @@ const InnerList = (props, ref) => {
|
|
|
314
364
|
result.push({ header, items, key: child.key || String(idx) });
|
|
315
365
|
}
|
|
316
366
|
else if (React.isValidElement(child) && child.type === ListItem) {
|
|
317
|
-
// 普通 ListItem
|
|
318
367
|
defaultItems.push(child);
|
|
319
368
|
}
|
|
320
|
-
// 忽略 Refresher 和 NoMore 组件(已在上面提取配置)
|
|
321
369
|
});
|
|
322
370
|
if (defaultItems.length > 0) {
|
|
323
371
|
result.push({ header: null, items: defaultItems, key: 'default' });
|
|
324
372
|
}
|
|
325
373
|
return result;
|
|
326
374
|
}, [children]);
|
|
327
|
-
//
|
|
328
|
-
const
|
|
329
|
-
const estimatedSize = (_a = props.estimatedItemSize) !== null && _a !== void 0 ? _a : defaultEstimatedSize;
|
|
375
|
+
// 动态尺寸管理
|
|
376
|
+
const estimatedSize = resolveItemSizeByIndex(0, defaultItemSize);
|
|
330
377
|
// 计算总 item 数量(跨所有 section)
|
|
331
378
|
const totalItemCount = React.useMemo(() => {
|
|
332
379
|
return sections.reduce((sum, section) => sum + section.items.length, 0);
|
|
@@ -340,129 +387,83 @@ const InnerList = (props, ref) => {
|
|
|
340
387
|
// 动态尺寸缓存更新版本:setItemSize 后递增,用于驱动 sectionOffsets/totalLength 与 item 定位重算
|
|
341
388
|
const [sizeCacheVersion, setSizeCacheVersion] = React.useState(0);
|
|
342
389
|
const sizeCacheRafRef = React.useRef(null);
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
const measureScrollProtectRef = React.useRef(false);
|
|
346
|
-
// 小程序端(定高路径):滚动期间 sizeCacheVersion bump 被延迟,此标记记录有 pending bump 需要 flush
|
|
347
|
-
const pendingBumpRef = React.useRef(false);
|
|
348
|
-
// 定高路径:flush 后需要在下一帧恢复滚动位置(存储目标 scrollTop,null 表示无需恢复)
|
|
349
|
-
const scrollRestoreRef = React.useRef(null);
|
|
350
|
-
// 定高路径:restore 来源滚动位置(用于判断 restore 是否过期)
|
|
351
|
-
const scrollRestoreFromRef = React.useRef(null);
|
|
352
|
-
// 小程序端(定高路径):flush 恢复窗口标记。
|
|
353
|
-
const weappFlushRestorePendingRef = React.useRef(false);
|
|
354
|
-
// 定高路径:滚动期间累积的测量记录,用于 flush 时计算滚动修正量
|
|
355
|
-
const pendingMeasurementsRef = React.useRef(new Map());
|
|
356
|
-
// 小程序端:延迟 bump 时不同步调用 onItemSizeChange,避免父组件重渲染导致 List remount 闪回顶部;flush 时统一调用
|
|
357
|
-
const pendingSizeChangesRef = React.useRef([]);
|
|
358
|
-
// 小程序端(定高路径):pending 测量的“微动静止检测”
|
|
359
|
-
const scheduleWeappSettleFlushCheck = React.useCallback(() => {
|
|
360
|
-
if (!isWeapp)
|
|
361
|
-
return;
|
|
362
|
-
if (settleCheckTimerRef.current)
|
|
363
|
-
return;
|
|
364
|
-
const check = () => {
|
|
365
|
-
settleCheckTimerRef.current = null;
|
|
366
|
-
const hasPending = pendingBumpRef.current || pendingMeasurementsRef.current.size > 0;
|
|
367
|
-
if (!hasPending || !isUserScrollingRef.current)
|
|
368
|
-
return;
|
|
369
|
-
const nowOffset = lastScrollTopRef.current;
|
|
370
|
-
const delta = Math.abs(nowOffset - settleLastOffsetRef.current);
|
|
371
|
-
settleLastOffsetRef.current = nowOffset;
|
|
372
|
-
if (delta <= 1) {
|
|
373
|
-
settleStillCountRef.current += 1;
|
|
374
|
-
}
|
|
375
|
-
else {
|
|
376
|
-
settleStillCountRef.current = 0;
|
|
377
|
-
}
|
|
378
|
-
const idleFor = Date.now() - lastScrollEventAtRef.current;
|
|
379
|
-
// 条件:连续两次小位移且最近 scroll 事件已进入静止窗口
|
|
380
|
-
if (settleStillCountRef.current >= 2 && idleFor >= 140) {
|
|
381
|
-
if (scrollTimeoutRef.current) {
|
|
382
|
-
clearTimeout(scrollTimeoutRef.current);
|
|
383
|
-
scrollTimeoutRef.current = null;
|
|
384
|
-
}
|
|
385
|
-
isScrollingRef.current = false;
|
|
386
|
-
isUserScrollingRef.current = false;
|
|
387
|
-
setIsUserScrolling(false);
|
|
388
|
-
settleStillCountRef.current = 0;
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
settleCheckTimerRef.current = setTimeout(check, 80);
|
|
392
|
-
};
|
|
393
|
-
settleLastOffsetRef.current = lastScrollTopRef.current;
|
|
394
|
-
settleStillCountRef.current = 0;
|
|
395
|
-
settleCheckTimerRef.current = setTimeout(check, 80);
|
|
396
|
-
}, [isWeapp]);
|
|
390
|
+
// (measureScrollProtectRef 已移除:scrollTop + 内容变更同帧 setData 会与 scrollAnchoring
|
|
391
|
+
// 冲突导致归顶;改由 scrollAnchoring=true 独立处理内容重排,无需显式传 scrollTop 保护)
|
|
397
392
|
// 动态尺寸缓存
|
|
398
393
|
const sizeCache = useItemSizeCache({
|
|
399
394
|
isHorizontal,
|
|
400
|
-
|
|
395
|
+
estimatedSize,
|
|
401
396
|
itemCount: totalItemCount
|
|
402
397
|
});
|
|
403
398
|
// header 动态尺寸缓存(sectionIndex -> size)
|
|
404
399
|
const headerSizeCacheRef = React.useRef(new Map());
|
|
405
|
-
//
|
|
400
|
+
// 滚动修正的可见起始索引
|
|
406
401
|
const visibleStartIndexRef = React.useRef(0);
|
|
407
|
-
// ScrollTop
|
|
408
|
-
// 小程序端禁用 scrollCorrection:
|
|
409
|
-
// 1. 其 100ms setTimeout 的闭包捕获的 renderOffset/setScrollOffset 可能过期,导致错误的程序性滚动
|
|
410
|
-
// 2. 小程序采用「延迟更新」策略(滚动中不 bump sizeCacheVersion),不需要实时修正
|
|
411
|
-
// 3. 其 setScrollOffset 调用 updateRenderOffset(true) 会触发 setData,可能干扰 scroll-view
|
|
402
|
+
// ScrollTop 修正(仅 H5):动高时尺寸变化自动修正 scrollTop
|
|
412
403
|
const scrollCorrectionEnabled = !isWeapp && props.useResizeObserver === true;
|
|
413
404
|
const scrollCorrection = useScrollCorrection({
|
|
414
405
|
enabled: scrollCorrectionEnabled,
|
|
415
|
-
|
|
406
|
+
visibleStartIndexRef,
|
|
416
407
|
setScrollOffset: (offsetOrUpdater) => {
|
|
417
408
|
const newOffset = typeof offsetOrUpdater === 'function'
|
|
418
409
|
? offsetOrUpdater(renderOffset)
|
|
419
410
|
: offsetOrUpdater;
|
|
420
|
-
updateRenderOffset(newOffset, true); // 程序性修正需同步到 ScrollView
|
|
411
|
+
updateRenderOffset(newOffset, true, 'scrollCorrection'); // 程序性修正需同步到 ScrollView
|
|
421
412
|
}
|
|
422
413
|
});
|
|
423
|
-
|
|
414
|
+
const scrollCorrectionRef = React.useRef(scrollCorrection);
|
|
415
|
+
scrollCorrectionRef.current = scrollCorrection;
|
|
416
|
+
const onScrollRef = React.useRef(onScroll);
|
|
417
|
+
onScrollRef.current = onScroll;
|
|
418
|
+
const onScrollToUpperRef = React.useRef(onScrollToUpper);
|
|
419
|
+
onScrollToUpperRef.current = onScrollToUpper;
|
|
420
|
+
const onScrollToLowerRef = React.useRef(onScrollToLower);
|
|
421
|
+
onScrollToLowerRef.current = onScrollToLower;
|
|
422
|
+
const thresholdRef = React.useRef({ upper: upperThreshold, lower: lowerThreshold });
|
|
423
|
+
thresholdRef.current = { upper: upperThreshold, lower: lowerThreshold };
|
|
424
|
+
const listContentLengthRef = React.useRef(0);
|
|
425
|
+
const inUpperZoneRef = React.useRef(true);
|
|
426
|
+
const inLowerZoneRef = React.useRef(false);
|
|
427
|
+
// 小程序 + 动高(virtual-list 风格):测量变化后同帧重排,不做程序性 scrollTop 回拉。
|
|
428
|
+
// 若用户正在滚动(惯性未结束),推迟到滚动停止后再触发,防止内容重排 + setData 同帧
|
|
429
|
+
// 导致 WeChat scrollAnchoring 失效或传滞后位置造成归顶。
|
|
424
430
|
const scheduleWeappDynamicReflow = React.useCallback(() => {
|
|
425
431
|
if (!isWeapp || props.useResizeObserver !== true)
|
|
426
432
|
return;
|
|
433
|
+
// 滚动中:仅标记为待处理,等 onScrollEnd / 200ms 超时触发
|
|
434
|
+
if (isUserScrollingRef.current) {
|
|
435
|
+
pendingWeappReflowRef.current = true;
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
427
438
|
if (sizeCacheRafRef.current != null)
|
|
428
439
|
return;
|
|
429
440
|
sizeCacheRafRef.current = requestAnimationFrame(() => {
|
|
430
441
|
sizeCacheRafRef.current = null;
|
|
431
|
-
|
|
442
|
+
pendingWeappReflowRef.current = false;
|
|
432
443
|
setSizeCacheVersion((v) => v + 1);
|
|
433
444
|
});
|
|
434
445
|
}, [isWeapp, props.useResizeObserver]);
|
|
446
|
+
// 每次渲染时更新 ref,保证 setTimeout 闭包内拿到最新版本
|
|
447
|
+
scheduleWeappDynamicReflowRef.current = scheduleWeappDynamicReflow;
|
|
435
448
|
// ResizeObserver(当启用动态测量时)
|
|
436
449
|
const resizeObserver = useResizeObserver({
|
|
437
450
|
enabled: props.useResizeObserver === true,
|
|
438
451
|
isHorizontal,
|
|
439
452
|
listId,
|
|
453
|
+
selectorQueryScope: props.selectorQueryScope,
|
|
440
454
|
onResize: (index, size) => {
|
|
441
455
|
var _a;
|
|
442
456
|
const oldSize = sizeCache.getItemSize(index);
|
|
443
457
|
sizeCache.setItemSize(index, size);
|
|
444
|
-
// 仅当尺寸实际变化(≥1px)时才批量驱动重渲染,避免重复/微小变化导致卡顿
|
|
445
458
|
if (Math.abs(oldSize - size) >= 1) {
|
|
446
459
|
if (isWeapp && props.useResizeObserver === true) {
|
|
447
460
|
scheduleWeappDynamicReflow();
|
|
448
461
|
}
|
|
449
|
-
else {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
}
|
|
455
|
-
if (sizeCacheRafRef.current == null) {
|
|
456
|
-
sizeCacheRafRef.current = requestAnimationFrame(() => {
|
|
457
|
-
sizeCacheRafRef.current = null;
|
|
458
|
-
if (isWeapp) {
|
|
459
|
-
weappSizeCacheVersionBump(isUserScrollingRef, pendingBumpRef, measureScrollProtectRef, setSizeCacheVersion);
|
|
460
|
-
}
|
|
461
|
-
else {
|
|
462
|
-
setSizeCacheVersion((v) => v + 1);
|
|
463
|
-
}
|
|
464
|
-
});
|
|
465
|
-
}
|
|
462
|
+
else if (sizeCacheRafRef.current == null) {
|
|
463
|
+
sizeCacheRafRef.current = requestAnimationFrame(() => {
|
|
464
|
+
sizeCacheRafRef.current = null;
|
|
465
|
+
setSizeCacheVersion((v) => v + 1);
|
|
466
|
+
});
|
|
466
467
|
}
|
|
467
468
|
}
|
|
468
469
|
// 触发 ScrollTop 修正
|
|
@@ -470,92 +471,70 @@ const InnerList = (props, ref) => {
|
|
|
470
471
|
// 小程序:延迟 onItemSizeChange,避免父组件重渲染导致 List remount
|
|
471
472
|
// H5:直接回调
|
|
472
473
|
if (isWeapp) {
|
|
473
|
-
weappDeferItemSizeChange(
|
|
474
|
+
weappDeferItemSizeChange(index, size, props.onItemSizeChange);
|
|
474
475
|
}
|
|
475
476
|
else {
|
|
476
477
|
(_a = props.onItemSizeChange) === null || _a === void 0 ? void 0 : _a.call(props, index, size);
|
|
477
478
|
}
|
|
478
479
|
}
|
|
479
480
|
});
|
|
480
|
-
//
|
|
481
|
-
const
|
|
482
|
-
if (
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
481
|
+
// 嵌套滚动:内层高度变化上报
|
|
482
|
+
const handleReportNestedHeightChange = React.useCallback((height, index) => {
|
|
483
|
+
if (height <= 0)
|
|
484
|
+
return;
|
|
485
|
+
const estimatedHeader = estimatedSize * 0.5;
|
|
486
|
+
const fullHeight = height + estimatedHeader;
|
|
487
|
+
const oldSize = sizeCache.getItemSize(index);
|
|
488
|
+
if (Math.abs(oldSize - fullHeight) < 1)
|
|
489
|
+
return;
|
|
490
|
+
sizeCache.setItemSize(index, fullHeight);
|
|
491
|
+
scrollCorrection.recordSizeChange(index, oldSize, fullHeight);
|
|
492
|
+
if (isWeapp && props.useResizeObserver === true) {
|
|
493
|
+
scheduleWeappDynamicReflow();
|
|
492
494
|
}
|
|
493
|
-
else {
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
if (typeof props.itemSize === 'number')
|
|
499
|
-
return props.itemSize;
|
|
500
|
-
if (typeof props.itemSize === 'function')
|
|
501
|
-
return props.itemSize(0, props.itemData) || DEFAULT_ITEM_HEIGHT;
|
|
502
|
-
return DEFAULT_ITEM_HEIGHT;
|
|
495
|
+
else if (sizeCacheRafRef.current == null) {
|
|
496
|
+
sizeCacheRafRef.current = requestAnimationFrame(() => {
|
|
497
|
+
sizeCacheRafRef.current = null;
|
|
498
|
+
setSizeCacheVersion((v) => v + 1);
|
|
499
|
+
});
|
|
503
500
|
}
|
|
504
|
-
};
|
|
505
|
-
|
|
501
|
+
}, [sizeCache, scrollCorrection, estimatedSize, isWeapp, props.useResizeObserver, scheduleWeappDynamicReflow]);
|
|
502
|
+
const getDefaultHeaderSize = React.useCallback(() => {
|
|
503
|
+
const headerSize = normalizeSize(props.headerSize);
|
|
504
|
+
if (headerSize != null)
|
|
505
|
+
return headerSize;
|
|
506
|
+
return resolveItemSizeByIndex(0, defaultItemSize);
|
|
507
|
+
}, [props.headerSize, resolveItemSizeByIndex, defaultItemSize, normalizeSize]);
|
|
508
|
+
// 获取 header 尺寸(支持动态测量)
|
|
506
509
|
const getHeaderSize = React.useCallback((sectionIndex) => {
|
|
507
|
-
// 如果启用动态测量,优先从缓存读取
|
|
508
510
|
if (props.useResizeObserver === true) {
|
|
509
511
|
const cached = headerSizeCacheRef.current.get(sectionIndex);
|
|
510
512
|
if (cached != null && cached > 0)
|
|
511
513
|
return cached;
|
|
512
514
|
}
|
|
513
|
-
// 否则返回默认尺寸
|
|
514
515
|
return getDefaultHeaderSize();
|
|
515
|
-
}, [props.useResizeObserver,
|
|
516
|
-
// 工具:获取 item 尺寸,支持函数/props/默认值/动态测量
|
|
516
|
+
}, [props.useResizeObserver, getDefaultHeaderSize]);
|
|
517
517
|
const getItemSize = React.useCallback((index) => {
|
|
518
|
-
// 优先级1:如果启用动态测量,从缓存读取
|
|
519
518
|
if (props.useResizeObserver === true) {
|
|
520
519
|
return sizeCache.getItemSize(index);
|
|
521
520
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
return props.itemWidth;
|
|
526
|
-
if (typeof props.itemSize === 'number')
|
|
527
|
-
return props.itemSize;
|
|
528
|
-
if (typeof props.itemSize === 'function')
|
|
529
|
-
return props.itemSize(index, props.itemData) || DEFAULT_ITEM_WIDTH;
|
|
530
|
-
return DEFAULT_ITEM_WIDTH;
|
|
531
|
-
}
|
|
532
|
-
else {
|
|
533
|
-
if (typeof props.itemHeight === 'number')
|
|
534
|
-
return props.itemHeight;
|
|
535
|
-
if (typeof props.itemSize === 'number')
|
|
536
|
-
return props.itemSize;
|
|
537
|
-
if (typeof props.itemSize === 'function')
|
|
538
|
-
return props.itemSize(index, props.itemData) || DEFAULT_ITEM_HEIGHT;
|
|
539
|
-
return DEFAULT_ITEM_HEIGHT;
|
|
540
|
-
}
|
|
541
|
-
}, [props.useResizeObserver, props.itemWidth, props.itemHeight, props.itemSize, props.itemData, isHorizontal, sizeCache]);
|
|
542
|
-
// 计算分组累积高度/宽度(依赖 sizeCacheVersion,动态测量更新后重算,保证最外层容器高度与 item 定位正确)
|
|
521
|
+
return resolveItemSizeByIndex(index, defaultItemSize);
|
|
522
|
+
}, [props.useResizeObserver, sizeCache, resolveItemSizeByIndex, defaultItemSize]);
|
|
523
|
+
// 分组累积高度/宽度,sizeCacheVersion 变化时重算
|
|
543
524
|
const sectionOffsets = React.useMemo(() => {
|
|
544
525
|
const offsets = [0];
|
|
545
|
-
let globalItemIndex = 0;
|
|
526
|
+
let globalItemIndex = 0;
|
|
546
527
|
sections.forEach((section, sectionIdx) => {
|
|
547
528
|
const headerSize = getHeaderSize(sectionIdx);
|
|
548
|
-
// 使用全局索引计算每个 item 的尺寸
|
|
549
529
|
const itemSizes = section.items.map((_, localIdx) => getItemSize(globalItemIndex + localIdx));
|
|
550
530
|
const groupSize = (section.header ? headerSize : 0) +
|
|
551
531
|
itemSizes.reduce((a, b) => a + b, 0) +
|
|
552
532
|
Math.max(0, section.items.length) * space;
|
|
553
533
|
offsets.push(offsets[offsets.length - 1] + groupSize);
|
|
554
|
-
// 累加当前 section 的 item 数量
|
|
555
534
|
globalItemIndex += section.items.length;
|
|
556
535
|
});
|
|
557
536
|
return offsets;
|
|
558
|
-
}, [sections, space,
|
|
537
|
+
}, [sections, space, getItemSize, getHeaderSize, sizeCacheVersion]);
|
|
559
538
|
// 外层虚拟滚动:可见分组
|
|
560
539
|
const [startSection, endSection] = React.useMemo(() => {
|
|
561
540
|
let start = 0;
|
|
@@ -574,8 +553,7 @@ const InnerList = (props, ref) => {
|
|
|
574
553
|
}
|
|
575
554
|
return [start, end];
|
|
576
555
|
}, [renderOffset, containerLength, sectionOffsets, sections.length, cacheCount]);
|
|
577
|
-
//
|
|
578
|
-
// 按视口 [renderOffset, renderOffset + containerLength] 精确计算,而非「可见 section 的整段 item」
|
|
556
|
+
// 视口内可见 item 的全局索引范围(供 onScrollIndex)
|
|
579
557
|
const [visibleStartItem, visibleEndItem] = React.useMemo(() => {
|
|
580
558
|
const viewportTop = renderOffset;
|
|
581
559
|
const viewportBottom = renderOffset + containerLength;
|
|
@@ -605,11 +583,9 @@ const InnerList = (props, ref) => {
|
|
|
605
583
|
return [0, 0];
|
|
606
584
|
return [firstVisible, lastVisible];
|
|
607
585
|
}, [renderOffset, containerLength, sections, sectionOffsets, getHeaderSize, getItemSize, space]);
|
|
608
|
-
// 触发 onScrollIndex 回调(带防重复)
|
|
609
586
|
const lastVisibleRangeRef = React.useRef({ start: -1, end: -1 });
|
|
610
587
|
React.useEffect(() => {
|
|
611
588
|
if (props.onScrollIndex) {
|
|
612
|
-
// 避免重复触发
|
|
613
589
|
if (lastVisibleRangeRef.current.start !== visibleStartItem ||
|
|
614
590
|
lastVisibleRangeRef.current.end !== visibleEndItem) {
|
|
615
591
|
lastVisibleRangeRef.current = { start: visibleStartItem, end: visibleEndItem };
|
|
@@ -617,18 +593,8 @@ const InnerList = (props, ref) => {
|
|
|
617
593
|
}
|
|
618
594
|
}
|
|
619
595
|
}, [visibleStartItem, visibleEndItem, props.onScrollIndex]);
|
|
620
|
-
// 触顶/触底事件
|
|
621
|
-
React.useEffect(() => {
|
|
622
|
-
if (onScrollToUpper && renderOffset <= (upperThreshold > 0 ? sectionOffsets[upperThreshold] : 0)) {
|
|
623
|
-
onScrollToUpper();
|
|
624
|
-
}
|
|
625
|
-
if (onScrollToLower && renderOffset + containerLength >= sectionOffsets[sectionOffsets.length - 1] - (lowerThreshold > 0 ? sectionOffsets[sectionOffsets.length - 1] - sectionOffsets[sections.length - lowerThreshold] : 0)) {
|
|
626
|
-
onScrollToLower();
|
|
627
|
-
}
|
|
628
|
-
}, [renderOffset, containerLength, sectionOffsets, sections.length, upperThreshold, lowerThreshold, onScrollToUpper, onScrollToLower]);
|
|
629
|
-
// 智能滚动处理函数(H5 下拉刷新时 scrollTop 含 Refresher 区,虚拟列表用列表内偏移)
|
|
630
596
|
const handleScroll = React.useCallback((e) => {
|
|
631
|
-
var _a, _b;
|
|
597
|
+
var _a, _b, _c, _d;
|
|
632
598
|
let newOffset;
|
|
633
599
|
if (e.detail) {
|
|
634
600
|
newOffset = isHorizontal ? e.detail.scrollLeft : e.detail.scrollTop;
|
|
@@ -636,197 +602,108 @@ const InnerList = (props, ref) => {
|
|
|
636
602
|
else {
|
|
637
603
|
newOffset = isHorizontal ? e.scrollLeft : e.scrollTop;
|
|
638
604
|
}
|
|
639
|
-
// H5 顶栏悬浮:只滚列表,scrollTop 即列表偏移,无需 clamp
|
|
640
605
|
const effectiveOffset = newOffset;
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
if (isWeapp &&
|
|
654
|
-
props.useResizeObserver !== true &&
|
|
655
|
-
(scrollRestoreRef.current !== null || weappFlushRestorePendingRef.current) &&
|
|
656
|
-
effectiveOffset <= 1) {
|
|
657
|
-
return;
|
|
658
|
-
}
|
|
659
|
-
// 小程序端:程序性恢复后的短窗口内,过滤异常“回顶/大幅反向跳变”噪声事件
|
|
660
|
-
// 动高分支改走 virtual-list 风格,不在这里做程序性回拉。
|
|
661
|
-
if (isWeapp && props.useResizeObserver !== true && suppressResetUntilRef.current > Date.now()) {
|
|
662
|
-
const target = (_b = programmaticTargetOffsetRef.current) !== null && _b !== void 0 ? _b : lastScrollTopRef.current;
|
|
663
|
-
const isResetToTop = effectiveOffset <= 1 && target > 40;
|
|
664
|
-
if (isResetToTop) {
|
|
665
|
-
const now = Date.now();
|
|
666
|
-
// 仅“忽略”会导致原生位置与虚拟渲染位置脱节(空白)。
|
|
667
|
-
// 这里改为回补一次目标位置,保持两者一致。
|
|
668
|
-
if (now - guardHealAtRef.current > 120) {
|
|
669
|
-
guardHealAtRef.current = now;
|
|
670
|
-
updateRenderOffset(target, !isUserScrollingRef.current);
|
|
671
|
-
}
|
|
672
|
-
return;
|
|
673
|
-
}
|
|
674
|
-
}
|
|
606
|
+
const currentThreshold = thresholdRef.current;
|
|
607
|
+
const { upper, lower } = currentThreshold;
|
|
608
|
+
// WeChat 小程序节点不具备 clientHeight/clientWidth,需显式回退到 containerLength state
|
|
609
|
+
const rawContainerSize = containerRef.current
|
|
610
|
+
? (isHorizontal ? containerRef.current.clientWidth : containerRef.current.clientHeight)
|
|
611
|
+
: null;
|
|
612
|
+
const currentContainerLength = (typeof rawContainerSize === 'number' && rawContainerSize > 0)
|
|
613
|
+
? rawContainerSize
|
|
614
|
+
: containerLength;
|
|
615
|
+
const nowInUpper = effectiveOffset <= upper;
|
|
616
|
+
const currentContentLength = listContentLengthRef.current;
|
|
617
|
+
const nowInLower = currentContentLength > 0 && effectiveOffset + currentContainerLength >= currentContentLength - lower;
|
|
675
618
|
const diff = effectiveOffset - lastScrollTopRef.current;
|
|
676
619
|
scrollDiffListRef.current.shift();
|
|
677
620
|
scrollDiffListRef.current.push(diff);
|
|
678
|
-
|
|
621
|
+
const shaking = isScrollingRef.current && isShaking(scrollDiffListRef.current);
|
|
622
|
+
if (shaking) {
|
|
679
623
|
return;
|
|
680
624
|
}
|
|
681
|
-
// 小程序端程序性滚动冷却期内:只更新 renderOffset 用于虚拟列表渲染,不触发同步逻辑,避免被"拉回"
|
|
682
|
-
// 注意:programmaticCooldownRef 只在小程序端会被设为 true,H5 端不受影响
|
|
683
625
|
if (programmaticCooldownRef.current) {
|
|
684
626
|
lastScrollTopRef.current = effectiveOffset;
|
|
685
627
|
setRenderOffset(effectiveOffset);
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
628
|
+
const scrollTop = isHorizontal ? 0 : newOffset;
|
|
629
|
+
const scrollLeft = isHorizontal ? newOffset : 0;
|
|
630
|
+
onScroll === null || onScroll === void 0 ? void 0 : onScroll({ scrollTop, scrollLeft, detail: { scrollTop, scrollLeft } });
|
|
631
|
+
if (nowInUpper && !inUpperZoneRef.current)
|
|
632
|
+
(_a = onScrollToUpperRef.current) === null || _a === void 0 ? void 0 : _a.call(onScrollToUpperRef);
|
|
633
|
+
if (nowInLower && !inLowerZoneRef.current)
|
|
634
|
+
(_b = onScrollToLowerRef.current) === null || _b === void 0 ? void 0 : _b.call(onScrollToLowerRef);
|
|
635
|
+
inUpperZoneRef.current = nowInUpper;
|
|
636
|
+
inLowerZoneRef.current = nowInLower;
|
|
690
637
|
return;
|
|
691
638
|
}
|
|
692
639
|
scrollCorrection.markUserScrolling();
|
|
693
|
-
updateRenderOffset(effectiveOffset, false);
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
640
|
+
updateRenderOffset(effectiveOffset, false, 'onScroll');
|
|
641
|
+
const scrollTop = isHorizontal ? 0 : newOffset;
|
|
642
|
+
const scrollLeft = isHorizontal ? newOffset : 0;
|
|
643
|
+
onScroll === null || onScroll === void 0 ? void 0 : onScroll({ scrollTop, scrollLeft, detail: { scrollTop, scrollLeft } });
|
|
644
|
+
if (nowInUpper && !inUpperZoneRef.current)
|
|
645
|
+
(_c = onScrollToUpperRef.current) === null || _c === void 0 ? void 0 : _c.call(onScrollToUpperRef);
|
|
646
|
+
if (nowInLower && !inLowerZoneRef.current)
|
|
647
|
+
(_d = onScrollToLowerRef.current) === null || _d === void 0 ? void 0 : _d.call(onScrollToLowerRef);
|
|
648
|
+
inUpperZoneRef.current = nowInUpper;
|
|
649
|
+
inLowerZoneRef.current = nowInLower;
|
|
650
|
+
}, [isHorizontal, onScroll, updateRenderOffset, scrollCorrection, props.useResizeObserver, containerLength]);
|
|
651
|
+
// 小程序:onScrollEnd 优先结束 isUserScrolling,timeout 兜底
|
|
701
652
|
const handleNativeScrollEnd = React.useCallback(() => {
|
|
702
653
|
if (isWeapp) {
|
|
703
654
|
if (scrollTimeoutRef.current) {
|
|
704
655
|
clearTimeout(scrollTimeoutRef.current);
|
|
705
656
|
scrollTimeoutRef.current = null;
|
|
706
657
|
}
|
|
707
|
-
if (
|
|
708
|
-
clearTimeout(settleCheckTimerRef.current);
|
|
709
|
-
settleCheckTimerRef.current = null;
|
|
710
|
-
}
|
|
711
|
-
const hasPending = props.useResizeObserver === true
|
|
712
|
-
? false
|
|
713
|
-
: (pendingBumpRef.current || pendingMeasurementsRef.current.size > 0);
|
|
714
|
-
if (isUserScrollingRef.current || hasPending) {
|
|
658
|
+
if (isUserScrollingRef.current) {
|
|
715
659
|
isScrollingRef.current = false;
|
|
716
660
|
isUserScrollingRef.current = false;
|
|
717
661
|
setIsUserScrolling(false);
|
|
662
|
+
// 滚动结束后触发期间被延迟的 reflow(item 首次测量触发)
|
|
663
|
+
if (pendingWeappReflowRef.current) {
|
|
664
|
+
scheduleWeappDynamicReflowRef.current();
|
|
665
|
+
}
|
|
718
666
|
}
|
|
719
667
|
}
|
|
720
668
|
onScrollEnd === null || onScrollEnd === void 0 ? void 0 : onScrollEnd();
|
|
721
669
|
}, [isWeapp, onScrollEnd]);
|
|
722
|
-
//
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
pendingBumpRef.current = false;
|
|
733
|
-
if (pendingMeasurementsRef.current.size > 0) {
|
|
734
|
-
pendingMeasurementsRef.current.clear();
|
|
735
|
-
}
|
|
736
|
-
return;
|
|
737
|
-
}
|
|
738
|
-
const hasPendingMeasurements = pendingMeasurementsRef.current.size > 0;
|
|
739
|
-
if (pendingBumpRef.current || hasPendingMeasurements) {
|
|
740
|
-
pendingBumpRef.current = false;
|
|
741
|
-
// 计算视口上方 item 高度变化的累积 delta
|
|
742
|
-
// visibleStartItem = 当前视口第一个可见 item(旧布局,因为 sizeCacheVersion 还没 bump)
|
|
743
|
-
let heightDelta = 0;
|
|
744
|
-
pendingMeasurementsRef.current.forEach(({ originalOldSize, latestNewSize }, index) => {
|
|
745
|
-
if (index < visibleStartItem) {
|
|
746
|
-
heightDelta += (latestNewSize - originalOldSize);
|
|
747
|
-
}
|
|
748
|
-
});
|
|
749
|
-
pendingMeasurementsRef.current.clear();
|
|
750
|
-
// 修正后的 scrollTop = 旧 scrollTop + 视口上方高度变化量(定高 / 非动高路径)
|
|
751
|
-
const restoreFrom = lastScrollTopRef.current;
|
|
752
|
-
const correctedScrollTop = restoreFrom + heightDelta;
|
|
753
|
-
scrollRestoreFromRef.current = restoreFrom;
|
|
754
|
-
scrollRestoreRef.current = correctedScrollTop;
|
|
755
|
-
weappFlushRestorePendingRef.current = true;
|
|
756
|
-
measureScrollProtectRef.current = true;
|
|
757
|
-
setSizeCacheVersion((v) => v + 1);
|
|
758
|
-
}
|
|
759
|
-
else {
|
|
760
|
-
// 没有 pending bump 但可能有残留的测量记录(scrolling 结束但没有新测量),清理
|
|
761
|
-
if (pendingMeasurementsRef.current.size > 0) {
|
|
762
|
-
pendingMeasurementsRef.current.clear();
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
// flush 延迟的 onItemSizeChange(暂时禁用,验证 scroll-anchoring)
|
|
766
|
-
// const pending = pendingSizeChangesRef.current
|
|
767
|
-
// if (pending.length > 0) {
|
|
768
|
-
// pendingSizeChangesRef.current = []
|
|
769
|
-
// pending.forEach(({ index, size }) => props.onItemSizeChange?.(index, size))
|
|
770
|
-
// }
|
|
771
|
-
}
|
|
772
|
-
}, [isUserScrolling, props.useResizeObserver]);
|
|
773
|
-
// scrollRestoreEffect:sizeCacheVersion 变化后,在下一帧单独恢复 scroll 位置
|
|
774
|
-
// 核心思想:第一次 setData 更新 children(不带 scroll-top),
|
|
775
|
-
// 第二次 setData 仅恢复 scroll-top(children 不变),避免同批冲突
|
|
776
|
-
React.useEffect(() => {
|
|
777
|
-
if (props.useResizeObserver === true)
|
|
778
|
-
return;
|
|
779
|
-
if (scrollRestoreRef.current === null) {
|
|
780
|
-
weappFlushRestorePendingRef.current = false;
|
|
781
|
-
return;
|
|
782
|
-
}
|
|
783
|
-
const targetPos = scrollRestoreRef.current;
|
|
784
|
-
const sourcePos = scrollRestoreFromRef.current;
|
|
785
|
-
scrollRestoreRef.current = null;
|
|
786
|
-
scrollRestoreFromRef.current = null;
|
|
787
|
-
// 使用 Taro.nextTick 确保在上一次 setData 完成后执行
|
|
788
|
-
Taro.nextTick(() => {
|
|
789
|
-
try {
|
|
790
|
-
// 若用户已开始下一次滚动,则取消这次过期 restore,避免回拉/回顶/空白
|
|
791
|
-
const userContinued = sourcePos != null &&
|
|
792
|
-
isUserScrollingRef.current &&
|
|
793
|
-
Math.abs(lastScrollTopRef.current - sourcePos) > 8;
|
|
794
|
-
if (userContinued) {
|
|
795
|
-
return;
|
|
796
|
-
}
|
|
797
|
-
updateRenderOffset(targetPos, true);
|
|
798
|
-
}
|
|
799
|
-
finally {
|
|
800
|
-
weappFlushRestorePendingRef.current = false;
|
|
801
|
-
}
|
|
802
|
-
});
|
|
803
|
-
}, [sizeCacheVersion, updateRenderOffset]);
|
|
804
|
-
// 小程序端:保持稳定优先,暂不回放 onItemSizeChange。
|
|
805
|
-
React.useEffect(() => {
|
|
806
|
-
pendingSizeChangesRef.current = [];
|
|
807
|
-
}, [isUserScrolling, sizeCacheVersion]);
|
|
808
|
-
}
|
|
809
|
-
// 初始化后的延迟同步 - 确保 ScrollView 与虚拟列表窗口一致(scrollTop 受控跳转)
|
|
670
|
+
// 暴露 scrollRef 给父组件,供内层 List/WaterFlow 传入 scrollElement
|
|
671
|
+
React.useLayoutEffect(() => {
|
|
672
|
+
if (!scrollRefProp)
|
|
673
|
+
return;
|
|
674
|
+
const el = useScrollElementMode ? effectiveScrollElement === null || effectiveScrollElement === void 0 ? void 0 : effectiveScrollElement.current : containerRef.current;
|
|
675
|
+
if (el) {
|
|
676
|
+
scrollRefProp.current = el;
|
|
677
|
+
}
|
|
678
|
+
}, [scrollRefProp, useScrollElementMode, effectiveScrollElement]);
|
|
679
|
+
// controlledScrollTop 变化时同步到 ScrollView
|
|
810
680
|
React.useEffect(() => {
|
|
811
681
|
if (typeof controlledScrollTop === 'number') {
|
|
682
|
+
const sv = scrollViewOffsetRef.current;
|
|
683
|
+
const same = isWeapp && Math.abs(sv - controlledScrollTop) < 1;
|
|
684
|
+
if (same) {
|
|
685
|
+
const intermediate = controlledScrollTop > 0 ? controlledScrollTop - 0.01 : 0.01;
|
|
686
|
+
setRenderOffset(intermediate);
|
|
687
|
+
setScrollViewOffset(intermediate);
|
|
688
|
+
requestAnimationFrame(() => {
|
|
689
|
+
setRenderOffset(controlledScrollTop);
|
|
690
|
+
setScrollViewOffset(controlledScrollTop);
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
else {
|
|
694
|
+
setRenderOffset(controlledScrollTop);
|
|
695
|
+
setScrollViewOffset(controlledScrollTop);
|
|
696
|
+
}
|
|
812
697
|
lastScrollTopRef.current = controlledScrollTop;
|
|
813
|
-
setRenderOffset(controlledScrollTop);
|
|
814
|
-
setScrollViewOffset(controlledScrollTop);
|
|
815
698
|
programmaticScrollRef.current = true;
|
|
816
699
|
}
|
|
817
700
|
}, [controlledScrollTop]);
|
|
818
701
|
// 清理定时器、ResizeObserver 与尺寸缓存 RAF(仅在卸载时执行)
|
|
819
|
-
// 注意:不要依赖整个对象(resizeObserver/scrollCorrection),否则每次渲染都会触发 cleanup,
|
|
820
|
-
// 进而反复清掉 pendingSizeFlushTimer,导致 onItemSizeChange 批量回放永远不执行。
|
|
821
702
|
React.useEffect(() => {
|
|
822
703
|
return () => {
|
|
823
704
|
if (scrollTimeoutRef.current) {
|
|
824
705
|
clearTimeout(scrollTimeoutRef.current);
|
|
825
706
|
}
|
|
826
|
-
if (settleCheckTimerRef.current) {
|
|
827
|
-
clearTimeout(settleCheckTimerRef.current);
|
|
828
|
-
settleCheckTimerRef.current = null;
|
|
829
|
-
}
|
|
830
707
|
// 小程序端冷却期定时器清理
|
|
831
708
|
if (isWeapp && programmaticCooldownTimerRef.current) {
|
|
832
709
|
clearTimeout(programmaticCooldownTimerRef.current);
|
|
@@ -838,7 +715,7 @@ const InnerList = (props, ref) => {
|
|
|
838
715
|
resizeObserver.disconnect();
|
|
839
716
|
scrollCorrection.clearQueue();
|
|
840
717
|
};
|
|
841
|
-
}, [
|
|
718
|
+
}, []);
|
|
842
719
|
// scrollIntoView:仅当 scrollIntoView 变化时执行一次跳动,统一转换为 scrollTop 路径(updateRenderOffset)。
|
|
843
720
|
const lastScrollIntoViewRef = React.useRef(null);
|
|
844
721
|
React.useEffect(() => {
|
|
@@ -875,34 +752,65 @@ const InnerList = (props, ref) => {
|
|
|
875
752
|
currentGlobalIndex += section.items.length;
|
|
876
753
|
}
|
|
877
754
|
}
|
|
878
|
-
|
|
755
|
+
const el = effectiveScrollElement === null || effectiveScrollElement === void 0 ? void 0 : effectiveScrollElement.current;
|
|
756
|
+
if (useScrollElementMode && el) {
|
|
757
|
+
const scrollTarget = targetOffset + (isWeapp ? effectiveStartOffsetRef.current : effectiveStartOffset);
|
|
758
|
+
if (isH5) {
|
|
759
|
+
if (isHorizontal) {
|
|
760
|
+
el.scrollTo({ left: scrollTarget });
|
|
761
|
+
}
|
|
762
|
+
else {
|
|
763
|
+
el.scrollTo({ top: scrollTarget });
|
|
764
|
+
}
|
|
765
|
+
updateRenderOffset(targetOffset, true, 'scrollIntoView');
|
|
766
|
+
}
|
|
767
|
+
else if (isWeapp) {
|
|
768
|
+
const scrollViewId = el.id || `_ls_${listId}`;
|
|
769
|
+
if (!el.id)
|
|
770
|
+
el.id = scrollViewId;
|
|
771
|
+
getScrollViewContextNode(`#${scrollViewId}`).then((node) => {
|
|
772
|
+
var _a, _b;
|
|
773
|
+
if (isHorizontal) {
|
|
774
|
+
(_a = node === null || node === void 0 ? void 0 : node.scrollTo) === null || _a === void 0 ? void 0 : _a.call(node, { left: scrollTarget, animated: false });
|
|
775
|
+
}
|
|
776
|
+
else {
|
|
777
|
+
(_b = node === null || node === void 0 ? void 0 : node.scrollTo) === null || _b === void 0 ? void 0 : _b.call(node, { top: scrollTarget, animated: false });
|
|
778
|
+
}
|
|
779
|
+
updateRenderOffset(targetOffset, true, 'scrollIntoView');
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
else {
|
|
783
|
+
updateRenderOffset(targetOffset, true, 'scrollIntoView');
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
else {
|
|
787
|
+
updateRenderOffset(targetOffset, true, 'scrollIntoView');
|
|
788
|
+
}
|
|
879
789
|
}
|
|
880
|
-
}, [props.scrollIntoView, totalItemCount, sections, getHeaderSize, getItemSize, space, updateRenderOffset]);
|
|
881
|
-
//
|
|
790
|
+
}, [props.scrollIntoView, totalItemCount, sections, getHeaderSize, getItemSize, space, updateRenderOffset, useScrollElementMode, effectiveScrollElement, effectiveStartOffset, effectiveStartOffsetRef, isH5, isWeapp, isHorizontal, listId]);
|
|
791
|
+
// 容器样式;H5 刷新中禁止滚动
|
|
882
792
|
const containerStyle = Object.assign(Object.assign({ position: 'relative', boxSizing: 'border-box', height,
|
|
883
793
|
width }, style), (isH5 && refresherConfig && !supportsNativeRefresher && h5RefresherProps.isRefreshing
|
|
884
794
|
? { overflow: 'hidden' }
|
|
885
795
|
: {}));
|
|
886
|
-
// ScrollView
|
|
887
|
-
const scrollViewProps = Object.assign(Object.assign(Object.assign(Object.assign(
|
|
796
|
+
// ScrollView 属性
|
|
797
|
+
const scrollViewProps = Object.assign(Object.assign(Object.assign(Object.assign({ scrollY: !scrollX && scrollY, scrollX, style: containerStyle, className, enhanced: true, showScrollbar,
|
|
888
798
|
upperThreshold,
|
|
889
|
-
lowerThreshold, scrollWithAnimation: false, onScroll: handleScroll,
|
|
890
|
-
onScrollToLower,
|
|
891
|
-
onScrollStart, onScrollEnd: handleNativeScrollEnd, enableBackToTop }, (isWeapp ? { scrollAnchoring: true } : {})), (typeof cacheExtent === 'number' ? { cacheExtent } : {})), { 'data-testid': 'taro-list-container' }), scrollViewRefresherProps), scrollViewRefresherHandlers), (!supportsNativeRefresher && refresherConfig && !addImperativeTouchListeners ? h5RefresherProps.touchHandlers : {}));
|
|
799
|
+
lowerThreshold, scrollWithAnimation: false, onScroll: handleScroll, onScrollStart, onScrollEnd: handleNativeScrollEnd, enableBackToTop }, (isWeapp ? { scrollAnchoring: true } : {})), (typeof cacheExtent === 'number' ? { cacheExtent } : {})), scrollViewRefresherProps), scrollViewRefresherHandlers);
|
|
892
800
|
// H5 对齐小程序:refresherTriggered=true 时顶部立即显示加载指示器,滚到顶部并锁定滚动直至设为 false
|
|
893
801
|
React.useEffect(() => {
|
|
894
802
|
if (!isH5 || !refresherConfig || supportsNativeRefresher || !h5RefresherProps.isRefreshing)
|
|
895
803
|
return;
|
|
896
|
-
updateRenderOffset(0, true);
|
|
804
|
+
updateRenderOffset(0, true, 'refresher');
|
|
897
805
|
}, [h5RefresherProps.isRefreshing, isH5, refresherConfig, supportsNativeRefresher, updateRenderOffset]);
|
|
898
|
-
// H5
|
|
806
|
+
// H5 下拉刷新:ref 存 addImperativeTouchListeners,避免 config 变化导致 effect 循环
|
|
899
807
|
React.useLayoutEffect(() => {
|
|
900
808
|
const attach = addImperativeTouchListenersRef.current;
|
|
901
809
|
if (!attach)
|
|
902
810
|
return;
|
|
903
811
|
let teardown = null;
|
|
904
812
|
const tryAttach = () => {
|
|
905
|
-
const el = containerRef.current;
|
|
813
|
+
const el = useScrollElementMode ? effectiveScrollElement === null || effectiveScrollElement === void 0 ? void 0 : effectiveScrollElement.current : containerRef.current;
|
|
906
814
|
if (el && !refresherTeardownRef.current) {
|
|
907
815
|
teardown = attach(el);
|
|
908
816
|
refresherTeardownRef.current = teardown;
|
|
@@ -917,28 +825,16 @@ const InnerList = (props, ref) => {
|
|
|
917
825
|
}
|
|
918
826
|
refresherTeardownRef.current = null;
|
|
919
827
|
};
|
|
920
|
-
}, []);
|
|
921
|
-
|
|
922
|
-
//
|
|
923
|
-
//
|
|
924
|
-
//
|
|
828
|
+
}, [useScrollElementMode, effectiveScrollElement]);
|
|
829
|
+
scrollViewOffsetRef.current = scrollViewOffset;
|
|
830
|
+
// scrollTop 传递策略(对齐 virtual-list enhanced=true 的做法):
|
|
831
|
+
// - 程序性滚动帧(programmaticScrollRef=true):传 scrollViewOffset(目标位置)
|
|
832
|
+
// - 其余所有帧(用户滚动 / 测量重排 / 空闲):完全不传,依赖微信原生 + scrollAnchoring
|
|
833
|
+
// scrollAnchoring=true 负责在内容重排时自动维持视口位置,避免与显式 scrollTop
|
|
834
|
+
// 同帧冲突(冲突会导致 scrollAnchoring 失去锚点后归顶)
|
|
925
835
|
if (isWeapp) {
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
measureScrollProtectRef.current = false;
|
|
929
|
-
}
|
|
930
|
-
else if (isWeappDynamicHeight) {
|
|
931
|
-
// 动高:仅程序性滚动时传,其他时刻由原生滚动主导
|
|
932
|
-
if (programmaticScrollRef.current) {
|
|
933
|
-
if (isHorizontal)
|
|
934
|
-
scrollViewProps.scrollLeft = scrollViewOffset;
|
|
935
|
-
else
|
|
936
|
-
scrollViewProps.scrollTop = scrollViewOffset;
|
|
937
|
-
programmaticScrollRef.current = false;
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
else {
|
|
941
|
-
// 定高:每帧传,支持受控跳转
|
|
836
|
+
if (programmaticScrollRef.current) {
|
|
837
|
+
// 程序性滚动(imperative / scrollCorrection / scrollIntoView 等):传目标位置
|
|
942
838
|
if (isHorizontal) {
|
|
943
839
|
scrollViewProps.scrollLeft = scrollViewOffset;
|
|
944
840
|
}
|
|
@@ -947,6 +843,7 @@ const InnerList = (props, ref) => {
|
|
|
947
843
|
}
|
|
948
844
|
programmaticScrollRef.current = false;
|
|
949
845
|
}
|
|
846
|
+
// else:用户滚动 / 空闲帧不传 scrollTop,让微信原生 + scrollAnchoring 完全接管
|
|
950
847
|
}
|
|
951
848
|
else {
|
|
952
849
|
// H5:非用户滑动时传
|
|
@@ -959,27 +856,40 @@ const InnerList = (props, ref) => {
|
|
|
959
856
|
}
|
|
960
857
|
}
|
|
961
858
|
}
|
|
962
|
-
// H5
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
}
|
|
979
|
-
//
|
|
859
|
+
// H5:程序性滚动时写回 DOM scrollTop
|
|
860
|
+
React.useEffect(() => {
|
|
861
|
+
if (!isH5)
|
|
862
|
+
return;
|
|
863
|
+
if (isUserScrolling || !containerRef.current || typeof scrollViewOffset !== 'number')
|
|
864
|
+
return;
|
|
865
|
+
if (!programmaticScrollRef.current)
|
|
866
|
+
return;
|
|
867
|
+
const scrollValue = scrollViewOffset;
|
|
868
|
+
if (isHorizontal) {
|
|
869
|
+
containerRef.current.scrollLeft = scrollValue;
|
|
870
|
+
}
|
|
871
|
+
else {
|
|
872
|
+
containerRef.current.scrollTop = scrollValue;
|
|
873
|
+
}
|
|
874
|
+
programmaticScrollRef.current = false;
|
|
875
|
+
}, [isH5, scrollViewOffset, isHorizontal, isUserScrolling]);
|
|
876
|
+
// 总高度/宽度(含 NoMore)
|
|
980
877
|
const noMoreHeight = (noMoreConfig === null || noMoreConfig === void 0 ? void 0 : noMoreConfig.visible) ? (noMoreConfig.height || 60) : 0;
|
|
981
878
|
const listContentLength = sectionOffsets[sectionOffsets.length - 1] + noMoreHeight;
|
|
982
879
|
const totalLength = listContentLength;
|
|
880
|
+
// scrollElement 模式下 onScrollToLower 需用内层内容高度判断,供 scroll handler 读取
|
|
881
|
+
listContentLengthRef.current = listContentLength;
|
|
882
|
+
const scrollAttachRefsRef = React.useRef(null);
|
|
883
|
+
scrollAttachRefsRef.current = {
|
|
884
|
+
scrollCorrection: scrollCorrectionRef.current,
|
|
885
|
+
onScroll: onScrollRef.current,
|
|
886
|
+
onScrollToUpper: onScrollToUpperRef.current,
|
|
887
|
+
onScrollToLower: onScrollToLowerRef.current,
|
|
888
|
+
threshold: thresholdRef.current,
|
|
889
|
+
listContentLength: listContentLengthRef.current,
|
|
890
|
+
};
|
|
891
|
+
useListScrollElementAttach(useScrollElementMode && isH5, effectiveScrollElement, effectiveStartOffset, isHorizontal, setContainerLength, updateRenderOffset, scrollRefProp, scrollAttachRefsRef);
|
|
892
|
+
useListScrollElementAttachWeapp(useScrollElementMode && isWeapp, effectiveScrollElement, effectiveStartOffsetRef, effectiveStartOffset, isHorizontal, setContainerLength, updateRenderOffset, scrollRefProp, scrollAttachRefsRef, initialContainerLength, props.selectorQueryScope);
|
|
983
893
|
// 吸顶/吸左 header
|
|
984
894
|
const stickyHeaderNode = React.useMemo(() => {
|
|
985
895
|
if (!stickyHeader)
|
|
@@ -1005,7 +915,7 @@ const InnerList = (props, ref) => {
|
|
|
1005
915
|
}
|
|
1006
916
|
}
|
|
1007
917
|
return null;
|
|
1008
|
-
}, [stickyHeader, renderOffset, sectionOffsets, sections
|
|
918
|
+
}, [stickyHeader, renderOffset, sectionOffsets, sections]);
|
|
1009
919
|
// 渲染分组+item双层虚拟滚动
|
|
1010
920
|
const renderSections = () => {
|
|
1011
921
|
const nodes = [];
|
|
@@ -1043,12 +953,7 @@ const InnerList = (props, ref) => {
|
|
|
1043
953
|
else if (sizeCacheRafRef.current == null) {
|
|
1044
954
|
sizeCacheRafRef.current = requestAnimationFrame(() => {
|
|
1045
955
|
sizeCacheRafRef.current = null;
|
|
1046
|
-
|
|
1047
|
-
weappSizeCacheVersionBump(isUserScrollingRef, pendingBumpRef, measureScrollProtectRef, setSizeCacheVersion);
|
|
1048
|
-
}
|
|
1049
|
-
else {
|
|
1050
|
-
setSizeCacheVersion((v) => v + 1);
|
|
1051
|
-
}
|
|
956
|
+
setSizeCacheVersion((v) => v + 1);
|
|
1052
957
|
});
|
|
1053
958
|
}
|
|
1054
959
|
}
|
|
@@ -1066,7 +971,7 @@ const InnerList = (props, ref) => {
|
|
|
1066
971
|
Taro.nextTick(() => {
|
|
1067
972
|
if (!headerRefsRef.current.has(sectionIndex))
|
|
1068
973
|
return;
|
|
1069
|
-
|
|
974
|
+
createSelectorQueryScoped(props.selectorQueryScope)
|
|
1070
975
|
.select(`#${listId}-list-header-inner-${sectionIndex}`)
|
|
1071
976
|
.boundingClientRect((rect) => {
|
|
1072
977
|
if (rect) {
|
|
@@ -1098,6 +1003,7 @@ const InnerList = (props, ref) => {
|
|
|
1098
1003
|
const headerInnerProps = {
|
|
1099
1004
|
ref: headerRefCallback,
|
|
1100
1005
|
style: headerContentStyle,
|
|
1006
|
+
'data-index': String(-sectionIndex - 1), // 用于 unobserve 获取 index;与 observe(el, -sectionIndex-1) 对应
|
|
1101
1007
|
};
|
|
1102
1008
|
if (isWeapp) {
|
|
1103
1009
|
headerInnerProps.id = `${listId}-list-header-inner-${sectionIndex}`;
|
|
@@ -1143,7 +1049,6 @@ const InnerList = (props, ref) => {
|
|
|
1143
1049
|
for (let i = startItem; i <= endItem; i++) {
|
|
1144
1050
|
const currentGlobalIndex = globalItemIndex + i;
|
|
1145
1051
|
const itemId = `list-item-${currentGlobalIndex}`;
|
|
1146
|
-
// 内联样式替代className
|
|
1147
1052
|
const sectionItemStyle = Object.assign({ position: 'absolute', zIndex: 1, boxSizing: 'border-box', width: '100%', minHeight: '20px', overflow: 'hidden', lineHeight: 1 }, (isHorizontal
|
|
1148
1053
|
? {
|
|
1149
1054
|
top: 0,
|
|
@@ -1157,8 +1062,7 @@ const InnerList = (props, ref) => {
|
|
|
1157
1062
|
height: itemSizes[i],
|
|
1158
1063
|
marginBottom: space
|
|
1159
1064
|
}));
|
|
1160
|
-
//
|
|
1161
|
-
// 后加载项:挂载后单帧 RAF 备用测量;仅当尺寸实际变化时才 bump 版本,避免与 ResizeObserver 重复触发导致连续两次重渲染(卡顿)
|
|
1065
|
+
// ResizeObserver:绑定内层内容容器测量真实尺寸,尺寸变化时才 bump 版本
|
|
1162
1066
|
const refCallback = (el) => {
|
|
1163
1067
|
if (el) {
|
|
1164
1068
|
const capturedIndex = currentGlobalIndex;
|
|
@@ -1170,7 +1074,6 @@ const InnerList = (props, ref) => {
|
|
|
1170
1074
|
var _a;
|
|
1171
1075
|
if (measured > 0) {
|
|
1172
1076
|
const oldSize = sizeCache.getItemSize(capturedIndex);
|
|
1173
|
-
// 尺寸未变化,跳过
|
|
1174
1077
|
if (Math.abs(oldSize - measured) < 1)
|
|
1175
1078
|
return;
|
|
1176
1079
|
sizeCache.setItemSize(capturedIndex, measured);
|
|
@@ -1178,29 +1081,19 @@ const InnerList = (props, ref) => {
|
|
|
1178
1081
|
if (isWeapp && props.useResizeObserver === true) {
|
|
1179
1082
|
scheduleWeappDynamicReflow();
|
|
1180
1083
|
}
|
|
1181
|
-
else if (
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1084
|
+
else if (sizeCacheRafRef.current == null) {
|
|
1085
|
+
sizeCacheRafRef.current = requestAnimationFrame(() => {
|
|
1086
|
+
sizeCacheRafRef.current = null;
|
|
1087
|
+
setSizeCacheVersion((v) => v + 1);
|
|
1088
|
+
});
|
|
1185
1089
|
}
|
|
1186
1090
|
// 小程序:延迟 onItemSizeChange,避免父组件重渲染导致 List remount
|
|
1187
1091
|
if (isWeapp) {
|
|
1188
|
-
weappDeferItemSizeChange(
|
|
1092
|
+
weappDeferItemSizeChange(capturedIndex, measured, props.onItemSizeChange);
|
|
1189
1093
|
}
|
|
1190
1094
|
else {
|
|
1191
1095
|
(_a = props.onItemSizeChange) === null || _a === void 0 ? void 0 : _a.call(props, capturedIndex, measured);
|
|
1192
1096
|
}
|
|
1193
|
-
if (!(isWeapp && props.useResizeObserver === true) && sizeCacheRafRef.current == null) {
|
|
1194
|
-
sizeCacheRafRef.current = requestAnimationFrame(() => {
|
|
1195
|
-
sizeCacheRafRef.current = null;
|
|
1196
|
-
if (isWeapp) {
|
|
1197
|
-
weappSizeCacheVersionBump(isUserScrollingRef, pendingBumpRef, measureScrollProtectRef, setSizeCacheVersion);
|
|
1198
|
-
}
|
|
1199
|
-
else {
|
|
1200
|
-
setSizeCacheVersion((v) => v + 1);
|
|
1201
|
-
}
|
|
1202
|
-
});
|
|
1203
|
-
}
|
|
1204
1097
|
}
|
|
1205
1098
|
};
|
|
1206
1099
|
if (isH5) {
|
|
@@ -1216,7 +1109,7 @@ const InnerList = (props, ref) => {
|
|
|
1216
1109
|
// 注意:必须选 inner 容器(无固定高度,由内容撑开),不能选 outer(有虚拟列表设置的固定高度)
|
|
1217
1110
|
// 已测量过的 item 跳过,避免 refCallback 重复触发导致无限循环
|
|
1218
1111
|
// 滚动期间跳过 SelectorQuery(不加入 weappMeasuredItemsRef),避免异步查询干扰 scroll-view
|
|
1219
|
-
// SelectorQuery
|
|
1112
|
+
// SelectorQuery 是只读查询,滚动期间执行安全
|
|
1220
1113
|
if (shouldMeasureWeappItem(capturedIndex, weappMeasuredItemsRef.current)) {
|
|
1221
1114
|
weappMeasuredItemsRef.current.add(capturedIndex);
|
|
1222
1115
|
// 使用 Taro.nextTick 代替 setTimeout(50):等待下一帧渲染完成后再测量
|
|
@@ -1224,7 +1117,7 @@ const InnerList = (props, ref) => {
|
|
|
1224
1117
|
Taro.nextTick(() => {
|
|
1225
1118
|
if (!itemRefsRef.current.has(capturedIndex))
|
|
1226
1119
|
return;
|
|
1227
|
-
|
|
1120
|
+
createSelectorQueryScoped(props.selectorQueryScope)
|
|
1228
1121
|
// 页面上可能同时存在多个 List,inner id 必须带 listId 前缀避免跨列表误命中
|
|
1229
1122
|
.select(`#${listId}-list-item-inner-${capturedIndex}`)
|
|
1230
1123
|
.boundingClientRect((rect) => {
|
|
@@ -1274,10 +1167,39 @@ const InnerList = (props, ref) => {
|
|
|
1274
1167
|
innerProps.id = `${listId}-list-item-inner-${currentGlobalIndex}`;
|
|
1275
1168
|
}
|
|
1276
1169
|
innerProps['data-index'] = String(currentGlobalIndex);
|
|
1277
|
-
|
|
1170
|
+
const itemNode = React.createElement(View, outerItemProps, React.createElement(View, innerProps, section.items[i]));
|
|
1171
|
+
// 任务 4.1:当 List 暴露 scrollRef 时,为每个 item 提供 Context,供内层 WaterFlow 使用
|
|
1172
|
+
const itemWithContext = scrollRefProp && !useScrollElementMode
|
|
1173
|
+
? React.createElement(ScrollElementContextOrFallback.Provider, {
|
|
1174
|
+
key: outerItemProps.key,
|
|
1175
|
+
value: {
|
|
1176
|
+
scrollRef: scrollRefProp,
|
|
1177
|
+
containerHeight: containerLength,
|
|
1178
|
+
startOffset: offset + itemOffsets[i],
|
|
1179
|
+
reportNestedHeightChange: props.useResizeObserver
|
|
1180
|
+
? (h) => handleReportNestedHeightChange(h, currentGlobalIndex)
|
|
1181
|
+
: undefined,
|
|
1182
|
+
},
|
|
1183
|
+
}, itemNode)
|
|
1184
|
+
: itemNode;
|
|
1185
|
+
nodes.push(itemWithContext);
|
|
1278
1186
|
}
|
|
1279
1187
|
else {
|
|
1280
|
-
|
|
1188
|
+
const itemNode = React.createElement(View, outerItemProps, section.items[i]);
|
|
1189
|
+
const itemWithContext = scrollRefProp && !useScrollElementMode
|
|
1190
|
+
? React.createElement(ScrollElementContextOrFallback.Provider, {
|
|
1191
|
+
key: outerItemProps.key,
|
|
1192
|
+
value: {
|
|
1193
|
+
scrollRef: scrollRefProp,
|
|
1194
|
+
containerHeight: containerLength,
|
|
1195
|
+
startOffset: offset + itemOffsets[i],
|
|
1196
|
+
reportNestedHeightChange: props.useResizeObserver
|
|
1197
|
+
? (h) => handleReportNestedHeightChange(h, currentGlobalIndex)
|
|
1198
|
+
: undefined,
|
|
1199
|
+
},
|
|
1200
|
+
}, itemNode)
|
|
1201
|
+
: itemNode;
|
|
1202
|
+
nodes.push(itemWithContext);
|
|
1281
1203
|
}
|
|
1282
1204
|
}
|
|
1283
1205
|
globalItemIndex += section.items.length;
|
|
@@ -1294,16 +1216,17 @@ const InnerList = (props, ref) => {
|
|
|
1294
1216
|
const defaultStyle = Object.assign(Object.assign(Object.assign({ position: 'absolute' }, (isHorizontal
|
|
1295
1217
|
? { left: listContentEnd, top: 0, width: noMoreHeightValue, height: '100%' }
|
|
1296
1218
|
: { top: listContentEnd, left: 0, width: '100%', height: noMoreHeightValue })), { display: 'flex', alignItems: 'center', justifyContent: 'center', textAlign: 'center', color: '#999', fontSize: '14px', boxSizing: 'border-box' }), noMoreConfig.style);
|
|
1297
|
-
return (jsx(View, { style: defaultStyle,
|
|
1219
|
+
return (jsx(View, { style: defaultStyle, children: noMoreConfig.children || noMoreConfig.text || '没有更多了' }));
|
|
1298
1220
|
};
|
|
1299
1221
|
// 空列表场景:仅显示 NoMore
|
|
1300
1222
|
if (sections.length === 0 && (noMoreConfig === null || noMoreConfig === void 0 ? void 0 : noMoreConfig.visible)) {
|
|
1301
1223
|
return (jsx(ScrollView, Object.assign({ ref: containerRef }, scrollViewProps, { children: jsx(View, { style: Object.assign({ minHeight: containerLength, display: 'flex', alignItems: 'center', justifyContent: 'center' }, containerStyle), children: renderNoMoreContent() }) })));
|
|
1302
1224
|
}
|
|
1303
|
-
//
|
|
1225
|
+
// 可滚区域总尺寸
|
|
1226
|
+
// H5 refresher 用负 translateY 隐藏,有上方混排时可能延伸到 sibling 区域;overflow:hidden 裁剪避免重叠
|
|
1227
|
+
const needsRefresherClip = refresherHeightForH5 > 0;
|
|
1304
1228
|
const contentWrapperStyle = isHorizontal
|
|
1305
|
-
? { width: totalLength, position: 'relative', height: '100%' }
|
|
1306
|
-
: { height: totalLength, position: 'relative', width: '100%' };
|
|
1229
|
+
? Object.assign({ width: totalLength, position: 'relative', height: '100%' }, (needsRefresherClip && { overflow: 'hidden' })) : Object.assign({ height: totalLength, position: 'relative', width: '100%' }, (needsRefresherClip && { overflow: 'hidden' }));
|
|
1307
1230
|
const listWrapperStyle = isHorizontal
|
|
1308
1231
|
? { width: listContentLength, position: 'relative', height: '100%' }
|
|
1309
1232
|
: { height: listContentLength, position: 'relative', width: '100%' };
|
|
@@ -1311,7 +1234,7 @@ const InnerList = (props, ref) => {
|
|
|
1311
1234
|
? { transform: `translateY(${h5RefresherProps.pullDistance}px)` }
|
|
1312
1235
|
: {};
|
|
1313
1236
|
const h5RefresherTranslateY = -refresherHeightForH5 + h5RefresherProps.pullDistance;
|
|
1314
|
-
//
|
|
1237
|
+
// 内容区域渲染
|
|
1315
1238
|
const renderContentArea = () => {
|
|
1316
1239
|
var _a, _b;
|
|
1317
1240
|
return (jsx(View, { style: contentWrapperStyle, children: refresherHeightForH5 > 0 ? (jsxs(Fragment, { children: [jsx(View, { style: {
|
|
@@ -1324,9 +1247,23 @@ const InnerList = (props, ref) => {
|
|
|
1324
1247
|
transform: `translateY(${h5RefresherTranslateY}px)`,
|
|
1325
1248
|
}, children: renderRefresherContent() }), jsxs(View, { style: Object.assign(Object.assign(Object.assign({}, listWrapperStyle), pullTranslate), { zIndex: 1, background: (_b = (_a = style === null || style === void 0 ? void 0 : style.background) !== null && _a !== void 0 ? _a : style === null || style === void 0 ? void 0 : style.backgroundColor) !== null && _b !== void 0 ? _b : '#fff' }), children: [stickyHeaderNode, renderSections(), renderNoMoreContent()] })] })) : (jsxs(Fragment, { children: [!supportsNativeRefresher && renderRefresherContent(), stickyHeaderNode, renderSections(), renderNoMoreContent()] })) }));
|
|
1326
1249
|
};
|
|
1250
|
+
// useScrollElementMode 或 needAutoFind&&pending:渲染 View 以便监听外部滚动或 probe 阶段查找滚动父节点
|
|
1251
|
+
const renderView = useScrollElementMode || (!!needAutoFind && autoFindStatus === 'pending');
|
|
1252
|
+
if (renderView) {
|
|
1253
|
+
// 任务 2.4:恢复 refresher DOM 结构(refresher 层 + listWrapperStyle)
|
|
1254
|
+
return (jsx(View, { ref: contentWrapperRef, id: contentId, style: contentWrapperStyle, children: refresherHeightForH5 > 0 ? (jsxs(Fragment, { children: [jsx(View, { style: {
|
|
1255
|
+
position: 'absolute',
|
|
1256
|
+
top: 0,
|
|
1257
|
+
left: 0,
|
|
1258
|
+
right: 0,
|
|
1259
|
+
height: refresherHeightForH5,
|
|
1260
|
+
zIndex: 0,
|
|
1261
|
+
transform: `translateY(${h5RefresherTranslateY}px)`,
|
|
1262
|
+
}, children: renderRefresherContent() }), jsxs(View, { style: Object.assign(Object.assign(Object.assign({}, listWrapperStyle), pullTranslate), { zIndex: 1, background: (_b = (_a = style === null || style === void 0 ? void 0 : style.background) !== null && _a !== void 0 ? _a : style === null || style === void 0 ? void 0 : style.backgroundColor) !== null && _b !== void 0 ? _b : '#fff' }), children: [stickyHeaderNode, renderSections(), renderNoMoreContent()] })] })) : (jsxs(Fragment, { children: [stickyHeaderNode, renderSections(), renderNoMoreContent()] })) }));
|
|
1263
|
+
}
|
|
1327
1264
|
return (jsxs(ScrollView, Object.assign({ ref: containerRef }, scrollViewProps, { id: listId, children: [supportsNativeRefresher && renderRefresherContent(), renderContentArea()] })));
|
|
1328
1265
|
};
|
|
1329
1266
|
const List = React.forwardRef(InnerList);
|
|
1330
1267
|
|
|
1331
|
-
export { List, ListItem, NoMore, StickyHeader, StickySection, accumulate, List as default, isShaking };
|
|
1268
|
+
export { List, ListItem, ScrollElementContextOrFallback as ListScrollElementContext, NoMore, StickyHeader, StickySection, accumulate, List as default, isShaking };
|
|
1332
1269
|
//# sourceMappingURL=index.js.map
|