Skip to content

Commit

Permalink
Support multiple DraggableFlatLists within single parent ScrollView (#…
Browse files Browse the repository at this point in the history
…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]>
  • Loading branch information
computerjazz and computerjazz authored Apr 13, 2022
1 parent 1779964 commit c8e47e9
Show file tree
Hide file tree
Showing 11 changed files with 539 additions and 5 deletions.
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,38 @@ Cell Decorators are an easy way to add common hover animations. For example, wra

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

## Nesting DraggableFlatLists

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`.

```tsx
<NestableScrollContainer>
<Header text='List 1' />
<NestableDraggableFlatList
data={data1}
renderItem={renderItem}
keyExtractor={keyExtractor}
onDragEnd={({ data }) => setData1(data)}
/>
<Header text='List 2' />
<NestableDraggableFlatList
data={data2}
renderItem={renderItem}
keyExtractor={keyExtractor}
onDragEnd={({ data }) => setData2(data)}
/>
<Header text='List 3' />
<NestableDraggableFlatList
data={data3}
renderItem={renderItem}
keyExtractor={keyExtractor}
onDragEnd={({ data }) => setData3(data)}
/>
</NestableScrollContainer>
```

![Nested DraggableFlatList demo](https://i.imgur.com/Kv0aj4l.gif)

## Example

Example snack: https://snack.expo.io/@computerjazz/rndfl3 <br />
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-native-draggable-flatlist",
"version": "3.0.4",
"version": "3.0.7",
"description": "A drag-and-drop-enabled FlatList component for React Native",
"main": "lib/index.js",
"types": "lib/index.d.ts",
Expand Down
2 changes: 1 addition & 1 deletion src/components/DraggableFlatList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import Animated, {
sub,
} from "react-native-reanimated";
import CellRendererComponent from "./CellRendererComponent";
import { DEFAULT_PROPS, isReanimatedV2, isWeb } from "../constants";
import { DEFAULT_PROPS, isWeb } from "../constants";
import PlaceholderItem from "./PlaceholderItem";
import RowItem from "./RowItem";
import ScrollOffsetListener from "./ScrollOffsetListener";
Expand Down
72 changes: 72 additions & 0 deletions src/components/NestableDraggableFlatList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React, { useMemo, useRef, useState } from "react";
import { findNodeHandle, LogBox } from "react-native";
import Animated, { add } from "react-native-reanimated";
import DraggableFlatList, { DraggableFlatListProps } from "../index";
import { useNestableScrollContainerContext } from "../context/nestableScrollContainerContext";
import { useNestedAutoScroll } from "../hooks/useNestedAutoScroll";

export function NestableDraggableFlatList<T>(props: DraggableFlatListProps<T>) {
const hasSuppressedWarnings = useRef(false);

if (!hasSuppressedWarnings.current) {
LogBox.ignoreLogs([
"VirtualizedLists should never be nested inside plain ScrollViews with the same orientation because it can break windowing",
]); // Ignore log notification by message
//@ts-ignore
console.reportErrorsAsExceptions = false;
hasSuppressedWarnings.current = true;
}

const {
containerRef,
outerScrollOffset,
setOuterScrollEnabled,
} = useNestableScrollContainerContext();

const listVerticalOffset = useMemo(() => new Animated.Value<number>(0), []);
const viewRef = useRef<Animated.View>(null);
const [animVals, setAnimVals] = useState({});

useNestedAutoScroll(animVals);

const onListContainerLayout = async () => {
const viewNode = viewRef.current;
const nodeHandle = findNodeHandle(containerRef.current);

const onSuccess = (_x: number, y: number) => {
listVerticalOffset.setValue(y);
};
const onFail = () => {
console.log("## nested draggable list measure fail");
};
//@ts-ignore
viewNode.measureLayout(nodeHandle, onSuccess, onFail);
};

return (
<Animated.View ref={viewRef} onLayout={onListContainerLayout}>
<DraggableFlatList
activationDistance={20}
autoscrollSpeed={50}
scrollEnabled={false}
{...props}
outerScrollOffset={outerScrollOffset}
onDragBegin={(...args) => {
setOuterScrollEnabled(false);
props.onDragBegin?.(...args);
}}
onDragEnd={(...args) => {
props.onDragEnd?.(...args);
setOuterScrollEnabled(true);
}}
onAnimValInit={(animVals) => {
setAnimVals({
...animVals,
hoverAnim: add(animVals.hoverAnim, listVerticalOffset),
});
props.onAnimValInit?.(animVals);
}}
/>
</Animated.View>
);
}
61 changes: 61 additions & 0 deletions src/components/NestableScrollContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React, { useMemo } from "react";
import { NativeScrollEvent, ScrollViewProps } from "react-native";
import { ScrollView } from "react-native-gesture-handler";
import Animated, { block, set } from "react-native-reanimated";
import {
NestableScrollContainerProvider,
useNestableScrollContainerContext,
} from "../context/nestableScrollContainerContext";

const AnimatedScrollView = Animated.createAnimatedComponent(ScrollView);

function NestableScrollContainerInner(props: ScrollViewProps) {
const {
outerScrollOffset,
containerRef,
containerSize,
scrollViewSize,
scrollableRef,
outerScrollEnabled,
} = useNestableScrollContainerContext();

const onScroll = useMemo(
() =>
Animated.event([
{
nativeEvent: ({ contentOffset }: NativeScrollEvent) =>
block([set(outerScrollOffset, contentOffset.y)]),
},
]),
[]
);

return (
<Animated.View
ref={containerRef}
onLayout={({ nativeEvent: { layout } }) => {
containerSize.setValue(layout.height);
}}
>
<AnimatedScrollView
{...props}
onContentSizeChange={(w, h) => {
scrollViewSize.setValue(h);
props.onContentSizeChange?.(w, h);
}}
scrollEnabled={outerScrollEnabled}
ref={scrollableRef}
scrollEventThrottle={1}
onScroll={onScroll}
/>
</Animated.View>
);
}

export function NestableScrollContainer(props: ScrollViewProps) {
return (
<NestableScrollContainerProvider>
<NestableScrollContainerInner {...props} />
</NestableScrollContainerProvider>
);
}
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const DEFAULT_PROPS = {
dragHitSlop: 0 as PanGestureHandlerProperties["hitSlop"],
activationDistance: 0,
dragItemOverflow: false,
outerScrollOffset: new Animated.Value<number>(0),
};

export const isIOS = Platform.OS === "ios";
Expand Down
34 changes: 31 additions & 3 deletions src/context/animatedValueContext.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import React, { useContext } from "react";
import React, { useContext, useEffect, useMemo } from "react";
import Animated, {
add,
and,
block,
greaterThan,
max,
min,
onChange,
set,
sub,
useCode,
useValue,
} from "react-native-reanimated";
import { State as GestureState } from "react-native-gesture-handler";
import { useNode } from "../hooks/useNode";
import { useMemo } from "react";
import { useProps } from "./propsContext";
import { DEFAULT_PROPS } from "../constants";

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

const scrollOffset = useValue<number>(0);

const outerScrollOffset =
props.outerScrollOffset || DEFAULT_PROPS.outerScrollOffset;
const outerScrollOffsetSnapshot = useValue<number>(0); // Amount any outer scrollview has scrolled since last gesture event.
const outerScrollOffsetDiff = sub(
outerScrollOffset,
outerScrollOffsetSnapshot
);

const scrollViewSize = useValue<number>(0);

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

const hoverAnimUnconstrained = useNode(
sub(sub(touchAbsolute, activationDistance), touchCellOffset)
add(
outerScrollOffsetDiff,
sub(sub(touchAbsolute, activationDistance), touchCellOffset)
)
);

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

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

useCode(
() =>
onChange(
touchAbsolute,
// If the list is being used in "nested" mode (ie. there's an outer scrollview that contains the list)
// then we need a way to track the amound the outer list has auto-scrolled during the current touch position.
set(outerScrollOffsetSnapshot, outerScrollOffset)
),
[outerScrollOffset]
);

const placeholderOffset = useValue<number>(0);

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

useEffect(() => {
props.onAnimValInit?.(value);
}, [value]);

return value;
}
57 changes: 57 additions & 0 deletions src/context/nestableScrollContainerContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React, { useContext, useMemo, useRef, useState } from "react";
import { ScrollView } from "react-native-gesture-handler";
import Animated from "react-native-reanimated";

type NestableScrollContainerContextVal = ReturnType<
typeof useSetupNestableScrollContextValue
>;
const NestableScrollContainerContext = React.createContext<
NestableScrollContainerContextVal | undefined
>(undefined);

function useSetupNestableScrollContextValue() {
const [outerScrollEnabled, setOuterScrollEnabled] = useState(true);
const scrollViewSize = useMemo(() => new Animated.Value<number>(0), []);
const scrollableRef = useRef<ScrollView>(null);
const outerScrollOffset = useMemo(() => new Animated.Value<number>(0), []);
const containerRef = useRef<Animated.View>(null);
const containerSize = useMemo(() => new Animated.Value<number>(0), []);

const contextVal = useMemo(
() => ({
outerScrollEnabled,
setOuterScrollEnabled,
outerScrollOffset,
scrollViewSize,
scrollableRef,
containerRef,
containerSize,
}),
[outerScrollEnabled]
);

return contextVal;
}

export function NestableScrollContainerProvider({
children,
}: {
children: React.ReactNode;
}) {
const contextVal = useSetupNestableScrollContextValue();
return (
<NestableScrollContainerContext.Provider value={contextVal}>
{children}
</NestableScrollContainerContext.Provider>
);
}

export function useNestableScrollContainerContext() {
const value = useContext(NestableScrollContainerContext);
if (!value) {
throw new Error(
"useNestableScrollContainerContext must be called from within NestableScrollContainerContext Provider!"
);
}
return value;
}
Loading

0 comments on commit c8e47e9

Please sign in to comment.