Skip to content

Commit c8e47e9

Browse files
authored
Support multiple DraggableFlatLists within single parent ScrollView (#373)
* v3.0.5 * v3.0.6 * v3.0.7 * feat: support nested flatlists * comments * rename nested -> nestable * update export * update README * typo Co-authored-by: computerjazz <[email protected]>
1 parent 1779964 commit c8e47e9

File tree

11 files changed

+539
-5
lines changed

11 files changed

+539
-5
lines changed

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,38 @@ Cell Decorators are an easy way to add common hover animations. For example, wra
6464

6565
`ScaleDecorator`, `ShadowDecorator`, and `OpacityDecorator` are currently exported. Developers may create their own custom decorators using the animated values provided by the `useOnCellActiveAnimation` hook.
6666

67+
## Nesting DraggableFlatLists
68+
69+
Use an outer `NestableScrollContainer` paired with one or more inner `NestableDraggableFlatList` components to nest multiple separate `DraggableFlatList` components within a single scrollable parent. `NestableScrollContainer` extends a `ScrollView` from `react-native-gesture-handler`, and `NestableDraggableFlatList` shares the same API as a regular `DraggableFlatList`.
70+
71+
```tsx
72+
<NestableScrollContainer>
73+
<Header text='List 1' />
74+
<NestableDraggableFlatList
75+
data={data1}
76+
renderItem={renderItem}
77+
keyExtractor={keyExtractor}
78+
onDragEnd={({ data }) => setData1(data)}
79+
/>
80+
<Header text='List 2' />
81+
<NestableDraggableFlatList
82+
data={data2}
83+
renderItem={renderItem}
84+
keyExtractor={keyExtractor}
85+
onDragEnd={({ data }) => setData2(data)}
86+
/>
87+
<Header text='List 3' />
88+
<NestableDraggableFlatList
89+
data={data3}
90+
renderItem={renderItem}
91+
keyExtractor={keyExtractor}
92+
onDragEnd={({ data }) => setData3(data)}
93+
/>
94+
</NestableScrollContainer>
95+
```
96+
97+
![Nested DraggableFlatList demo](https://i.imgur.com/Kv0aj4l.gif)
98+
6799
## Example
68100

69101
Example snack: https://snack.expo.io/@computerjazz/rndfl3 <br />

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-native-draggable-flatlist",
3-
"version": "3.0.4",
3+
"version": "3.0.7",
44
"description": "A drag-and-drop-enabled FlatList component for React Native",
55
"main": "lib/index.js",
66
"types": "lib/index.d.ts",

src/components/DraggableFlatList.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import Animated, {
3535
sub,
3636
} from "react-native-reanimated";
3737
import CellRendererComponent from "./CellRendererComponent";
38-
import { DEFAULT_PROPS, isReanimatedV2, isWeb } from "../constants";
38+
import { DEFAULT_PROPS, isWeb } from "../constants";
3939
import PlaceholderItem from "./PlaceholderItem";
4040
import RowItem from "./RowItem";
4141
import ScrollOffsetListener from "./ScrollOffsetListener";
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import React, { useMemo, useRef, useState } from "react";
2+
import { findNodeHandle, LogBox } from "react-native";
3+
import Animated, { add } from "react-native-reanimated";
4+
import DraggableFlatList, { DraggableFlatListProps } from "../index";
5+
import { useNestableScrollContainerContext } from "../context/nestableScrollContainerContext";
6+
import { useNestedAutoScroll } from "../hooks/useNestedAutoScroll";
7+
8+
export function NestableDraggableFlatList<T>(props: DraggableFlatListProps<T>) {
9+
const hasSuppressedWarnings = useRef(false);
10+
11+
if (!hasSuppressedWarnings.current) {
12+
LogBox.ignoreLogs([
13+
"VirtualizedLists should never be nested inside plain ScrollViews with the same orientation because it can break windowing",
14+
]); // Ignore log notification by message
15+
//@ts-ignore
16+
console.reportErrorsAsExceptions = false;
17+
hasSuppressedWarnings.current = true;
18+
}
19+
20+
const {
21+
containerRef,
22+
outerScrollOffset,
23+
setOuterScrollEnabled,
24+
} = useNestableScrollContainerContext();
25+
26+
const listVerticalOffset = useMemo(() => new Animated.Value<number>(0), []);
27+
const viewRef = useRef<Animated.View>(null);
28+
const [animVals, setAnimVals] = useState({});
29+
30+
useNestedAutoScroll(animVals);
31+
32+
const onListContainerLayout = async () => {
33+
const viewNode = viewRef.current;
34+
const nodeHandle = findNodeHandle(containerRef.current);
35+
36+
const onSuccess = (_x: number, y: number) => {
37+
listVerticalOffset.setValue(y);
38+
};
39+
const onFail = () => {
40+
console.log("## nested draggable list measure fail");
41+
};
42+
//@ts-ignore
43+
viewNode.measureLayout(nodeHandle, onSuccess, onFail);
44+
};
45+
46+
return (
47+
<Animated.View ref={viewRef} onLayout={onListContainerLayout}>
48+
<DraggableFlatList
49+
activationDistance={20}
50+
autoscrollSpeed={50}
51+
scrollEnabled={false}
52+
{...props}
53+
outerScrollOffset={outerScrollOffset}
54+
onDragBegin={(...args) => {
55+
setOuterScrollEnabled(false);
56+
props.onDragBegin?.(...args);
57+
}}
58+
onDragEnd={(...args) => {
59+
props.onDragEnd?.(...args);
60+
setOuterScrollEnabled(true);
61+
}}
62+
onAnimValInit={(animVals) => {
63+
setAnimVals({
64+
...animVals,
65+
hoverAnim: add(animVals.hoverAnim, listVerticalOffset),
66+
});
67+
props.onAnimValInit?.(animVals);
68+
}}
69+
/>
70+
</Animated.View>
71+
);
72+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import React, { useMemo } from "react";
2+
import { NativeScrollEvent, ScrollViewProps } from "react-native";
3+
import { ScrollView } from "react-native-gesture-handler";
4+
import Animated, { block, set } from "react-native-reanimated";
5+
import {
6+
NestableScrollContainerProvider,
7+
useNestableScrollContainerContext,
8+
} from "../context/nestableScrollContainerContext";
9+
10+
const AnimatedScrollView = Animated.createAnimatedComponent(ScrollView);
11+
12+
function NestableScrollContainerInner(props: ScrollViewProps) {
13+
const {
14+
outerScrollOffset,
15+
containerRef,
16+
containerSize,
17+
scrollViewSize,
18+
scrollableRef,
19+
outerScrollEnabled,
20+
} = useNestableScrollContainerContext();
21+
22+
const onScroll = useMemo(
23+
() =>
24+
Animated.event([
25+
{
26+
nativeEvent: ({ contentOffset }: NativeScrollEvent) =>
27+
block([set(outerScrollOffset, contentOffset.y)]),
28+
},
29+
]),
30+
[]
31+
);
32+
33+
return (
34+
<Animated.View
35+
ref={containerRef}
36+
onLayout={({ nativeEvent: { layout } }) => {
37+
containerSize.setValue(layout.height);
38+
}}
39+
>
40+
<AnimatedScrollView
41+
{...props}
42+
onContentSizeChange={(w, h) => {
43+
scrollViewSize.setValue(h);
44+
props.onContentSizeChange?.(w, h);
45+
}}
46+
scrollEnabled={outerScrollEnabled}
47+
ref={scrollableRef}
48+
scrollEventThrottle={1}
49+
onScroll={onScroll}
50+
/>
51+
</Animated.View>
52+
);
53+
}
54+
55+
export function NestableScrollContainer(props: ScrollViewProps) {
56+
return (
57+
<NestableScrollContainerProvider>
58+
<NestableScrollContainerInner {...props} />
59+
</NestableScrollContainerProvider>
60+
);
61+
}

src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export const DEFAULT_PROPS = {
2424
dragHitSlop: 0 as PanGestureHandlerProperties["hitSlop"],
2525
activationDistance: 0,
2626
dragItemOverflow: false,
27+
outerScrollOffset: new Animated.Value<number>(0),
2728
};
2829

2930
export const isIOS = Platform.OS === "ios";

src/context/animatedValueContext.tsx

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
1-
import React, { useContext } from "react";
1+
import React, { useContext, useEffect, useMemo } from "react";
22
import Animated, {
33
add,
44
and,
55
block,
66
greaterThan,
77
max,
88
min,
9+
onChange,
910
set,
1011
sub,
12+
useCode,
1113
useValue,
1214
} from "react-native-reanimated";
1315
import { State as GestureState } from "react-native-gesture-handler";
1416
import { useNode } from "../hooks/useNode";
15-
import { useMemo } from "react";
1617
import { useProps } from "./propsContext";
18+
import { DEFAULT_PROPS } from "../constants";
1719

1820
if (!useValue) {
1921
throw new Error("Incompatible Reanimated version (useValue not found)");
@@ -73,12 +75,23 @@ function useSetupAnimatedValues<T>() {
7375

7476
const scrollOffset = useValue<number>(0);
7577

78+
const outerScrollOffset =
79+
props.outerScrollOffset || DEFAULT_PROPS.outerScrollOffset;
80+
const outerScrollOffsetSnapshot = useValue<number>(0); // Amount any outer scrollview has scrolled since last gesture event.
81+
const outerScrollOffsetDiff = sub(
82+
outerScrollOffset,
83+
outerScrollOffsetSnapshot
84+
);
85+
7686
const scrollViewSize = useValue<number>(0);
7787

7888
const touchCellOffset = useNode(sub(touchInit, activeCellOffset));
7989

8090
const hoverAnimUnconstrained = useNode(
81-
sub(sub(touchAbsolute, activationDistance), touchCellOffset)
91+
add(
92+
outerScrollOffsetDiff,
93+
sub(sub(touchAbsolute, activationDistance), touchCellOffset)
94+
)
8295
);
8396

8497
const hoverAnimConstrained = useNode(
@@ -91,6 +104,17 @@ function useSetupAnimatedValues<T>() {
91104

92105
const hoverOffset = useNode(add(hoverAnim, scrollOffset));
93106

107+
useCode(
108+
() =>
109+
onChange(
110+
touchAbsolute,
111+
// If the list is being used in "nested" mode (ie. there's an outer scrollview that contains the list)
112+
// then we need a way to track the amound the outer list has auto-scrolled during the current touch position.
113+
set(outerScrollOffsetSnapshot, outerScrollOffset)
114+
),
115+
[outerScrollOffset]
116+
);
117+
94118
const placeholderOffset = useValue<number>(0);
95119

96120
// Note: this could use a refactor as it combines touch state + cell animation
@@ -154,5 +178,9 @@ function useSetupAnimatedValues<T>() {
154178
]
155179
);
156180

181+
useEffect(() => {
182+
props.onAnimValInit?.(value);
183+
}, [value]);
184+
157185
return value;
158186
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import React, { useContext, useMemo, useRef, useState } from "react";
2+
import { ScrollView } from "react-native-gesture-handler";
3+
import Animated from "react-native-reanimated";
4+
5+
type NestableScrollContainerContextVal = ReturnType<
6+
typeof useSetupNestableScrollContextValue
7+
>;
8+
const NestableScrollContainerContext = React.createContext<
9+
NestableScrollContainerContextVal | undefined
10+
>(undefined);
11+
12+
function useSetupNestableScrollContextValue() {
13+
const [outerScrollEnabled, setOuterScrollEnabled] = useState(true);
14+
const scrollViewSize = useMemo(() => new Animated.Value<number>(0), []);
15+
const scrollableRef = useRef<ScrollView>(null);
16+
const outerScrollOffset = useMemo(() => new Animated.Value<number>(0), []);
17+
const containerRef = useRef<Animated.View>(null);
18+
const containerSize = useMemo(() => new Animated.Value<number>(0), []);
19+
20+
const contextVal = useMemo(
21+
() => ({
22+
outerScrollEnabled,
23+
setOuterScrollEnabled,
24+
outerScrollOffset,
25+
scrollViewSize,
26+
scrollableRef,
27+
containerRef,
28+
containerSize,
29+
}),
30+
[outerScrollEnabled]
31+
);
32+
33+
return contextVal;
34+
}
35+
36+
export function NestableScrollContainerProvider({
37+
children,
38+
}: {
39+
children: React.ReactNode;
40+
}) {
41+
const contextVal = useSetupNestableScrollContextValue();
42+
return (
43+
<NestableScrollContainerContext.Provider value={contextVal}>
44+
{children}
45+
</NestableScrollContainerContext.Provider>
46+
);
47+
}
48+
49+
export function useNestableScrollContainerContext() {
50+
const value = useContext(NestableScrollContainerContext);
51+
if (!value) {
52+
throw new Error(
53+
"useNestableScrollContainerContext must be called from within NestableScrollContainerContext Provider!"
54+
);
55+
}
56+
return value;
57+
}

0 commit comments

Comments
 (0)