@shopify/flash-list 2.0.0-alpha.9 → 2.0.0-rc.2
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/README.md +37 -97
- package/dist/AnimatedFlashList.d.ts.map +1 -1
- package/dist/AnimatedFlashList.js +3 -3
- package/dist/AnimatedFlashList.js.map +1 -1
- package/dist/FlashList.d.ts +9 -0
- package/dist/FlashList.d.ts.map +1 -1
- package/dist/FlashList.js +20 -0
- package/dist/FlashList.js.map +1 -1
- package/dist/FlashListProps.d.ts +15 -8
- package/dist/FlashListProps.d.ts.map +1 -1
- package/dist/FlashListProps.js.map +1 -1
- package/dist/FlashListRef.d.ts +305 -0
- package/dist/FlashListRef.d.ts.map +1 -0
- package/dist/FlashListRef.js +3 -0
- package/dist/FlashListRef.js.map +1 -0
- package/dist/MasonryFlashList.js.map +1 -1
- package/dist/__tests__/RecyclerView.test.js +62 -27
- package/dist/__tests__/RecyclerView.test.js.map +1 -1
- package/dist/__tests__/RenderStackManager.test.d.ts +2 -0
- package/dist/__tests__/RenderStackManager.test.d.ts.map +1 -0
- package/dist/__tests__/RenderStackManager.test.js +486 -0
- package/dist/__tests__/RenderStackManager.test.js.map +1 -0
- package/dist/__tests__/helpers/createLayoutManager.d.ts.map +1 -1
- package/dist/__tests__/helpers/createLayoutManager.js +3 -4
- package/dist/__tests__/helpers/createLayoutManager.js.map +1 -1
- package/dist/__tests__/useUnmountAwareCallbacks.test.js +1 -1
- package/dist/__tests__/useUnmountAwareCallbacks.test.js.map +1 -1
- package/dist/benchmark/useFlatListBenchmark.js +8 -7
- package/dist/benchmark/useFlatListBenchmark.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/native/config/PlatformHelper.android.d.ts +1 -0
- package/dist/native/config/PlatformHelper.android.d.ts.map +1 -1
- package/dist/native/config/PlatformHelper.android.js +1 -0
- package/dist/native/config/PlatformHelper.android.js.map +1 -1
- package/dist/native/config/PlatformHelper.d.ts +1 -0
- package/dist/native/config/PlatformHelper.d.ts.map +1 -1
- package/dist/native/config/PlatformHelper.ios.d.ts +1 -0
- package/dist/native/config/PlatformHelper.ios.d.ts.map +1 -1
- package/dist/native/config/PlatformHelper.ios.js +1 -0
- package/dist/native/config/PlatformHelper.ios.js.map +1 -1
- package/dist/native/config/PlatformHelper.js +1 -0
- package/dist/native/config/PlatformHelper.js.map +1 -1
- package/dist/native/config/PlatformHelper.web.d.ts +1 -0
- package/dist/native/config/PlatformHelper.web.d.ts.map +1 -1
- package/dist/native/config/PlatformHelper.web.js +1 -0
- package/dist/native/config/PlatformHelper.web.js.map +1 -1
- package/dist/recyclerview/RecyclerView.d.ts +2 -1
- package/dist/recyclerview/RecyclerView.d.ts.map +1 -1
- package/dist/recyclerview/RecyclerView.js +63 -45
- package/dist/recyclerview/RecyclerView.js.map +1 -1
- package/dist/recyclerview/RecyclerViewContextProvider.d.ts +6 -5
- package/dist/recyclerview/RecyclerViewContextProvider.d.ts.map +1 -1
- package/dist/recyclerview/RecyclerViewContextProvider.js.map +1 -1
- package/dist/recyclerview/RecyclerViewManager.d.ts +21 -7
- package/dist/recyclerview/RecyclerViewManager.d.ts.map +1 -1
- package/dist/recyclerview/RecyclerViewManager.js +105 -113
- package/dist/recyclerview/RecyclerViewManager.js.map +1 -1
- package/dist/recyclerview/RenderStackManager.d.ts +85 -0
- package/dist/recyclerview/RenderStackManager.d.ts.map +1 -0
- package/dist/recyclerview/RenderStackManager.js +324 -0
- package/dist/recyclerview/RenderStackManager.js.map +1 -0
- package/dist/recyclerview/ViewHolder.d.ts.map +1 -1
- package/dist/recyclerview/ViewHolder.js +5 -3
- package/dist/recyclerview/ViewHolder.js.map +1 -1
- package/dist/recyclerview/ViewHolderCollection.d.ts +3 -1
- package/dist/recyclerview/ViewHolderCollection.d.ts.map +1 -1
- package/dist/recyclerview/ViewHolderCollection.js +23 -8
- package/dist/recyclerview/ViewHolderCollection.js.map +1 -1
- package/dist/recyclerview/components/ScrollAnchor.d.ts +2 -2
- package/dist/recyclerview/components/ScrollAnchor.d.ts.map +1 -1
- package/dist/recyclerview/components/ScrollAnchor.js +9 -5
- package/dist/recyclerview/components/ScrollAnchor.js.map +1 -1
- package/dist/recyclerview/components/StickyHeaders.d.ts +1 -1
- package/dist/recyclerview/components/StickyHeaders.d.ts.map +1 -1
- package/dist/recyclerview/components/StickyHeaders.js +40 -33
- package/dist/recyclerview/components/StickyHeaders.js.map +1 -1
- package/dist/recyclerview/helpers/EngagedIndicesTracker.d.ts +45 -1
- package/dist/recyclerview/helpers/EngagedIndicesTracker.d.ts.map +1 -1
- package/dist/recyclerview/helpers/EngagedIndicesTracker.js +77 -20
- package/dist/recyclerview/helpers/EngagedIndicesTracker.js.map +1 -1
- package/dist/recyclerview/helpers/RenderTimeTracker.d.ts +10 -0
- package/dist/recyclerview/helpers/RenderTimeTracker.d.ts.map +1 -0
- package/dist/recyclerview/helpers/RenderTimeTracker.js +39 -0
- package/dist/recyclerview/helpers/RenderTimeTracker.js.map +1 -0
- package/dist/recyclerview/helpers/VelocityTracker.d.ts +29 -0
- package/dist/recyclerview/helpers/VelocityTracker.d.ts.map +1 -0
- package/dist/recyclerview/helpers/VelocityTracker.js +70 -0
- package/dist/recyclerview/helpers/VelocityTracker.js.map +1 -0
- package/dist/recyclerview/hooks/useBoundDetection.d.ts +1 -2
- package/dist/recyclerview/hooks/useBoundDetection.d.ts.map +1 -1
- package/dist/recyclerview/hooks/useBoundDetection.js +19 -16
- package/dist/recyclerview/hooks/useBoundDetection.js.map +1 -1
- package/dist/recyclerview/hooks/useMappingHelper.d.ts +1 -1
- package/dist/recyclerview/hooks/useMappingHelper.d.ts.map +1 -1
- package/dist/recyclerview/hooks/useMappingHelper.js +1 -1
- package/dist/recyclerview/hooks/useMappingHelper.js.map +1 -1
- package/dist/recyclerview/hooks/useOnLoad.d.ts.map +1 -1
- package/dist/recyclerview/hooks/useOnLoad.js +4 -6
- package/dist/recyclerview/hooks/useOnLoad.js.map +1 -1
- package/dist/recyclerview/hooks/useRecyclerViewController.d.ts +3 -48
- package/dist/recyclerview/hooks/useRecyclerViewController.d.ts.map +1 -1
- package/dist/recyclerview/hooks/useRecyclerViewController.js +174 -123
- package/dist/recyclerview/hooks/useRecyclerViewController.js.map +1 -1
- package/dist/recyclerview/hooks/useRecyclerViewManager.d.ts +2 -0
- package/dist/recyclerview/hooks/useRecyclerViewManager.d.ts.map +1 -1
- package/dist/recyclerview/hooks/useRecyclerViewManager.js +10 -1
- package/dist/recyclerview/hooks/useRecyclerViewManager.js.map +1 -1
- package/dist/recyclerview/hooks/useSecondaryProps.js +1 -1
- package/dist/recyclerview/hooks/useUnmountAwareCallbacks.d.ts +10 -3
- package/dist/recyclerview/hooks/useUnmountAwareCallbacks.d.ts.map +1 -1
- package/dist/recyclerview/hooks/useUnmountAwareCallbacks.js +33 -4
- package/dist/recyclerview/hooks/useUnmountAwareCallbacks.js.map +1 -1
- package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts +6 -0
- package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts.map +1 -1
- package/dist/recyclerview/layout-managers/GridLayoutManager.js +27 -5
- package/dist/recyclerview/layout-managers/GridLayoutManager.js.map +1 -1
- package/dist/recyclerview/layout-managers/LayoutManager.d.ts +10 -16
- package/dist/recyclerview/layout-managers/LayoutManager.d.ts.map +1 -1
- package/dist/recyclerview/layout-managers/LayoutManager.js +4 -14
- package/dist/recyclerview/layout-managers/LayoutManager.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/viewability/ViewToken.d.ts +2 -2
- package/dist/viewability/ViewToken.d.ts.map +1 -1
- package/dist/viewability/ViewabilityHelper.js +1 -1
- package/dist/viewability/ViewabilityHelper.js.map +1 -1
- package/dist/viewability/ViewabilityManager.d.ts.map +1 -1
- package/dist/viewability/ViewabilityManager.js +1 -2
- package/dist/viewability/ViewabilityManager.js.map +1 -1
- package/jestSetup.js +30 -11
- package/package.json +2 -1
- package/src/AnimatedFlashList.ts +3 -2
- package/src/FlashList.tsx +24 -0
- package/src/FlashListProps.ts +20 -8
- package/src/FlashListRef.ts +320 -0
- package/src/MasonryFlashList.tsx +2 -2
- package/src/__tests__/RecyclerView.test.tsx +83 -29
- package/src/__tests__/RenderStackManager.test.ts +575 -0
- package/src/__tests__/helpers/createLayoutManager.ts +2 -3
- package/src/__tests__/useUnmountAwareCallbacks.test.tsx +12 -12
- package/src/benchmark/useFlatListBenchmark.ts +2 -2
- package/src/index.ts +1 -0
- package/src/native/config/PlatformHelper.android.ts +1 -0
- package/src/native/config/PlatformHelper.ios.ts +1 -0
- package/src/native/config/PlatformHelper.ts +1 -0
- package/src/native/config/PlatformHelper.web.ts +1 -0
- package/src/recyclerview/RecyclerView.tsx +82 -52
- package/src/recyclerview/RecyclerViewContextProvider.ts +12 -6
- package/src/recyclerview/RecyclerViewManager.ts +123 -98
- package/src/recyclerview/RenderStackManager.ts +291 -0
- package/src/recyclerview/ViewHolder.tsx +5 -3
- package/src/recyclerview/ViewHolderCollection.tsx +33 -12
- package/src/recyclerview/components/ScrollAnchor.tsx +21 -9
- package/src/recyclerview/components/StickyHeaders.tsx +63 -45
- package/src/recyclerview/helpers/EngagedIndicesTracker.ts +118 -23
- package/src/recyclerview/helpers/RenderTimeTracker.ts +38 -0
- package/src/recyclerview/helpers/VelocityTracker.ts +77 -0
- package/src/recyclerview/hooks/useBoundDetection.ts +25 -18
- package/src/recyclerview/hooks/useMappingHelper.ts +1 -1
- package/src/recyclerview/hooks/useOnLoad.ts +4 -6
- package/src/recyclerview/hooks/useRecyclerViewController.tsx +199 -176
- package/src/recyclerview/hooks/useRecyclerViewManager.ts +11 -1
- package/src/recyclerview/hooks/useSecondaryProps.tsx +1 -1
- package/src/recyclerview/hooks/useUnmountAwareCallbacks.ts +39 -3
- package/src/recyclerview/layout-managers/GridLayoutManager.ts +30 -7
- package/src/recyclerview/layout-managers/LayoutManager.ts +12 -21
- package/src/viewability/ViewToken.ts +2 -2
- package/src/viewability/ViewabilityHelper.ts +1 -1
- package/src/viewability/ViewabilityManager.ts +6 -3
- package/dist/__tests__/RecycleKeyManager.test.d.ts +0 -2
- package/dist/__tests__/RecycleKeyManager.test.d.ts.map +0 -1
- package/dist/__tests__/RecycleKeyManager.test.js +0 -210
- package/dist/__tests__/RecycleKeyManager.test.js.map +0 -1
- package/dist/recyclerview/RecycleKeyManager.d.ts +0 -82
- package/dist/recyclerview/RecycleKeyManager.d.ts.map +0 -1
- package/dist/recyclerview/RecycleKeyManager.js +0 -135
- package/dist/recyclerview/RecycleKeyManager.js.map +0 -1
- package/src/__tests__/RecycleKeyManager.test.ts +0 -254
- package/src/recyclerview/RecycleKeyManager.ts +0 -185
|
@@ -88,7 +88,7 @@ const ViewHolderInternal = <TItem,>(props: ViewHolderProps<TItem>) => {
|
|
|
88
88
|
);
|
|
89
89
|
|
|
90
90
|
const separator = useMemo(() => {
|
|
91
|
-
return ItemSeparatorComponent ? (
|
|
91
|
+
return ItemSeparatorComponent && trailingItem !== undefined ? (
|
|
92
92
|
<ItemSeparatorComponent leadingItem={item} trailingItem={trailingItem} />
|
|
93
93
|
) : null;
|
|
94
94
|
}, [ItemSeparatorComponent, item, trailingItem]);
|
|
@@ -97,7 +97,10 @@ const ViewHolderInternal = <TItem,>(props: ViewHolderProps<TItem>) => {
|
|
|
97
97
|
|
|
98
98
|
const children = useMemo(() => {
|
|
99
99
|
return renderItem?.({ item, index, extraData, target }) ?? null;
|
|
100
|
-
|
|
100
|
+
// TODO: Test more thoroughly
|
|
101
|
+
// We don't really to re-render the children when the index changes
|
|
102
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
103
|
+
}, [item, extraData, target, renderItem]);
|
|
101
104
|
|
|
102
105
|
const style = {
|
|
103
106
|
flexDirection: horizontal ? "row" : "column",
|
|
@@ -110,7 +113,6 @@ const ViewHolderInternal = <TItem,>(props: ViewHolderProps<TItem>) => {
|
|
|
110
113
|
maxWidth: layout.maxWidth,
|
|
111
114
|
left: layout.x,
|
|
112
115
|
top: layout.y,
|
|
113
|
-
zIndex: 0,
|
|
114
116
|
} as const;
|
|
115
117
|
|
|
116
118
|
// TODO: Fix this type issue
|
|
@@ -21,7 +21,7 @@ export interface ViewHolderCollectionProps<TItem> {
|
|
|
21
21
|
/** The data array to be rendered */
|
|
22
22
|
data: FlashListProps<TItem>["data"];
|
|
23
23
|
/** Map of indices to React keys for each rendered item */
|
|
24
|
-
renderStack: Map<
|
|
24
|
+
renderStack: Map<string, { index: number }>;
|
|
25
25
|
/** Function to get layout information for a specific index */
|
|
26
26
|
getLayout: (index: number) => RVLayout;
|
|
27
27
|
/** Ref to control layout updates from parent components */
|
|
@@ -99,43 +99,64 @@ export const ViewHolderCollection = <TItem,>(
|
|
|
99
99
|
// );
|
|
100
100
|
recyclerViewContext?.layout();
|
|
101
101
|
}
|
|
102
|
+
// we need to run this callback on when fixedContainerSize changes
|
|
103
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
102
104
|
}, [fixedContainerSize]);
|
|
103
105
|
|
|
104
106
|
useLayoutEffect(() => {
|
|
105
107
|
if (renderId > 0) {
|
|
106
108
|
onCommitLayoutEffect?.();
|
|
107
109
|
}
|
|
110
|
+
// we need to run this callback on when renderId changes
|
|
111
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
108
112
|
}, [renderId]);
|
|
109
113
|
|
|
110
114
|
useEffect(() => {
|
|
111
115
|
if (renderId > 0) {
|
|
112
116
|
onCommitEffect?.();
|
|
113
117
|
}
|
|
118
|
+
// we need to run this callback on when renderId changes
|
|
119
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
114
120
|
}, [renderId]);
|
|
115
121
|
|
|
116
122
|
// Expose forceUpdate through ref
|
|
117
|
-
useImperativeHandle(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
+
useImperativeHandle(
|
|
124
|
+
viewHolderCollectionRef,
|
|
125
|
+
() => ({
|
|
126
|
+
commitLayout: () => {
|
|
127
|
+
// This will trigger a re-render of the component
|
|
128
|
+
setRenderId((prev) => prev + 1);
|
|
129
|
+
},
|
|
130
|
+
}),
|
|
131
|
+
[setRenderId]
|
|
132
|
+
);
|
|
123
133
|
|
|
124
134
|
const hasData = data && data.length > 0;
|
|
125
135
|
|
|
126
136
|
const containerStyle = {
|
|
127
137
|
width: horizontal ? containerLayout?.width : undefined,
|
|
128
138
|
height: containerLayout?.height,
|
|
139
|
+
// TODO: Temp workaround, useLayoutEffect doesn't block paint in some cases
|
|
140
|
+
// We need to investigate why this is happening
|
|
141
|
+
opacity: renderId > 0 ? 1 : 0,
|
|
129
142
|
};
|
|
130
143
|
|
|
144
|
+
// sort by index and log
|
|
145
|
+
// const sortedRenderStack = Array.from(renderStack.entries()).sort(
|
|
146
|
+
// ([, a], [, b]) => a.index - b.index
|
|
147
|
+
// );
|
|
148
|
+
// console.log(
|
|
149
|
+
// "sortedRenderStack",
|
|
150
|
+
// sortedRenderStack.map(([reactKey, { index }]) => {
|
|
151
|
+
// return `${index} => ${reactKey}`;
|
|
152
|
+
// })
|
|
153
|
+
// );
|
|
154
|
+
|
|
131
155
|
return (
|
|
132
|
-
<CompatView
|
|
133
|
-
// TODO: Take care of web scroll bar here
|
|
134
|
-
style={hasData && containerStyle}
|
|
135
|
-
>
|
|
156
|
+
<CompatView style={hasData && containerStyle}>
|
|
136
157
|
{containerLayout &&
|
|
137
158
|
hasData &&
|
|
138
|
-
Array.from(renderStack, ([
|
|
159
|
+
Array.from(renderStack.entries(), ([reactKey, { index }]) => {
|
|
139
160
|
const item = data[index];
|
|
140
161
|
const trailingItem = ItemSeparatorComponent
|
|
141
162
|
? data[index + 1]
|
|
@@ -15,6 +15,7 @@ import { CompatView } from "./CompatView";
|
|
|
15
15
|
export interface ScrollAnchorProps {
|
|
16
16
|
/** Ref to access scroll anchor methods */
|
|
17
17
|
scrollAnchorRef: React.Ref<ScrollAnchorRef>;
|
|
18
|
+
horizontal: boolean;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
/**
|
|
@@ -27,28 +28,39 @@ export interface ScrollAnchorRef {
|
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* ScrollAnchor component that provides programmatic scrolling capabilities using maintainVisibleContentPosition property
|
|
30
|
-
* TODO: Implement this for web
|
|
31
31
|
* @param props - Component props
|
|
32
32
|
* @returns An invisible anchor element used for scrolling
|
|
33
33
|
*/
|
|
34
|
-
export function ScrollAnchor({
|
|
34
|
+
export function ScrollAnchor({
|
|
35
|
+
scrollAnchorRef,
|
|
36
|
+
horizontal,
|
|
37
|
+
}: ScrollAnchorProps) {
|
|
35
38
|
const [scrollOffset, setScrollOffset] = useState(1000000); // TODO: Fix this value
|
|
36
39
|
|
|
37
40
|
// Expose scrollBy method through ref
|
|
38
|
-
useImperativeHandle(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
useImperativeHandle(
|
|
42
|
+
scrollAnchorRef,
|
|
43
|
+
() => ({
|
|
44
|
+
scrollBy: (offset: number) => {
|
|
45
|
+
setScrollOffset((prev) => prev + offset);
|
|
46
|
+
},
|
|
47
|
+
}),
|
|
48
|
+
[]
|
|
49
|
+
);
|
|
43
50
|
|
|
44
51
|
// Create an invisible anchor element that can be positioned
|
|
45
52
|
const anchor = useMemo(() => {
|
|
46
53
|
return (
|
|
47
54
|
<CompatView
|
|
48
|
-
style={{
|
|
55
|
+
style={{
|
|
56
|
+
position: "absolute",
|
|
57
|
+
height: 0,
|
|
58
|
+
top: horizontal ? 0 : scrollOffset,
|
|
59
|
+
left: horizontal ? scrollOffset : 0,
|
|
60
|
+
}}
|
|
49
61
|
/>
|
|
50
62
|
);
|
|
51
|
-
}, [scrollOffset]);
|
|
63
|
+
}, [scrollOffset, horizontal]);
|
|
52
64
|
|
|
53
65
|
return anchor;
|
|
54
66
|
}
|
|
@@ -49,6 +49,11 @@ export interface StickyHeaderRef {
|
|
|
49
49
|
reportScrollEvent: (event: NativeScrollEvent) => void;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
interface StickyHeaderState {
|
|
53
|
+
currentStickyIndex: number;
|
|
54
|
+
pushStartsAt: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
52
57
|
export const StickyHeaders = <TItem,>({
|
|
53
58
|
stickyHeaderIndices,
|
|
54
59
|
renderItem,
|
|
@@ -58,25 +63,35 @@ export const StickyHeaders = <TItem,>({
|
|
|
58
63
|
data,
|
|
59
64
|
extraData,
|
|
60
65
|
}: StickyHeaderProps<TItem>) => {
|
|
61
|
-
const [
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
66
|
+
const [stickyHeaderState, setStickyHeaderState] = useState<StickyHeaderState>(
|
|
67
|
+
{
|
|
68
|
+
currentStickyIndex: -1,
|
|
69
|
+
pushStartsAt: Number.MAX_SAFE_INTEGER,
|
|
70
|
+
}
|
|
71
|
+
);
|
|
65
72
|
|
|
66
|
-
const { currentStickyIndex,
|
|
73
|
+
const { currentStickyIndex, pushStartsAt } = stickyHeaderState;
|
|
67
74
|
|
|
68
|
-
//
|
|
75
|
+
// sort indices and memoize compute
|
|
69
76
|
const sortedIndices = useMemo(() => {
|
|
70
|
-
return stickyHeaderIndices.sort((first, second) => first - second);
|
|
77
|
+
return [...stickyHeaderIndices].sort((first, second) => first - second);
|
|
71
78
|
}, [stickyHeaderIndices]);
|
|
72
79
|
|
|
80
|
+
const legthInvalid =
|
|
81
|
+
sortedIndices.length === 0 ||
|
|
82
|
+
recyclerViewManager.getDataLength() <=
|
|
83
|
+
sortedIndices[sortedIndices.length - 1];
|
|
84
|
+
|
|
73
85
|
const compute = useCallback(() => {
|
|
74
|
-
|
|
86
|
+
if (legthInvalid) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const adjustedScrollOffset = recyclerViewManager.getLastScrollOffset();
|
|
75
90
|
|
|
76
91
|
// Binary search for current sticky index
|
|
77
92
|
const currentIndexInArray = findCurrentStickyIndex(
|
|
78
93
|
sortedIndices,
|
|
79
|
-
|
|
94
|
+
adjustedScrollOffset,
|
|
80
95
|
(index) => recyclerViewManager.getLayout(index).y
|
|
81
96
|
);
|
|
82
97
|
|
|
@@ -87,16 +102,33 @@ export const StickyHeaders = <TItem,>({
|
|
|
87
102
|
newNextStickyIndex = -1;
|
|
88
103
|
}
|
|
89
104
|
|
|
105
|
+
// To make sure header offset is 0 in the interpolate compute
|
|
106
|
+
const newNextStickyY =
|
|
107
|
+
newNextStickyIndex === -1
|
|
108
|
+
? Number.MAX_SAFE_INTEGER
|
|
109
|
+
: (recyclerViewManager.tryGetLayout(newNextStickyIndex)?.y ?? 0) +
|
|
110
|
+
recyclerViewManager.firstItemOffset;
|
|
111
|
+
const newCurrentStickyHeight =
|
|
112
|
+
recyclerViewManager.tryGetLayout(newStickyIndex)?.height ?? 0;
|
|
113
|
+
|
|
114
|
+
const newPushStartsAt = newNextStickyY - newCurrentStickyHeight;
|
|
115
|
+
|
|
90
116
|
if (
|
|
91
117
|
newStickyIndex !== currentStickyIndex ||
|
|
92
|
-
|
|
118
|
+
newPushStartsAt !== pushStartsAt
|
|
93
119
|
) {
|
|
94
|
-
|
|
120
|
+
setStickyHeaderState({
|
|
95
121
|
currentStickyIndex: newStickyIndex,
|
|
96
|
-
|
|
122
|
+
pushStartsAt: newPushStartsAt,
|
|
97
123
|
});
|
|
98
124
|
}
|
|
99
|
-
}, [
|
|
125
|
+
}, [
|
|
126
|
+
legthInvalid,
|
|
127
|
+
recyclerViewManager,
|
|
128
|
+
sortedIndices,
|
|
129
|
+
currentStickyIndex,
|
|
130
|
+
pushStartsAt,
|
|
131
|
+
]);
|
|
100
132
|
|
|
101
133
|
useEffect(() => {
|
|
102
134
|
compute();
|
|
@@ -115,35 +147,19 @@ export const StickyHeaders = <TItem,>({
|
|
|
115
147
|
|
|
116
148
|
const refHolder = useRef(new Map()).current;
|
|
117
149
|
|
|
118
|
-
// Memoize translateY calculation
|
|
119
150
|
const translateY = useMemo(() => {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
inputRange: [0, Infinity],
|
|
123
|
-
outputRange: [0, 0],
|
|
124
|
-
extrapolate: "clamp",
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const currentLayout = recyclerViewManager.getLayout(currentStickyIndex);
|
|
129
|
-
const nextLayout = recyclerViewManager.getLayout(nextStickyIndex);
|
|
130
|
-
|
|
131
|
-
const pushStartsAt = nextLayout.y - currentLayout.height;
|
|
151
|
+
const currentStickyHeight =
|
|
152
|
+
recyclerViewManager.tryGetLayout(currentStickyIndex)?.height ?? 0;
|
|
132
153
|
|
|
133
154
|
return scrollY.interpolate({
|
|
134
|
-
inputRange: [
|
|
135
|
-
|
|
136
|
-
nextLayout.y + recyclerViewManager.firstItemOffset,
|
|
137
|
-
],
|
|
138
|
-
outputRange: [0, -currentLayout.height],
|
|
155
|
+
inputRange: [pushStartsAt, pushStartsAt + currentStickyHeight],
|
|
156
|
+
outputRange: [0, -currentStickyHeight],
|
|
139
157
|
extrapolate: "clamp",
|
|
140
158
|
});
|
|
141
|
-
}, [
|
|
159
|
+
}, [recyclerViewManager, currentStickyIndex, scrollY, pushStartsAt]);
|
|
142
160
|
|
|
143
161
|
// Memoize header content
|
|
144
162
|
const headerContent = useMemo(() => {
|
|
145
|
-
if (currentStickyIndex === -1) return null;
|
|
146
|
-
|
|
147
163
|
return (
|
|
148
164
|
<CompatAnimatedView
|
|
149
165
|
style={{
|
|
@@ -155,19 +171,21 @@ export const StickyHeaders = <TItem,>({
|
|
|
155
171
|
transform: [{ translateY }],
|
|
156
172
|
}}
|
|
157
173
|
>
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
174
|
+
{currentStickyIndex !== -1 ? (
|
|
175
|
+
<ViewHolder
|
|
176
|
+
index={currentStickyIndex}
|
|
177
|
+
item={data[currentStickyIndex]}
|
|
178
|
+
renderItem={renderItem}
|
|
179
|
+
layout={{ x: 0, y: 0, width: 0, height: 0 }}
|
|
180
|
+
refHolder={refHolder}
|
|
181
|
+
extraData={extraData}
|
|
182
|
+
trailingItem={null}
|
|
183
|
+
target="StickyHeader"
|
|
184
|
+
/>
|
|
185
|
+
) : null}
|
|
168
186
|
</CompatAnimatedView>
|
|
169
187
|
);
|
|
170
|
-
}, [currentStickyIndex, data, renderItem,
|
|
188
|
+
}, [translateY, currentStickyIndex, data, renderItem, refHolder, extraData]);
|
|
171
189
|
|
|
172
190
|
return headerContent;
|
|
173
191
|
};
|
|
@@ -8,6 +8,10 @@ export interface RVEngagedIndicesTracker {
|
|
|
8
8
|
scrollOffset: number;
|
|
9
9
|
// Total distance (in pixels) to pre-render items before and after the visible viewport
|
|
10
10
|
drawDistance: number;
|
|
11
|
+
// Whether to use offset projection to predict the next scroll offset
|
|
12
|
+
enableOffsetProjection: boolean;
|
|
13
|
+
// Average render time of the list
|
|
14
|
+
averageRenderTime: number;
|
|
11
15
|
|
|
12
16
|
/**
|
|
13
17
|
* Updates the scroll offset and calculates which items should be rendered (engaged indices).
|
|
@@ -21,9 +25,31 @@ export interface RVEngagedIndicesTracker {
|
|
|
21
25
|
velocity: Velocity | null | undefined,
|
|
22
26
|
layoutManager: RVLayoutManager
|
|
23
27
|
) => ConsecutiveNumbers | undefined;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Returns the currently engaged (rendered) indices.
|
|
31
|
+
* This includes both visible items and buffer items.
|
|
32
|
+
* @returns The last computed set of engaged indices
|
|
33
|
+
*/
|
|
24
34
|
getEngagedIndices: () => ConsecutiveNumbers;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Computes the visible indices in the viewport.
|
|
38
|
+
* @param layoutManager - Layout manager to fetch item positions and dimensions
|
|
39
|
+
* @returns Indices of items currently visible in the viewport
|
|
40
|
+
*/
|
|
25
41
|
computeVisibleIndices: (layoutManager: RVLayoutManager) => ConsecutiveNumbers;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Sets the scroll direction for velocity history tracking.
|
|
45
|
+
* @param scrollDirection - The direction of scrolling ("forward" or "backward")
|
|
46
|
+
*/
|
|
26
47
|
setScrollDirection: (scrollDirection: "forward" | "backward") => void;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resets the velocity history based on the current scroll direction.
|
|
51
|
+
*/
|
|
52
|
+
resetVelocityHistory: () => void;
|
|
27
53
|
}
|
|
28
54
|
|
|
29
55
|
export interface Velocity {
|
|
@@ -35,17 +61,26 @@ export class RVEngagedIndicesTrackerImpl implements RVEngagedIndicesTracker {
|
|
|
35
61
|
// Current scroll position of the list
|
|
36
62
|
public scrollOffset = 0;
|
|
37
63
|
// Distance to pre-render items before and after the visible viewport (in pixels)
|
|
38
|
-
// TODO: Increase this value for web
|
|
39
64
|
public drawDistance = PlatformConfig.defaultDrawDistance;
|
|
65
|
+
|
|
66
|
+
// Whether to use offset projection to predict the next scroll offset
|
|
67
|
+
public enableOffsetProjection = true;
|
|
68
|
+
|
|
69
|
+
// Average render time of the list
|
|
70
|
+
public averageRenderTime = 16;
|
|
71
|
+
|
|
72
|
+
// Internal override to disable offset projection
|
|
73
|
+
private forceDisableOffsetProjection = false;
|
|
74
|
+
|
|
40
75
|
// Currently rendered item indices (including buffer items)
|
|
41
76
|
private engagedIndices = ConsecutiveNumbers.EMPTY;
|
|
42
77
|
|
|
43
78
|
// Buffer distribution multipliers for scroll direction optimization
|
|
44
|
-
private smallMultiplier = 0.
|
|
45
|
-
private largeMultiplier = 0.
|
|
79
|
+
private smallMultiplier = 0.3; // Used for buffer in the opposite direction of scroll
|
|
80
|
+
private largeMultiplier = 0.7; // Used for buffer in the direction of scroll
|
|
46
81
|
|
|
47
82
|
// Circular buffer to track recent scroll velocities for direction detection
|
|
48
|
-
private velocityHistory = [
|
|
83
|
+
private velocityHistory = [0, 0, 0, -0.1, -0.1];
|
|
49
84
|
private velocityIndex = 0;
|
|
50
85
|
|
|
51
86
|
/**
|
|
@@ -67,7 +102,21 @@ export class RVEngagedIndicesTrackerImpl implements RVEngagedIndicesTracker {
|
|
|
67
102
|
// STEP 1: Determine the currently visible viewport
|
|
68
103
|
const windowSize = layoutManager.getWindowsSize();
|
|
69
104
|
const isHorizontal = layoutManager.isHorizontal();
|
|
70
|
-
|
|
105
|
+
|
|
106
|
+
// Update velocity history
|
|
107
|
+
if (velocity) {
|
|
108
|
+
this.updateVelocityHistory(isHorizontal ? velocity.x : velocity.y);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Determine scroll direction to optimize buffer distribution
|
|
112
|
+
const isScrollingBackward = this.isScrollingBackward();
|
|
113
|
+
const viewportStart =
|
|
114
|
+
this.enableOffsetProjection && !this.forceDisableOffsetProjection
|
|
115
|
+
? this.getProjectedScrollOffset(offset, this.averageRenderTime)
|
|
116
|
+
: offset;
|
|
117
|
+
|
|
118
|
+
// console.log("timeMs", this.averageRenderTime, offset, viewportStart);
|
|
119
|
+
|
|
71
120
|
const viewportSize = isHorizontal ? windowSize.width : windowSize.height;
|
|
72
121
|
const viewportEnd = viewportStart + viewportSize;
|
|
73
122
|
|
|
@@ -75,11 +124,6 @@ export class RVEngagedIndicesTrackerImpl implements RVEngagedIndicesTracker {
|
|
|
75
124
|
// The total extra space where items will be pre-rendered
|
|
76
125
|
const totalBuffer = this.drawDistance * 2;
|
|
77
126
|
|
|
78
|
-
// Determine scroll direction to optimize buffer distribution
|
|
79
|
-
const isScrollingBackward = this.isScrollingBackward(
|
|
80
|
-
isHorizontal ? velocity?.x : velocity?.y
|
|
81
|
-
);
|
|
82
|
-
|
|
83
127
|
// Distribute more buffer in the direction of scrolling
|
|
84
128
|
// When scrolling forward: more buffer after viewport
|
|
85
129
|
// When scrolling backward: more buffer before viewport
|
|
@@ -123,9 +167,12 @@ export class RVEngagedIndicesTrackerImpl implements RVEngagedIndicesTracker {
|
|
|
123
167
|
extendedStart,
|
|
124
168
|
extendedEnd
|
|
125
169
|
);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
170
|
+
// console.log(
|
|
171
|
+
// "newEngagedIndices",
|
|
172
|
+
// newEngagedIndices,
|
|
173
|
+
// this.scrollOffset,
|
|
174
|
+
// viewportStart
|
|
175
|
+
// );
|
|
129
176
|
// Only return new indices if they've changed
|
|
130
177
|
const oldEngagedIndices = this.engagedIndices;
|
|
131
178
|
this.engagedIndices = newEngagedIndices;
|
|
@@ -135,19 +182,21 @@ export class RVEngagedIndicesTrackerImpl implements RVEngagedIndicesTracker {
|
|
|
135
182
|
: newEngagedIndices;
|
|
136
183
|
}
|
|
137
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Updates the velocity history with a new velocity value.
|
|
187
|
+
* @param velocity - Current scroll velocity component (x or y)
|
|
188
|
+
*/
|
|
189
|
+
private updateVelocityHistory(velocity: number) {
|
|
190
|
+
this.velocityHistory[this.velocityIndex] = velocity;
|
|
191
|
+
this.velocityIndex = (this.velocityIndex + 1) % this.velocityHistory.length;
|
|
192
|
+
}
|
|
193
|
+
|
|
138
194
|
/**
|
|
139
195
|
* Determines scroll direction by analyzing recent velocity history.
|
|
140
196
|
* Uses a majority voting system on the last 5 velocity values.
|
|
141
|
-
* @param velocity - Current scroll velocity component (x or y)
|
|
142
197
|
* @returns true if scrolling backward (negative direction), false otherwise
|
|
143
198
|
*/
|
|
144
|
-
private isScrollingBackward(
|
|
145
|
-
// update velocity history
|
|
146
|
-
if (velocity) {
|
|
147
|
-
this.velocityHistory[this.velocityIndex] = velocity;
|
|
148
|
-
this.velocityIndex =
|
|
149
|
-
(this.velocityIndex + 1) % this.velocityHistory.length;
|
|
150
|
-
}
|
|
199
|
+
private isScrollingBackward(): boolean {
|
|
151
200
|
// should decide based on whether we have more positive or negative values, use for loop
|
|
152
201
|
let positiveCount = 0;
|
|
153
202
|
let negativeCount = 0;
|
|
@@ -162,6 +211,40 @@ export class RVEngagedIndicesTrackerImpl implements RVEngagedIndicesTracker {
|
|
|
162
211
|
return positiveCount < negativeCount;
|
|
163
212
|
}
|
|
164
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Calculates the median velocity based on velocity history
|
|
216
|
+
* Medina works better agains outliers
|
|
217
|
+
* @returns Median velocity over the recent history
|
|
218
|
+
*/
|
|
219
|
+
private getMedianVelocity(): number {
|
|
220
|
+
// Make a copy of velocity history and sort it
|
|
221
|
+
const sortedVelocities = [...this.velocityHistory].sort(
|
|
222
|
+
(valueA, valueB) => valueA - valueB
|
|
223
|
+
);
|
|
224
|
+
const length = sortedVelocities.length;
|
|
225
|
+
|
|
226
|
+
// If length is odd, return the middle element
|
|
227
|
+
if (length % 2 === 1) {
|
|
228
|
+
return sortedVelocities[Math.floor(length / 2)];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// If length is even, return the average of the two middle elements
|
|
232
|
+
const midIndex = length / 2;
|
|
233
|
+
return (sortedVelocities[midIndex - 1] + sortedVelocities[midIndex]) / 2;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Projects the next scroll offset based on median velocity
|
|
238
|
+
* @param timeMs Time in milliseconds to predict ahead
|
|
239
|
+
* @returns Projected scroll offset
|
|
240
|
+
*/
|
|
241
|
+
private getProjectedScrollOffset(offset: number, timeMs: number): number {
|
|
242
|
+
const medianVelocity = this.getMedianVelocity();
|
|
243
|
+
// Convert time from ms to seconds for velocity calculation
|
|
244
|
+
// Predict next position: current position + (velocity * time)
|
|
245
|
+
return offset + medianVelocity * timeMs;
|
|
246
|
+
}
|
|
247
|
+
|
|
165
248
|
/**
|
|
166
249
|
* Calculates which items are currently visible in the viewport.
|
|
167
250
|
* Unlike getEngagedIndices, this doesn't include buffer items.
|
|
@@ -196,11 +279,23 @@ export class RVEngagedIndicesTrackerImpl implements RVEngagedIndicesTracker {
|
|
|
196
279
|
|
|
197
280
|
setScrollDirection(scrollDirection: "forward" | "backward") {
|
|
198
281
|
if (scrollDirection === "forward") {
|
|
199
|
-
this.velocityHistory = [
|
|
282
|
+
this.velocityHistory = [0, 0, 0, 0.1, 0.1];
|
|
200
283
|
this.velocityIndex = 0;
|
|
201
284
|
} else {
|
|
202
|
-
this.velocityHistory = [
|
|
285
|
+
this.velocityHistory = [0, 0, 0, -0.1, -0.1];
|
|
203
286
|
this.velocityIndex = 0;
|
|
204
287
|
}
|
|
205
288
|
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Resets the velocity history based on the current scroll direction.
|
|
292
|
+
* This ensures that the velocity history is always in sync with the current scroll direction.
|
|
293
|
+
*/
|
|
294
|
+
resetVelocityHistory() {
|
|
295
|
+
if (this.isScrollingBackward()) {
|
|
296
|
+
this.setScrollDirection("backward");
|
|
297
|
+
} else {
|
|
298
|
+
this.setScrollDirection("forward");
|
|
299
|
+
}
|
|
300
|
+
}
|
|
206
301
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { PlatformConfig } from "../../native/config/PlatformHelper";
|
|
2
|
+
import { AverageWindow } from "../../utils/AverageWindow";
|
|
3
|
+
|
|
4
|
+
export class RenderTimeTracker {
|
|
5
|
+
private renderTimeAvgWindow = new AverageWindow(5);
|
|
6
|
+
private lastTimerStartedAt = -1;
|
|
7
|
+
private maxRenderTime = 32; // TODO: Improve this even more
|
|
8
|
+
private defaultRenderTime = 16;
|
|
9
|
+
|
|
10
|
+
startTracking() {
|
|
11
|
+
if (!PlatformConfig.trackAverageRenderTimeForOffsetProjection) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (this.lastTimerStartedAt === -1) {
|
|
15
|
+
this.lastTimerStartedAt = Date.now();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
markRenderComplete() {
|
|
20
|
+
if (!PlatformConfig.trackAverageRenderTimeForOffsetProjection) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (this.lastTimerStartedAt !== -1) {
|
|
24
|
+
this.renderTimeAvgWindow.addValue(Date.now() - this.lastTimerStartedAt);
|
|
25
|
+
this.lastTimerStartedAt = -1;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
getAverageRenderTime() {
|
|
30
|
+
if (!PlatformConfig.trackAverageRenderTimeForOffsetProjection) {
|
|
31
|
+
return this.defaultRenderTime;
|
|
32
|
+
}
|
|
33
|
+
return Math.min(
|
|
34
|
+
this.maxRenderTime,
|
|
35
|
+
Math.max(Math.round(this.renderTimeAvgWindow.currentValue), 16)
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracks and calculates velocity for scroll/drag movements
|
|
3
|
+
* Used to determine momentum scrolling behavior
|
|
4
|
+
*/
|
|
5
|
+
export class VelocityTracker<T> {
|
|
6
|
+
/** Timestamp of the last velocity update */
|
|
7
|
+
private lastUpdateTime = Date.now();
|
|
8
|
+
/** Current velocity vector with x and y components */
|
|
9
|
+
private velocity = { x: 0, y: 0 };
|
|
10
|
+
|
|
11
|
+
/** Reference to the momentum end timeout */
|
|
12
|
+
private timeoutId: NodeJS.Timeout | null = null;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Calculates velocity based on position change over time
|
|
16
|
+
* @param newOffset Current position value
|
|
17
|
+
* @param oldOffset Previous position value
|
|
18
|
+
* @param isHorizontal Whether movement is horizontal (true) or vertical (false)
|
|
19
|
+
* @param isRTL Whether layout direction is right-to-left
|
|
20
|
+
* @param callback Function to call with velocity updates and momentum end signal
|
|
21
|
+
*/
|
|
22
|
+
computeVelocity(
|
|
23
|
+
newOffset: number,
|
|
24
|
+
oldOffset: number,
|
|
25
|
+
isHorizontal: boolean,
|
|
26
|
+
callback: (
|
|
27
|
+
velocity: { x: number; y: number },
|
|
28
|
+
isMomentumEnd: boolean
|
|
29
|
+
) => void
|
|
30
|
+
) {
|
|
31
|
+
// Clear any pending momentum end timeout
|
|
32
|
+
this.cleanUp();
|
|
33
|
+
// Calculate time since last update
|
|
34
|
+
const currentTime = Date.now();
|
|
35
|
+
const timeSinceLastUpdate = Math.max(1, currentTime - this.lastUpdateTime);
|
|
36
|
+
|
|
37
|
+
// Calculate velocity as distance/time
|
|
38
|
+
const newVelocity = (newOffset - oldOffset) / timeSinceLastUpdate;
|
|
39
|
+
|
|
40
|
+
// console.log(
|
|
41
|
+
// "newVelocity",
|
|
42
|
+
// newOffset,
|
|
43
|
+
// oldOffset,
|
|
44
|
+
// currentTime,
|
|
45
|
+
// this.lastUpdateTime,
|
|
46
|
+
// timeSinceLastUpdate,
|
|
47
|
+
// newVelocity
|
|
48
|
+
// );
|
|
49
|
+
this.lastUpdateTime = currentTime;
|
|
50
|
+
|
|
51
|
+
// Apply velocity to the correct axis
|
|
52
|
+
this.velocity.x = isHorizontal ? newVelocity : 0;
|
|
53
|
+
this.velocity.y = isHorizontal ? 0 : newVelocity;
|
|
54
|
+
|
|
55
|
+
// Trigger callback with current velocity
|
|
56
|
+
callback(this.velocity, false);
|
|
57
|
+
|
|
58
|
+
// Set timeout to signal momentum end after 100ms of no updates
|
|
59
|
+
this.timeoutId = setTimeout(() => {
|
|
60
|
+
this.cleanUp();
|
|
61
|
+
this.lastUpdateTime = Date.now();
|
|
62
|
+
this.velocity.x = 0;
|
|
63
|
+
this.velocity.y = 0;
|
|
64
|
+
callback(this.velocity, true);
|
|
65
|
+
}, 100);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Cleans up resources by clearing any pending timeout
|
|
70
|
+
*/
|
|
71
|
+
cleanUp() {
|
|
72
|
+
if (this.timeoutId !== null) {
|
|
73
|
+
clearTimeout(this.timeoutId);
|
|
74
|
+
this.timeoutId = null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|