@shopify/flash-list 1.8.0 → 2.0.0-alpha.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 +147 -26
- package/dist/FlashListProps.d.ts +65 -2
- package/dist/FlashListProps.d.ts.map +1 -1
- package/dist/__tests__/AverageWindow.test.js +35 -0
- package/dist/__tests__/AverageWindow.test.js.map +1 -1
- package/dist/enableNewCore.d.ts +3 -0
- package/dist/enableNewCore.d.ts.map +1 -0
- package/dist/enableNewCore.js +25 -0
- package/dist/enableNewCore.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -8
- package/dist/index.js.map +1 -1
- package/dist/recyclerview/RecycleKeyManager.d.ts +82 -0
- package/dist/recyclerview/RecycleKeyManager.d.ts.map +1 -0
- package/dist/recyclerview/RecycleKeyManager.js +135 -0
- package/dist/recyclerview/RecycleKeyManager.js.map +1 -0
- package/dist/recyclerview/RecyclerView.d.ts +12 -0
- package/dist/recyclerview/RecyclerView.d.ts.map +1 -0
- package/dist/recyclerview/RecyclerView.js +283 -0
- package/dist/recyclerview/RecyclerView.js.map +1 -0
- package/dist/recyclerview/RecyclerViewContextProvider.d.ts +12 -0
- package/dist/recyclerview/RecyclerViewContextProvider.d.ts.map +1 -0
- package/dist/recyclerview/RecyclerViewContextProvider.js +11 -0
- package/dist/recyclerview/RecyclerViewContextProvider.js.map +1 -0
- package/dist/recyclerview/RecyclerViewManager.d.ts +52 -0
- package/dist/recyclerview/RecyclerViewManager.d.ts.map +1 -0
- package/dist/recyclerview/RecyclerViewManager.js +323 -0
- package/dist/recyclerview/RecyclerViewManager.js.map +1 -0
- package/dist/recyclerview/RecyclerViewProps.d.ts +9 -0
- package/dist/recyclerview/RecyclerViewProps.d.ts.map +1 -0
- package/dist/recyclerview/RecyclerViewProps.js +3 -0
- package/dist/recyclerview/RecyclerViewProps.js.map +1 -0
- package/dist/recyclerview/ViewHolder.d.ts +45 -0
- package/dist/recyclerview/ViewHolder.d.ts.map +1 -0
- package/dist/recyclerview/ViewHolder.js +96 -0
- package/dist/recyclerview/ViewHolder.js.map +1 -0
- package/dist/recyclerview/ViewHolderCollection.d.ts +57 -0
- package/dist/recyclerview/ViewHolderCollection.d.ts.map +1 -0
- package/dist/recyclerview/ViewHolderCollection.js +75 -0
- package/dist/recyclerview/ViewHolderCollection.js.map +1 -0
- package/dist/recyclerview/components/CompatScroller.d.ts +7 -0
- package/dist/recyclerview/components/CompatScroller.d.ts.map +1 -0
- package/dist/recyclerview/components/CompatScroller.js +8 -0
- package/dist/recyclerview/components/CompatScroller.js.map +1 -0
- package/dist/recyclerview/components/CompatView.d.ts +7 -0
- package/dist/recyclerview/components/CompatView.d.ts.map +1 -0
- package/dist/recyclerview/components/CompatView.js +8 -0
- package/dist/recyclerview/components/CompatView.js.map +1 -0
- package/dist/recyclerview/components/ScrollAnchor.d.ts +28 -0
- package/dist/recyclerview/components/ScrollAnchor.d.ts.map +1 -0
- package/dist/recyclerview/components/ScrollAnchor.js +35 -0
- package/dist/recyclerview/components/ScrollAnchor.js.map +1 -0
- package/dist/recyclerview/components/StickyHeaders.d.ts +38 -0
- package/dist/recyclerview/components/StickyHeaders.d.ts.map +1 -0
- package/dist/recyclerview/components/StickyHeaders.js +119 -0
- package/dist/recyclerview/components/StickyHeaders.js.map +1 -0
- package/dist/recyclerview/helpers/ConsecutiveNumbers.d.ts +51 -0
- package/dist/recyclerview/helpers/ConsecutiveNumbers.d.ts.map +1 -0
- package/dist/recyclerview/helpers/ConsecutiveNumbers.js +122 -0
- package/dist/recyclerview/helpers/ConsecutiveNumbers.js.map +1 -0
- package/dist/recyclerview/helpers/EngagedIndicesTracker.d.ts +59 -0
- package/dist/recyclerview/helpers/EngagedIndicesTracker.d.ts.map +1 -0
- package/dist/recyclerview/helpers/EngagedIndicesTracker.js +138 -0
- package/dist/recyclerview/helpers/EngagedIndicesTracker.js.map +1 -0
- package/dist/recyclerview/hooks/useBoundDetection.d.ts +19 -0
- package/dist/recyclerview/hooks/useBoundDetection.d.ts.map +1 -0
- package/dist/recyclerview/hooks/useBoundDetection.js +103 -0
- package/dist/recyclerview/hooks/useBoundDetection.js.map +1 -0
- package/dist/recyclerview/hooks/useLayoutState.d.ts +12 -0
- package/dist/recyclerview/hooks/useLayoutState.d.ts.map +1 -0
- package/dist/recyclerview/hooks/useLayoutState.js +43 -0
- package/dist/recyclerview/hooks/useLayoutState.js.map +1 -0
- package/dist/recyclerview/hooks/useOnLoad.d.ts +25 -0
- package/dist/recyclerview/hooks/useOnLoad.d.ts.map +1 -0
- package/dist/recyclerview/hooks/useOnLoad.js +73 -0
- package/dist/recyclerview/hooks/useOnLoad.js.map +1 -0
- package/dist/recyclerview/hooks/useRecyclerViewController.d.ts +72 -0
- package/dist/recyclerview/hooks/useRecyclerViewController.d.ts.map +1 -0
- package/dist/recyclerview/hooks/useRecyclerViewController.js +370 -0
- package/dist/recyclerview/hooks/useRecyclerViewController.js.map +1 -0
- package/dist/recyclerview/hooks/useRecyclerViewManager.d.ts +6 -0
- package/dist/recyclerview/hooks/useRecyclerViewManager.d.ts.map +1 -0
- package/dist/recyclerview/hooks/useRecyclerViewManager.js +27 -0
- package/dist/recyclerview/hooks/useRecyclerViewManager.js.map +1 -0
- package/dist/recyclerview/hooks/useRecyclingState.d.ts +16 -0
- package/dist/recyclerview/hooks/useRecyclingState.d.ts.map +1 -0
- package/dist/recyclerview/hooks/useRecyclingState.js +54 -0
- package/dist/recyclerview/hooks/useRecyclingState.js.map +1 -0
- package/dist/recyclerview/hooks/useSecondaryProps.d.ts +27 -0
- package/dist/recyclerview/hooks/useSecondaryProps.d.ts.map +1 -0
- package/dist/recyclerview/hooks/useSecondaryProps.js +93 -0
- package/dist/recyclerview/hooks/useSecondaryProps.js.map +1 -0
- package/dist/recyclerview/hooks/useUnmountFlag.d.ts +11 -0
- package/dist/recyclerview/hooks/useUnmountFlag.d.ts.map +1 -0
- package/dist/recyclerview/hooks/useUnmountFlag.js +28 -0
- package/dist/recyclerview/hooks/useUnmountFlag.js.map +1 -0
- package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts +65 -0
- package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts.map +1 -0
- package/dist/recyclerview/layout-managers/GridLayoutManager.js +204 -0
- package/dist/recyclerview/layout-managers/GridLayoutManager.js.map +1 -0
- package/dist/recyclerview/layout-managers/LayoutManager.d.ts +281 -0
- package/dist/recyclerview/layout-managers/LayoutManager.d.ts.map +1 -0
- package/dist/recyclerview/layout-managers/LayoutManager.js +250 -0
- package/dist/recyclerview/layout-managers/LayoutManager.js.map +1 -0
- package/dist/recyclerview/layout-managers/LinearLayoutManager.d.ts +52 -0
- package/dist/recyclerview/layout-managers/LinearLayoutManager.d.ts.map +1 -0
- package/dist/recyclerview/layout-managers/LinearLayoutManager.js +191 -0
- package/dist/recyclerview/layout-managers/LinearLayoutManager.js.map +1 -0
- package/dist/recyclerview/layout-managers/MasonryLayoutManager.d.ts +73 -0
- package/dist/recyclerview/layout-managers/MasonryLayoutManager.d.ts.map +1 -0
- package/dist/recyclerview/layout-managers/MasonryLayoutManager.js +274 -0
- package/dist/recyclerview/layout-managers/MasonryLayoutManager.js.map +1 -0
- package/dist/recyclerview/utils/adjustOffsetForRTL.d.ts +12 -0
- package/dist/recyclerview/utils/adjustOffsetForRTL.d.ts.map +1 -0
- package/dist/recyclerview/utils/adjustOffsetForRTL.js +18 -0
- package/dist/recyclerview/utils/adjustOffsetForRTL.js.map +1 -0
- package/dist/recyclerview/utils/componentUtils.d.ts +19 -0
- package/dist/recyclerview/utils/componentUtils.d.ts.map +1 -0
- package/dist/recyclerview/utils/componentUtils.js +32 -0
- package/dist/recyclerview/utils/componentUtils.js.map +1 -0
- package/dist/recyclerview/utils/findVisibleIndex.d.ts +24 -0
- package/dist/recyclerview/utils/findVisibleIndex.d.ts.map +1 -0
- package/dist/recyclerview/utils/findVisibleIndex.js +82 -0
- package/dist/recyclerview/utils/findVisibleIndex.js.map +1 -0
- package/dist/recyclerview/utils/measureLayout.d.ts +56 -0
- package/dist/recyclerview/utils/measureLayout.d.ts.map +1 -0
- package/dist/recyclerview/utils/measureLayout.js +77 -0
- package/dist/recyclerview/utils/measureLayout.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utils/AverageWindow.d.ts +13 -0
- package/dist/utils/AverageWindow.d.ts.map +1 -1
- package/dist/utils/AverageWindow.js +30 -1
- package/dist/utils/AverageWindow.js.map +1 -1
- package/package.json +1 -1
- package/src/FlashListProps.ts +73 -2
- package/src/__tests__/AverageWindow.test.ts +49 -1
- package/src/enableNewCore.ts +22 -0
- package/src/index.ts +21 -0
- package/src/recyclerview/RecycleKeyManager.ts +185 -0
- package/src/recyclerview/RecyclerView.tsx +500 -0
- package/src/recyclerview/RecyclerViewContextProvider.ts +19 -0
- package/src/recyclerview/RecyclerViewManager.ts +379 -0
- package/src/recyclerview/RecyclerViewProps.ts +10 -0
- package/src/recyclerview/ViewHolder.tsx +173 -0
- package/src/recyclerview/ViewHolderCollection.tsx +164 -0
- package/src/recyclerview/components/CompatScroller.ts +9 -0
- package/src/recyclerview/components/CompatView.ts +9 -0
- package/src/recyclerview/components/ScrollAnchor.tsx +53 -0
- package/src/recyclerview/components/StickyHeaders.tsx +210 -0
- package/src/recyclerview/helpers/ConsecutiveNumbers.ts +120 -0
- package/src/recyclerview/helpers/EngagedIndicesTracker.ts +191 -0
- package/src/recyclerview/hooks/useBoundDetection.ts +127 -0
- package/src/recyclerview/hooks/useLayoutState.ts +46 -0
- package/src/recyclerview/hooks/useOnLoad.ts +78 -0
- package/src/recyclerview/hooks/useRecyclerViewController.tsx +487 -0
- package/src/recyclerview/hooks/useRecyclerViewManager.ts +30 -0
- package/src/recyclerview/hooks/useRecyclingState.ts +63 -0
- package/src/recyclerview/hooks/useSecondaryProps.tsx +119 -0
- package/src/recyclerview/hooks/useUnmountFlag.ts +26 -0
- package/src/recyclerview/layout-managers/GridLayoutManager.ts +215 -0
- package/src/recyclerview/layout-managers/LayoutManager.ts +493 -0
- package/src/recyclerview/layout-managers/LinearLayoutManager.ts +167 -0
- package/src/recyclerview/layout-managers/MasonryLayoutManager.ts +302 -0
- package/src/recyclerview/utils/adjustOffsetForRTL.ts +17 -0
- package/src/recyclerview/utils/componentUtils.ts +28 -0
- package/src/recyclerview/utils/findVisibleIndex.ts +94 -0
- package/src/recyclerview/utils/measureLayout.ts +89 -0
- package/src/utils/AverageWindow.ts +33 -0
- package/src/viewability/ViewToken.ts +1 -1
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ViewHolderCollection is a container component that manages multiple ViewHolder instances.
|
|
3
|
+
* It handles the rendering of a collection of list items, manages layout updates,
|
|
4
|
+
* and coordinates with the RecyclerView context for layout changes.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React, { useEffect, useImperativeHandle, useLayoutEffect } from "react";
|
|
8
|
+
import { ViewHolder, ViewHolderProps } from "./ViewHolder";
|
|
9
|
+
import { RVDimension, RVLayout } from "./layout-managers/LayoutManager";
|
|
10
|
+
import { FlashListProps } from "../FlashListProps";
|
|
11
|
+
import { CompatView } from "./components/CompatView";
|
|
12
|
+
import { useRecyclerViewContext } from "./RecyclerViewContextProvider";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Props interface for the ViewHolderCollection component
|
|
16
|
+
* @template TItem - The type of items in the data array
|
|
17
|
+
*/
|
|
18
|
+
export interface ViewHolderCollectionProps<TItem> {
|
|
19
|
+
/** The data array to be rendered */
|
|
20
|
+
data: FlashListProps<TItem>["data"];
|
|
21
|
+
/** Map of indices to React keys for each rendered item */
|
|
22
|
+
renderStack: Map<number, string>;
|
|
23
|
+
/** Function to get layout information for a specific index */
|
|
24
|
+
getLayout: (index: number) => RVLayout;
|
|
25
|
+
/** Ref to control layout updates from parent components */
|
|
26
|
+
viewHolderCollectionRef: React.Ref<ViewHolderCollectionRef>;
|
|
27
|
+
/** Map to store refs for each ViewHolder instance */
|
|
28
|
+
refHolder: ViewHolderProps<TItem>["refHolder"];
|
|
29
|
+
/** Callback when any item's size changes */
|
|
30
|
+
onSizeChanged: ViewHolderProps<TItem>["onSizeChanged"];
|
|
31
|
+
/** Function to render each item */
|
|
32
|
+
renderItem: FlashListProps<TItem>["renderItem"];
|
|
33
|
+
/** Additional data passed to renderItem that can trigger re-renders */
|
|
34
|
+
extraData: any;
|
|
35
|
+
/** Function to get the container's layout dimensions */
|
|
36
|
+
getChildContainerLayout: () => RVDimension | undefined;
|
|
37
|
+
/** Callback after layout effects are committed */
|
|
38
|
+
onCommitLayoutEffect?: () => void;
|
|
39
|
+
/** Callback after effects are committed */
|
|
40
|
+
onCommitEffect?: () => void;
|
|
41
|
+
/** Optional custom component to wrap each item */
|
|
42
|
+
CellRendererComponent?: FlashListProps<TItem>["CellRendererComponent"];
|
|
43
|
+
/** Optional component to render between items */
|
|
44
|
+
ItemSeparatorComponent?: FlashListProps<TItem>["ItemSeparatorComponent"];
|
|
45
|
+
/** Whether the list is horizontal or vertical */
|
|
46
|
+
horizontal: FlashListProps<TItem>["horizontal"];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Ref interface for ViewHolderCollection that exposes methods to control layout updates
|
|
51
|
+
*/
|
|
52
|
+
export interface ViewHolderCollectionRef {
|
|
53
|
+
/** Forces a layout update by triggering a re-render */
|
|
54
|
+
commitLayout: () => void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* ViewHolderCollection component that manages the rendering of multiple ViewHolder instances
|
|
59
|
+
* and handles layout updates for the entire collection
|
|
60
|
+
* @template TItem - The type of items in the data array
|
|
61
|
+
*/
|
|
62
|
+
export const ViewHolderCollection = <TItem,>(
|
|
63
|
+
props: ViewHolderCollectionProps<TItem>
|
|
64
|
+
) => {
|
|
65
|
+
const {
|
|
66
|
+
data,
|
|
67
|
+
renderStack,
|
|
68
|
+
getLayout,
|
|
69
|
+
refHolder,
|
|
70
|
+
onSizeChanged,
|
|
71
|
+
renderItem,
|
|
72
|
+
extraData,
|
|
73
|
+
viewHolderCollectionRef,
|
|
74
|
+
getChildContainerLayout,
|
|
75
|
+
onCommitLayoutEffect,
|
|
76
|
+
CellRendererComponent,
|
|
77
|
+
ItemSeparatorComponent,
|
|
78
|
+
onCommitEffect,
|
|
79
|
+
horizontal,
|
|
80
|
+
} = props;
|
|
81
|
+
|
|
82
|
+
const [renderId, setRenderId] = React.useState(0);
|
|
83
|
+
|
|
84
|
+
const containerLayout = getChildContainerLayout();
|
|
85
|
+
|
|
86
|
+
// TODO: guard againt precision issues
|
|
87
|
+
const fixedContainerSize = horizontal
|
|
88
|
+
? containerLayout?.height
|
|
89
|
+
: containerLayout?.width;
|
|
90
|
+
|
|
91
|
+
const recyclerViewContext = useRecyclerViewContext();
|
|
92
|
+
|
|
93
|
+
useLayoutEffect(() => {
|
|
94
|
+
if (renderId > 0) {
|
|
95
|
+
// console.log(
|
|
96
|
+
// "parent layout trigger due to child container size change",
|
|
97
|
+
// fixedContainerSize
|
|
98
|
+
// );
|
|
99
|
+
recyclerViewContext?.layout();
|
|
100
|
+
}
|
|
101
|
+
}, [fixedContainerSize]);
|
|
102
|
+
|
|
103
|
+
useLayoutEffect(() => {
|
|
104
|
+
if (renderId > 0) {
|
|
105
|
+
onCommitLayoutEffect?.();
|
|
106
|
+
}
|
|
107
|
+
}, [renderId]);
|
|
108
|
+
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (renderId > 0) {
|
|
111
|
+
onCommitEffect?.();
|
|
112
|
+
}
|
|
113
|
+
}, [renderId]);
|
|
114
|
+
|
|
115
|
+
// Expose forceUpdate through ref
|
|
116
|
+
useImperativeHandle(viewHolderCollectionRef, () => ({
|
|
117
|
+
commitLayout: () => {
|
|
118
|
+
// This will trigger a re-render of the component
|
|
119
|
+
setRenderId((prev) => prev + 1);
|
|
120
|
+
},
|
|
121
|
+
}));
|
|
122
|
+
|
|
123
|
+
const hasData = data && data.length > 0;
|
|
124
|
+
|
|
125
|
+
const containerStyle = {
|
|
126
|
+
width: horizontal ? containerLayout?.width : undefined,
|
|
127
|
+
height: containerLayout?.height,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<CompatView
|
|
132
|
+
// TODO: Take care of web scroll bar here
|
|
133
|
+
style={hasData && containerStyle}
|
|
134
|
+
>
|
|
135
|
+
{containerLayout &&
|
|
136
|
+
hasData &&
|
|
137
|
+
Array.from(renderStack, ([index, reactKey]) => {
|
|
138
|
+
const item = data[index];
|
|
139
|
+
const trailingItem = ItemSeparatorComponent
|
|
140
|
+
? data[index + 1]
|
|
141
|
+
: undefined;
|
|
142
|
+
return (
|
|
143
|
+
<ViewHolder
|
|
144
|
+
key={reactKey}
|
|
145
|
+
index={index}
|
|
146
|
+
item={item}
|
|
147
|
+
trailingItem={trailingItem}
|
|
148
|
+
layout={{
|
|
149
|
+
...getLayout(index),
|
|
150
|
+
}}
|
|
151
|
+
refHolder={refHolder}
|
|
152
|
+
onSizeChanged={onSizeChanged}
|
|
153
|
+
target="Cell"
|
|
154
|
+
renderItem={renderItem}
|
|
155
|
+
extraData={extraData}
|
|
156
|
+
CellRendererComponent={CellRendererComponent}
|
|
157
|
+
ItemSeparatorComponent={ItemSeparatorComponent}
|
|
158
|
+
horizontal={horizontal}
|
|
159
|
+
/>
|
|
160
|
+
);
|
|
161
|
+
})}
|
|
162
|
+
</CompatView>
|
|
163
|
+
);
|
|
164
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ScrollView, Animated } from "react-native";
|
|
2
|
+
|
|
3
|
+
const AnimatedScrollView = Animated.ScrollView;
|
|
4
|
+
|
|
5
|
+
/** Regular scroll view component */
|
|
6
|
+
export { ScrollView as CompatScroller };
|
|
7
|
+
|
|
8
|
+
/** Animated scroll view component for smooth scrolling animations */
|
|
9
|
+
export { AnimatedScrollView as CompatAnimatedScroller };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ScrollAnchor component provides a mechanism to programmatically scroll
|
|
3
|
+
* the list by manipulating an invisible anchor element's position.
|
|
4
|
+
* This helps us use ScrollView's maintainVisibleContentPosition property
|
|
5
|
+
* to adjust the scroll position of the list as the size of content changes without any glitches.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useImperativeHandle, useMemo, useState } from "react";
|
|
9
|
+
import { CompatView } from "./CompatView";
|
|
10
|
+
import React from "react";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Props for the ScrollAnchor component
|
|
14
|
+
*/
|
|
15
|
+
export interface ScrollAnchorProps {
|
|
16
|
+
/** Ref to access scroll anchor methods */
|
|
17
|
+
scrollAnchorRef: React.Ref<ScrollAnchorRef>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Ref interface for ScrollAnchor component
|
|
22
|
+
*/
|
|
23
|
+
export interface ScrollAnchorRef {
|
|
24
|
+
/** Scrolls the list by the specified offset */
|
|
25
|
+
scrollBy: (offset: number) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* ScrollAnchor component that provides programmatic scrolling capabilities using maintainVisibleContentPosition property
|
|
30
|
+
* @param props - Component props
|
|
31
|
+
* @returns An invisible anchor element used for scrolling
|
|
32
|
+
*/
|
|
33
|
+
export function ScrollAnchor({ scrollAnchorRef }: ScrollAnchorProps) {
|
|
34
|
+
const [scrollOffset, setScrollOffset] = useState(1000000); //TODO: Fix this value
|
|
35
|
+
|
|
36
|
+
// Expose scrollBy method through ref
|
|
37
|
+
useImperativeHandle(scrollAnchorRef, () => ({
|
|
38
|
+
scrollBy: (offset: number) => {
|
|
39
|
+
setScrollOffset((prev) => prev + offset);
|
|
40
|
+
},
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
// Create an invisible anchor element that can be positioned
|
|
44
|
+
const anchor = useMemo(() => {
|
|
45
|
+
return (
|
|
46
|
+
<CompatView
|
|
47
|
+
style={{ position: "absolute", height: 0, top: scrollOffset }}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
}, [scrollOffset]);
|
|
51
|
+
|
|
52
|
+
return anchor;
|
|
53
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StickyHeaders component manages the sticky header behavior in a FlashList.
|
|
3
|
+
* It handles the animation and positioning of headers that should remain fixed
|
|
4
|
+
* at the top of the list while scrolling.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React, {
|
|
8
|
+
useRef,
|
|
9
|
+
useState,
|
|
10
|
+
useMemo,
|
|
11
|
+
useImperativeHandle,
|
|
12
|
+
useCallback,
|
|
13
|
+
useEffect,
|
|
14
|
+
} from "react";
|
|
15
|
+
import { Animated, NativeScrollEvent } from "react-native";
|
|
16
|
+
import { FlashListProps } from "../..";
|
|
17
|
+
import { CompatAnimatedView } from "./CompatView";
|
|
18
|
+
import { RecyclerViewManager } from "../RecyclerViewManager";
|
|
19
|
+
import { ViewHolder } from "../ViewHolder";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Props for the StickyHeaders component
|
|
23
|
+
* @template TItem - The type of items in the list
|
|
24
|
+
*/
|
|
25
|
+
export interface StickyHeaderProps<TItem> {
|
|
26
|
+
/** Array of indices that should have sticky headers */
|
|
27
|
+
stickyHeaderIndices: number[];
|
|
28
|
+
/** The data array being rendered */
|
|
29
|
+
data: readonly TItem[];
|
|
30
|
+
/** Animated value tracking scroll position */
|
|
31
|
+
scrollY: Animated.Value;
|
|
32
|
+
/** Function to render each item */
|
|
33
|
+
renderItem: FlashListProps<TItem>["renderItem"];
|
|
34
|
+
/** Ref to access sticky header methods */
|
|
35
|
+
stickyHeaderRef: React.RefObject<StickyHeaderRef>;
|
|
36
|
+
/** Manager for recycler view operations */
|
|
37
|
+
recyclerViewManager: RecyclerViewManager<TItem>;
|
|
38
|
+
/** Additional data to trigger re-renders */
|
|
39
|
+
extraData: FlashListProps<TItem>["extraData"];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Ref interface for StickyHeaders component
|
|
44
|
+
*/
|
|
45
|
+
export interface StickyHeaderRef {
|
|
46
|
+
/** Reports scroll events to update sticky header positions */
|
|
47
|
+
reportScrollEvent: (event: NativeScrollEvent) => void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const StickyHeaders = <TItem,>({
|
|
51
|
+
stickyHeaderIndices,
|
|
52
|
+
renderItem,
|
|
53
|
+
stickyHeaderRef,
|
|
54
|
+
recyclerViewManager,
|
|
55
|
+
scrollY,
|
|
56
|
+
data,
|
|
57
|
+
extraData,
|
|
58
|
+
}: StickyHeaderProps<TItem>) => {
|
|
59
|
+
const [stickyIndices, setStickyIndices] = useState<{
|
|
60
|
+
currentStickyIndex: number;
|
|
61
|
+
nextStickyIndex: number;
|
|
62
|
+
}>({ currentStickyIndex: -1, nextStickyIndex: -1 });
|
|
63
|
+
|
|
64
|
+
const { currentStickyIndex, nextStickyIndex } = stickyIndices;
|
|
65
|
+
const hasLayout = recyclerViewManager.hasLayout();
|
|
66
|
+
|
|
67
|
+
// Memoize sorted indices based on their Y positions
|
|
68
|
+
const sortedIndices = useMemo(() => {
|
|
69
|
+
if (!hasLayout) {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
return stickyHeaderIndices.sort((a, b) => a - b);
|
|
73
|
+
}, [stickyHeaderIndices, recyclerViewManager, hasLayout]);
|
|
74
|
+
|
|
75
|
+
const compute = useCallback(() => {
|
|
76
|
+
const adjustedValue = recyclerViewManager.getLastScrollOffset();
|
|
77
|
+
|
|
78
|
+
// Binary search for current sticky index
|
|
79
|
+
const currentIndexInArray = findCurrentStickyIndex(
|
|
80
|
+
sortedIndices,
|
|
81
|
+
adjustedValue,
|
|
82
|
+
(index) => recyclerViewManager.getLayout(index).y
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const newStickyIndex = sortedIndices[currentIndexInArray] ?? -1;
|
|
86
|
+
let newNextStickyIndex = sortedIndices[currentIndexInArray + 1] ?? -1;
|
|
87
|
+
|
|
88
|
+
if (newNextStickyIndex > recyclerViewManager.getEngagedIndices().endIndex) {
|
|
89
|
+
newNextStickyIndex = -1;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (
|
|
93
|
+
newStickyIndex !== currentStickyIndex ||
|
|
94
|
+
newNextStickyIndex !== nextStickyIndex
|
|
95
|
+
) {
|
|
96
|
+
setStickyIndices({
|
|
97
|
+
currentStickyIndex: newStickyIndex,
|
|
98
|
+
nextStickyIndex: newNextStickyIndex,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}, [currentStickyIndex, nextStickyIndex, recyclerViewManager, sortedIndices]);
|
|
102
|
+
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
compute();
|
|
105
|
+
}, [compute]);
|
|
106
|
+
|
|
107
|
+
// Optimized scroll handler using binary search pattern
|
|
108
|
+
useImperativeHandle(
|
|
109
|
+
stickyHeaderRef,
|
|
110
|
+
() => ({
|
|
111
|
+
reportScrollEvent: () => {
|
|
112
|
+
compute();
|
|
113
|
+
},
|
|
114
|
+
}),
|
|
115
|
+
[
|
|
116
|
+
stickyHeaderIndices,
|
|
117
|
+
recyclerViewManager,
|
|
118
|
+
currentStickyIndex,
|
|
119
|
+
nextStickyIndex,
|
|
120
|
+
]
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const refHolder = useRef(new Map()).current;
|
|
124
|
+
|
|
125
|
+
// Memoize translateY calculation
|
|
126
|
+
const translateY = useMemo(() => {
|
|
127
|
+
if (currentStickyIndex === -1 || nextStickyIndex === -1) {
|
|
128
|
+
return scrollY.interpolate({
|
|
129
|
+
inputRange: [0, Infinity],
|
|
130
|
+
outputRange: [0, 0],
|
|
131
|
+
extrapolate: "clamp",
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const currentLayout = recyclerViewManager.getLayout(currentStickyIndex);
|
|
136
|
+
const nextLayout = recyclerViewManager.getLayout(nextStickyIndex);
|
|
137
|
+
|
|
138
|
+
const pushStartsAt = nextLayout.y - currentLayout.height;
|
|
139
|
+
|
|
140
|
+
return scrollY.interpolate({
|
|
141
|
+
inputRange: [
|
|
142
|
+
pushStartsAt + recyclerViewManager.firstItemOffset,
|
|
143
|
+
nextLayout.y + recyclerViewManager.firstItemOffset,
|
|
144
|
+
],
|
|
145
|
+
outputRange: [0, -currentLayout.height],
|
|
146
|
+
extrapolate: "clamp",
|
|
147
|
+
});
|
|
148
|
+
}, [currentStickyIndex, nextStickyIndex, recyclerViewManager, scrollY]);
|
|
149
|
+
|
|
150
|
+
// Memoize header content
|
|
151
|
+
const headerContent = useMemo(() => {
|
|
152
|
+
if (currentStickyIndex === -1) return null;
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<CompatAnimatedView
|
|
156
|
+
style={{
|
|
157
|
+
position: "absolute",
|
|
158
|
+
top: 0,
|
|
159
|
+
left: 0,
|
|
160
|
+
right: 0,
|
|
161
|
+
transform: [{ translateY }],
|
|
162
|
+
}}
|
|
163
|
+
>
|
|
164
|
+
<ViewHolder
|
|
165
|
+
index={currentStickyIndex}
|
|
166
|
+
item={data[currentStickyIndex]}
|
|
167
|
+
renderItem={renderItem}
|
|
168
|
+
layout={{ x: 0, y: 0, width: 0, height: 0 }}
|
|
169
|
+
refHolder={refHolder}
|
|
170
|
+
extraData={extraData}
|
|
171
|
+
trailingItem={null}
|
|
172
|
+
target="StickyHeader"
|
|
173
|
+
/>
|
|
174
|
+
</CompatAnimatedView>
|
|
175
|
+
);
|
|
176
|
+
}, [currentStickyIndex, data, renderItem, extraData, refHolder, translateY]);
|
|
177
|
+
|
|
178
|
+
return headerContent;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Binary search utility to find the current sticky header index based on scroll position
|
|
183
|
+
* @param sortedIndices - Array of indices sorted by Y position
|
|
184
|
+
* @param adjustedValue - Current scroll position
|
|
185
|
+
* @param getY - Function to get Y position for an index
|
|
186
|
+
* @returns Index of the current sticky header
|
|
187
|
+
*/
|
|
188
|
+
function findCurrentStickyIndex(
|
|
189
|
+
sortedIndices: number[],
|
|
190
|
+
adjustedValue: number,
|
|
191
|
+
getY: (index: number) => number
|
|
192
|
+
): number {
|
|
193
|
+
let left = 0;
|
|
194
|
+
let right = sortedIndices.length - 1;
|
|
195
|
+
let result = -1;
|
|
196
|
+
|
|
197
|
+
while (left <= right) {
|
|
198
|
+
const mid = Math.floor((left + right) / 2);
|
|
199
|
+
const currentY = getY(sortedIndices[mid]);
|
|
200
|
+
|
|
201
|
+
if (currentY <= adjustedValue) {
|
|
202
|
+
result = mid;
|
|
203
|
+
left = mid + 1;
|
|
204
|
+
} else {
|
|
205
|
+
right = mid - 1;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A simple wrapper for consecutive integer arrays
|
|
3
|
+
* Only stores start and end indices for faster computation as numbers are consecutive.
|
|
4
|
+
*/
|
|
5
|
+
export class ConsecutiveNumbers {
|
|
6
|
+
constructor(
|
|
7
|
+
public readonly startIndex: number,
|
|
8
|
+
public readonly endIndex: number
|
|
9
|
+
) {}
|
|
10
|
+
|
|
11
|
+
static readonly EMPTY = new ConsecutiveNumbers(0, -1);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get the length of the array
|
|
15
|
+
*/
|
|
16
|
+
get length(): number {
|
|
17
|
+
return Math.max(0, this.endIndex - this.startIndex + 1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get element at specified index
|
|
22
|
+
*/
|
|
23
|
+
at(index: number): number {
|
|
24
|
+
return this.startIndex + index;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Check if two consecutive numbers are equal
|
|
29
|
+
*/
|
|
30
|
+
equals(other: ConsecutiveNumbers): boolean {
|
|
31
|
+
return (
|
|
32
|
+
this.startIndex === other.startIndex && this.endIndex === other.endIndex
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Converts the consecutive range to an actual array
|
|
38
|
+
* @returns An array containing all consecutive numbers
|
|
39
|
+
*/
|
|
40
|
+
toArray(): number[] {
|
|
41
|
+
if (this.length === 0) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
const array = new Array(this.length);
|
|
45
|
+
for (let i = 0; i < this.length; i++) {
|
|
46
|
+
array[i] = this.startIndex + i;
|
|
47
|
+
}
|
|
48
|
+
return array;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check if array includes a value
|
|
53
|
+
*/
|
|
54
|
+
includes(value: number): boolean {
|
|
55
|
+
return value >= this.startIndex && value <= this.endIndex;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get index of a value in the consecutive range
|
|
60
|
+
*/
|
|
61
|
+
indexOf(value: number): number {
|
|
62
|
+
return this.includes(value) ? value - this.startIndex : -1;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
findValue(
|
|
66
|
+
predicate: (
|
|
67
|
+
value: number,
|
|
68
|
+
index: number,
|
|
69
|
+
array: ConsecutiveNumbers
|
|
70
|
+
) => boolean | undefined
|
|
71
|
+
): number | undefined {
|
|
72
|
+
for (let i = 0; i < this.length; i++) {
|
|
73
|
+
const value = this.startIndex + i;
|
|
74
|
+
if (predicate(value, i, this)) {
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Tests whether all elements in the consecutive range pass the provided test function
|
|
83
|
+
* @param predicate A function that tests each element
|
|
84
|
+
* @returns true if all elements pass the test; otherwise, false
|
|
85
|
+
*/
|
|
86
|
+
every(
|
|
87
|
+
predicate: (
|
|
88
|
+
value: number,
|
|
89
|
+
index: number,
|
|
90
|
+
array: ConsecutiveNumbers
|
|
91
|
+
) => boolean | undefined
|
|
92
|
+
): boolean {
|
|
93
|
+
for (let i = 0; i < this.length; i++) {
|
|
94
|
+
const value = this.startIndex + i;
|
|
95
|
+
if (!predicate(value, i, this)) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get a slice of the consecutive array
|
|
104
|
+
*/
|
|
105
|
+
slice(start = 0, end = this.length): ConsecutiveNumbers {
|
|
106
|
+
const newStart = this.startIndex + start;
|
|
107
|
+
const newEnd = this.startIndex + Math.min(end, this.length) - 1;
|
|
108
|
+
|
|
109
|
+
return new ConsecutiveNumbers(newStart, Math.max(newStart - 1, newEnd));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Implement iterator to enable for...of
|
|
114
|
+
*/
|
|
115
|
+
*[Symbol.iterator]() {
|
|
116
|
+
for (let i = this.startIndex; i <= this.endIndex; i++) {
|
|
117
|
+
yield i;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|