feat(rewrite): convert to function components/hooks, separate concerns (#281)
* refactor to split up files, use hooks * whoops, use correct files * fix types * fix translation bugs * simplify procs * rename * fix issue with items getting stuck * add cell wrappers * re-organize state * nicer decorators * update types, decorators * capitalize * fix types * onRef * fix types * types * fix android * exoprt useOnCellActiveAnimation * memoize, fix some web-only bugs * fix web measure/layout issues * fix flicker on web * export types * more web fixes * make backwards compatible with reanimated v1 * fix v2 * update readme * update readme * adjust android autoscroll speed * establish min reanimated version * fix android jumpiness on autoscroll * update readme * defensive getNode Co-authored-by: computerjazz <hi@danielmerrill.com>
This commit is contained in:
parent
65a8e2eee8
commit
0017d7e7ce
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
||||
112
README.md
112
README.md
@ -32,7 +32,7 @@ yarn add react-native-draggable-flatlist
|
||||
All props are spread onto underlying [FlatList](https://facebook.github.io/react-native/docs/flatlist)
|
||||
|
||||
| Name | Type | Description |
|
||||
| :------------------------- | :---------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| :------------------------- | :---------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --- |
|
||||
| `data` | `T[]` | Items to be rendered. |
|
||||
| `horizontal` | `boolean` | Orientation of list. |
|
||||
| `renderItem` | `(params: { item: T, index: number, drag: () => void, isActive: boolean}) => JSX.Element` | Call `drag` when the row should become active (i.e. in an `onLongPress` or `onPressIn`). |
|
||||
@ -43,10 +43,9 @@ All props are spread onto underlying [FlatList](https://facebook.github.io/react
|
||||
| `onDragEnd` | `(params: { data: T[], from: number, to: number }) => void` | Called after animation has completed. Returns updated ordering of `data` |
|
||||
| `autoscrollThreshold` | `number` | Distance from edge of container where list begins to autoscroll when dragging. |
|
||||
| `autoscrollSpeed` | `number` | Determines how fast the list autoscrolls. |
|
||||
| `onRef` | `(ref: React.RefObject<DraggableFlatList<T>>) => void` | Returns underlying Animated FlatList ref. |
|
||||
| `onRef` | `(ref: DraggableFlatList<T>) => void` | Returns underlying Animated FlatList ref. |
|
||||
| `animationConfig` | `Partial<Animated.SpringConfig>` | Configure list animations. See [reanimated spring config](https://github.com/software-mansion/react-native-reanimated/blob/master/react-native-reanimated.d.ts#L112-L120) |
|
||||
| `activationDistance` | `number` | Distance a finger must travel before the gesture handler activates. Useful when using a draggable list within a TabNavigator so that the list does not capture navigator gestures. |
|
||||
| `layoutInvalidationKey` | `string` | Changing this value forces a remeasure of all item layouts. Useful if item size/ordering updates after initial mount. |
|
||||
| `activationDistance` | `number` | Distance a finger must travel before the gesture handler activates. Useful when using a draggable list within a TabNavigator so that the list does not capture navigator gestures. | |
|
||||
| `onScrollOffsetChange` | `(offset: number) => void` | Called with scroll offset. Stand-in for `onScroll`. |
|
||||
| `onPlaceholderIndexChange` | `(index: number) => void` | Called when the index of the placeholder changes |
|
||||
| `dragItemOverflow` | `boolean` | If true, dragged item follows finger beyond list boundary. |
|
||||
@ -55,82 +54,91 @@ All props are spread onto underlying [FlatList](https://facebook.github.io/react
|
||||
| `containerStyle` | `StyleProp<ViewStyle>` | Style of the main component. |
|
||||
| `simultaneousHandlers` | `React.Ref<any>` or `React.Ref<any>[]` | References to other gesture handlers, mainly useful when using this component within a `ScrollView`. See [Cross handler interactions](https://docs.swmansion.com/react-native-gesture-handler/docs/interactions/). |
|
||||
|
||||
## Cell Decorators
|
||||
|
||||
Cell Decorators are an easy way to add common hover animations. For example, wrapping `renderItem` in the `<ScaleDecorator>` component will automatically scale up the active item while hovering (see example below).
|
||||
|
||||
`ScaleDecorator`, `ShadowDecorator`, and `OpacityDecorator` are currently exported. Developers may create their own custom decorators using the animated values provided by the `useOnCellActiveAnimation` hook.
|
||||
|
||||
## Example
|
||||
|
||||
Example snack: https://snack.expo.io/@computerjazz/rndfl-example <br />
|
||||
Example snack with scale effect on hover: https://snack.expo.io/@computerjazz/rndfl-dragwithhovereffect
|
||||
Example snack: https://snack.expo.io/@computerjazz/rndfl3 <br />
|
||||
|
||||
```typescript
|
||||
import React, { useState, useCallback } from "react";
|
||||
import { View, TouchableOpacity, Text } from "react-native";
|
||||
import React, { useState } from "react";
|
||||
import { Text, View, StyleSheet, TouchableOpacity } from "react-native";
|
||||
import DraggableFlatList, {
|
||||
RenderItemParams,
|
||||
ScaleDecorator,
|
||||
} from "react-native-draggable-flatlist";
|
||||
|
||||
const NUM_ITEMS = 10;
|
||||
|
||||
function getColor(i: number) {
|
||||
const multiplier = 255 / (NUM_ITEMS - 1);
|
||||
const colorVal = i * multiplier;
|
||||
return `rgb(${colorVal}, ${Math.abs(128 - colorVal)}, ${255 - colorVal})`;
|
||||
}
|
||||
|
||||
const exampleData: Item[] = [...Array(20)].map((d, index) => {
|
||||
const backgroundColor = getColor(index);
|
||||
return {
|
||||
key: `item-${backgroundColor}`,
|
||||
label: String(index),
|
||||
backgroundColor
|
||||
};
|
||||
});
|
||||
|
||||
type Item = {
|
||||
key: string;
|
||||
label: string;
|
||||
height: number;
|
||||
width: number;
|
||||
backgroundColor: string;
|
||||
};
|
||||
|
||||
function Example() {
|
||||
const [data, setData] = useState(exampleData);
|
||||
const initialData: Item[] = [...Array(NUM_ITEMS)].map((d, index) => {
|
||||
const backgroundColor = getColor(index);
|
||||
return {
|
||||
key: `item-${index}`,
|
||||
label: String(index) + "",
|
||||
height: 100,
|
||||
width: 60 + Math.random() * 40,
|
||||
backgroundColor,
|
||||
};
|
||||
});
|
||||
|
||||
const renderItem = useCallback(
|
||||
({ item, index, drag, isActive }: RenderItemParams<Item>) => {
|
||||
return (
|
||||
export default function App() {
|
||||
const [data, setData] = useState(initialData);
|
||||
|
||||
const renderItem = ({ item, drag, isActive }: RenderItemParams<Item>) => {
|
||||
return (
|
||||
<ScaleDecorator>
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
height: 100,
|
||||
backgroundColor: isActive ? "red" : item.backgroundColor,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
onLongPress={drag}
|
||||
disabled={isActive}
|
||||
style={[
|
||||
styles.rowItem,
|
||||
{ backgroundColor: isActive ? "red" : item.backgroundColor },
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontWeight: "bold",
|
||||
color: "white",
|
||||
fontSize: 32,
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</Text>
|
||||
<Text style={styles.text}>{item.label}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
},
|
||||
[]
|
||||
);
|
||||
</ScaleDecorator>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<DraggableFlatList
|
||||
data={data}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={(item, index) => `draggable-item-${item.key}`}
|
||||
onDragEnd={({ data }) => setData(data)}
|
||||
/>
|
||||
</View>
|
||||
<DraggableFlatList
|
||||
data={data}
|
||||
onDragEnd={({ data }) => setData(data)}
|
||||
keyExtractor={(item) => item.key}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default Example;
|
||||
const styles = StyleSheet.create({
|
||||
rowItem: {
|
||||
height: 100,
|
||||
width: 100,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
text: {
|
||||
color: "white",
|
||||
fontSize: 24,
|
||||
fontWeight: "bold",
|
||||
textAlign: "center",
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@ -6,7 +6,8 @@
|
||||
"types": "lib/index.d.ts",
|
||||
"scripts": {
|
||||
"test": "jest",
|
||||
"build": "tsc"
|
||||
"build": "tsc",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
|
||||
93
src/components/CellDecorators.tsx
Normal file
93
src/components/CellDecorators.tsx
Normal file
@ -0,0 +1,93 @@
|
||||
import React from "react";
|
||||
import Animated, {
|
||||
interpolate,
|
||||
interpolateNode,
|
||||
multiply,
|
||||
} from "react-native-reanimated";
|
||||
import { useNode } from "../hooks/useNode";
|
||||
export { useOnCellActiveAnimation } from "../hooks/useOnCellActiveAnimation";
|
||||
import { useOnCellActiveAnimation } from "../hooks/useOnCellActiveAnimation";
|
||||
|
||||
type ScaleProps = {
|
||||
activeScale?: number;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
// support older versions of Reanimated v1 by using the old interpolate function
|
||||
// if interpolateNode not available.
|
||||
const interpolateFn = ((interpolateNode ||
|
||||
interpolate) as unknown) as typeof interpolateNode;
|
||||
|
||||
export const ScaleDecorator = ({ activeScale = 1.1, children }: ScaleProps) => {
|
||||
const { isActive, onActiveAnim } = useOnCellActiveAnimation({
|
||||
animationConfig: { mass: 0.1, restDisplacementThreshold: 0.0001 },
|
||||
});
|
||||
|
||||
const animScale = useNode(
|
||||
interpolateFn(onActiveAnim, {
|
||||
inputRange: [0, 1],
|
||||
outputRange: [1, activeScale],
|
||||
})
|
||||
);
|
||||
|
||||
const scale = isActive ? animScale : 1;
|
||||
return (
|
||||
<Animated.View
|
||||
style={{ transform: [{ scaleX: scale }, { scaleY: scale }] }}
|
||||
>
|
||||
{children}
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
type ShadowProps = {
|
||||
children: React.ReactNode;
|
||||
elevation?: number;
|
||||
radius?: number;
|
||||
color?: string;
|
||||
opacity?: number;
|
||||
};
|
||||
|
||||
export const ShadowDecorator = ({
|
||||
elevation = 10,
|
||||
color = "black",
|
||||
opacity = 0.25,
|
||||
radius = 5,
|
||||
children,
|
||||
}: ShadowProps) => {
|
||||
const { isActive, onActiveAnim } = useOnCellActiveAnimation();
|
||||
const shadowOpacity = useNode(multiply(onActiveAnim, opacity));
|
||||
|
||||
const style = {
|
||||
elevation: isActive ? elevation : 0,
|
||||
shadowRadius: isActive ? radius : 0,
|
||||
shadowColor: isActive ? color : "transparent",
|
||||
shadowOpacity: isActive ? shadowOpacity : 0,
|
||||
};
|
||||
|
||||
return <Animated.View style={style}>{children}</Animated.View>;
|
||||
};
|
||||
|
||||
type OpacityProps = {
|
||||
activeOpacity?: number;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const OpacityDecorator = ({
|
||||
activeOpacity = 0.25,
|
||||
children,
|
||||
}: OpacityProps) => {
|
||||
const { isActive, onActiveAnim } = useOnCellActiveAnimation();
|
||||
const opacity = useNode(
|
||||
interpolateFn(onActiveAnim, {
|
||||
inputRange: [0, 1],
|
||||
outputRange: [1, activeOpacity],
|
||||
})
|
||||
);
|
||||
|
||||
const style = {
|
||||
opacity: isActive ? opacity : 1,
|
||||
};
|
||||
|
||||
return <Animated.View style={style}>{children}</Animated.View>;
|
||||
};
|
||||
159
src/components/CellRendererComponent.tsx
Normal file
159
src/components/CellRendererComponent.tsx
Normal file
@ -0,0 +1,159 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from "react";
|
||||
import {
|
||||
findNodeHandle,
|
||||
LayoutChangeEvent,
|
||||
MeasureLayoutOnSuccessCallback,
|
||||
} from "react-native";
|
||||
import Animated, { cond, useValue } from "react-native-reanimated";
|
||||
import { useDraggableFlatListContext } from "../context/DraggableFlatListContext";
|
||||
import { isAndroid, isIOS, isReanimatedV2, isWeb } from "../constants";
|
||||
import { useCellTranslate } from "../hooks/useCellTranslate";
|
||||
import { typedMemo } from "../utils";
|
||||
import { useRefs } from "../context/RefContext";
|
||||
import { useAnimatedValues } from "../context/AnimatedValueContext";
|
||||
import CellProvider from "../context/CellContext";
|
||||
|
||||
type Props<T> = {
|
||||
item: T;
|
||||
index: number;
|
||||
children: React.ReactNode;
|
||||
onLayout: (e: LayoutChangeEvent) => void;
|
||||
};
|
||||
|
||||
function CellRendererComponent<T>(props: Props<T>) {
|
||||
const { item, index, onLayout, children } = props;
|
||||
|
||||
const currentIndexAnim = useValue(index);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
currentIndexAnim.setValue(index);
|
||||
}, [index, currentIndexAnim]);
|
||||
|
||||
const viewRef = useRef<Animated.View>(null);
|
||||
const { cellDataRef, propsRef, scrollOffsetRef, containerRef } = useRefs<T>();
|
||||
|
||||
const { horizontalAnim } = useAnimatedValues();
|
||||
const {
|
||||
activeKey,
|
||||
keyExtractor,
|
||||
horizontal,
|
||||
} = useDraggableFlatListContext<T>();
|
||||
|
||||
const key = keyExtractor(item, index);
|
||||
const offset = useValue<number>(-1);
|
||||
const size = useValue<number>(-1);
|
||||
const translate = useCellTranslate({
|
||||
cellOffset: offset,
|
||||
cellSize: size,
|
||||
cellIndex: currentIndexAnim,
|
||||
});
|
||||
|
||||
useMemo(() => {
|
||||
// prevent flicker on web
|
||||
if (isWeb) translate.setValue(0);
|
||||
}, [index]); //eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const isActive = activeKey === key;
|
||||
|
||||
const style = useMemo(
|
||||
() => ({
|
||||
transform: [
|
||||
{ translateX: cond(horizontalAnim, translate, 0) },
|
||||
{ translateY: cond(horizontalAnim, 0, translate) },
|
||||
],
|
||||
}),
|
||||
[horizontalAnim, translate]
|
||||
);
|
||||
|
||||
const updateCellMeasurements = useCallback(() => {
|
||||
const onSuccess: MeasureLayoutOnSuccessCallback = (x, y, w, h) => {
|
||||
if (isWeb && horizontal) x += scrollOffsetRef.current;
|
||||
const cellOffset = horizontal ? x : y;
|
||||
const cellSize = horizontal ? w : h;
|
||||
cellDataRef.current.set(key, {
|
||||
measurements: { size: cellSize, offset: cellOffset },
|
||||
});
|
||||
size.setValue(cellSize);
|
||||
offset.setValue(cellOffset);
|
||||
};
|
||||
|
||||
const onFail = () => {
|
||||
if (propsRef.current?.debug) {
|
||||
console.log(`## on measure fail, index: ${index}`);
|
||||
}
|
||||
};
|
||||
|
||||
// findNodeHandle is being deprecated. This is no longer necessary if using reanimated v2
|
||||
// remove once v1 is no longer supported
|
||||
const containerNode = containerRef.current;
|
||||
const viewNode = isReanimatedV2
|
||||
? viewRef.current
|
||||
: viewRef.current?.getNode();
|
||||
//@ts-ignore
|
||||
const nodeHandle = isReanimatedV2
|
||||
? containerNode
|
||||
: findNodeHandle(containerNode);
|
||||
|
||||
if (viewNode && nodeHandle) {
|
||||
//@ts-ignore
|
||||
viewNode.measureLayout(nodeHandle, onSuccess, onFail);
|
||||
}
|
||||
}, [
|
||||
cellDataRef,
|
||||
horizontal,
|
||||
index,
|
||||
key,
|
||||
offset,
|
||||
propsRef,
|
||||
size,
|
||||
scrollOffsetRef,
|
||||
containerRef,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isWeb) {
|
||||
// onLayout isn't called on web when the cell index changes, so we manually re-measure
|
||||
updateCellMeasurements();
|
||||
}
|
||||
}, [index, updateCellMeasurements]);
|
||||
|
||||
const onCellLayout = useCallback(
|
||||
(e: LayoutChangeEvent) => {
|
||||
updateCellMeasurements();
|
||||
onLayout(e);
|
||||
},
|
||||
[updateCellMeasurements, onLayout]
|
||||
);
|
||||
|
||||
// changing zIndex crashes android:
|
||||
// https://github.com/facebook/react-native/issues/28751
|
||||
return (
|
||||
<Animated.View
|
||||
{...props}
|
||||
ref={viewRef}
|
||||
onLayout={onCellLayout}
|
||||
style={[
|
||||
isAndroid && { elevation: isActive ? 1 : 0 },
|
||||
{ flexDirection: horizontal ? "row" : "column" },
|
||||
(isWeb || isIOS) && { zIndex: isActive ? 999 : 0 },
|
||||
]}
|
||||
pointerEvents={activeKey ? "none" : "auto"}
|
||||
>
|
||||
<Animated.View
|
||||
// Including both animated styles and non-animated styles causes react-native-web
|
||||
// to ignore updates in non-animated styles. Solution is to separate anima
|
||||
style={style}
|
||||
>
|
||||
<CellProvider isActive={isActive}>{children}</CellProvider>
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
export default typedMemo(CellRendererComponent);
|
||||
435
src/components/DraggableFlatList.tsx
Normal file
435
src/components/DraggableFlatList.tsx
Normal file
@ -0,0 +1,435 @@
|
||||
import React, { useCallback, useLayoutEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
ListRenderItem,
|
||||
FlatListProps,
|
||||
NativeScrollEvent,
|
||||
NativeSyntheticEvent,
|
||||
} from "react-native";
|
||||
import {
|
||||
PanGestureHandler,
|
||||
State as GestureState,
|
||||
FlatList,
|
||||
PanGestureHandlerGestureEvent,
|
||||
PanGestureHandlerStateChangeEvent,
|
||||
} from "react-native-gesture-handler";
|
||||
import Animated, {
|
||||
and,
|
||||
block,
|
||||
call,
|
||||
cond,
|
||||
eq,
|
||||
event,
|
||||
greaterThan,
|
||||
neq,
|
||||
not,
|
||||
onChange,
|
||||
or,
|
||||
set,
|
||||
sub,
|
||||
} from "react-native-reanimated";
|
||||
import CellRendererComponent from "./CellRendererComponent";
|
||||
import { DEFAULT_PROPS, isReanimatedV2, isWeb } from "../constants";
|
||||
import PlaceholderItem from "./PlaceholderItem";
|
||||
import RowItem from "./RowItem";
|
||||
import ScrollOffsetListener from "./ScrollOffsetListener";
|
||||
import { DraggableFlatListProps } from "../types";
|
||||
import { useAutoScroll } from "../hooks/useAutoScroll";
|
||||
import { useNode } from "../hooks/useNode";
|
||||
import PropsProvider from "../context/PropsContext";
|
||||
import AnimatedValueProvider, {
|
||||
useAnimatedValues,
|
||||
} from "../context/AnimatedValueContext";
|
||||
import RefProvider, { useRefs } from "../context/RefContext";
|
||||
import DraggableFlatListProvider from "../context/DraggableFlatListContext";
|
||||
|
||||
type RNGHFlatListProps<T> = Animated.AnimateProps<
|
||||
FlatListProps<T> & {
|
||||
ref: React.Ref<FlatList<T>>;
|
||||
simultaneousHandlers?: React.Ref<any> | React.Ref<any>[];
|
||||
}
|
||||
>;
|
||||
|
||||
const AnimatedFlatList = (Animated.createAnimatedComponent(
|
||||
FlatList
|
||||
) as unknown) as <T>(props: RNGHFlatListProps<T>) => React.ReactElement;
|
||||
|
||||
function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
|
||||
const {
|
||||
cellDataRef,
|
||||
containerRef,
|
||||
flatlistRef,
|
||||
isTouchActiveRef,
|
||||
keyToIndexRef,
|
||||
panGestureHandlerRef,
|
||||
propsRef,
|
||||
scrollOffsetRef,
|
||||
} = useRefs<T>();
|
||||
const {
|
||||
activationDistance,
|
||||
activeCellOffset,
|
||||
activeCellSize,
|
||||
activeIndexAnim,
|
||||
containerSize,
|
||||
disabled,
|
||||
hasMoved,
|
||||
panGestureState,
|
||||
resetTouchedCell,
|
||||
scrollOffset,
|
||||
scrollViewSize,
|
||||
spacerIndexAnim,
|
||||
touchAbsolute,
|
||||
touchInit,
|
||||
} = useAnimatedValues();
|
||||
|
||||
const {
|
||||
dragHitSlop = DEFAULT_PROPS.dragHitSlop,
|
||||
scrollEnabled = DEFAULT_PROPS.scrollEnabled,
|
||||
activationDistance: activationDistanceProp = DEFAULT_PROPS.activationDistance,
|
||||
} = props;
|
||||
|
||||
const [activeKey, setActiveKey] = useState<string | null>(null);
|
||||
|
||||
const keyExtractor = useCallback(
|
||||
(item: T, index: number) => {
|
||||
if (propsRef.current.keyExtractor)
|
||||
return propsRef.current.keyExtractor(item, index);
|
||||
else
|
||||
throw new Error("You must provide a keyExtractor to DraggableFlatList");
|
||||
},
|
||||
[propsRef]
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
props.data.forEach((d, i) => {
|
||||
const key = keyExtractor(d, i);
|
||||
keyToIndexRef.current.set(key, i);
|
||||
});
|
||||
}, [props.data, keyExtractor, keyToIndexRef]);
|
||||
|
||||
const drag = useCallback(
|
||||
(activeKey: string) => {
|
||||
if (!isTouchActiveRef.current.js) return;
|
||||
const index = keyToIndexRef.current.get(activeKey);
|
||||
const cellData = cellDataRef.current.get(activeKey);
|
||||
if (cellData) {
|
||||
activeCellOffset.setValue(
|
||||
cellData.measurements.offset - scrollOffsetRef.current
|
||||
);
|
||||
activeCellSize.setValue(cellData.measurements.size);
|
||||
}
|
||||
|
||||
const { onDragBegin } = propsRef.current;
|
||||
if (index !== undefined) {
|
||||
spacerIndexAnim.setValue(index);
|
||||
activeIndexAnim.setValue(index);
|
||||
setActiveKey(activeKey);
|
||||
onDragBegin?.(index);
|
||||
}
|
||||
},
|
||||
[
|
||||
isTouchActiveRef,
|
||||
keyToIndexRef,
|
||||
cellDataRef,
|
||||
propsRef,
|
||||
activeCellOffset,
|
||||
scrollOffsetRef,
|
||||
activeCellSize,
|
||||
spacerIndexAnim,
|
||||
activeIndexAnim,
|
||||
]
|
||||
);
|
||||
|
||||
const autoScrollNode = useAutoScroll();
|
||||
|
||||
const onContainerLayout = () => {
|
||||
const containerNode = isReanimatedV2
|
||||
? containerRef.current
|
||||
: containerRef.current?.getNode();
|
||||
|
||||
//@ts-ignore
|
||||
containerNode?.measure((_x, _y, w, h) => {
|
||||
containerSize.setValue(props.horizontal ? w : h);
|
||||
});
|
||||
};
|
||||
|
||||
const onListContentSizeChange = (w: number, h: number) => {
|
||||
scrollViewSize.setValue(props.horizontal ? w : h);
|
||||
props.onContentSizeChange?.(w, h);
|
||||
};
|
||||
|
||||
const onContainerTouchStart = () => {
|
||||
isTouchActiveRef.current.js = true;
|
||||
isTouchActiveRef.current.native.setValue(1);
|
||||
return false;
|
||||
};
|
||||
|
||||
const onContainerTouchEnd = () => {
|
||||
isTouchActiveRef.current.js = false;
|
||||
isTouchActiveRef.current.native.setValue(0);
|
||||
};
|
||||
|
||||
let dynamicProps = {};
|
||||
if (activationDistanceProp) {
|
||||
const activeOffset = [-activationDistanceProp, activationDistanceProp];
|
||||
dynamicProps = props.horizontal
|
||||
? { activeOffsetX: activeOffset }
|
||||
: { activeOffsetY: activeOffset };
|
||||
}
|
||||
|
||||
const extraData = useMemo(
|
||||
() => ({
|
||||
activeKey,
|
||||
extraData: props.extraData,
|
||||
}),
|
||||
[activeKey, props.extraData]
|
||||
);
|
||||
|
||||
const renderItem: ListRenderItem<T> = useCallback(
|
||||
({ item, index }) => {
|
||||
return (
|
||||
<RowItem
|
||||
item={item}
|
||||
itemKey={keyExtractor(item, index)}
|
||||
renderItem={props.renderItem}
|
||||
drag={drag}
|
||||
extraData={props.extraData}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[props.renderItem, props.extraData, drag, keyExtractor]
|
||||
);
|
||||
|
||||
const resetHoverState = useCallback(() => {
|
||||
activeIndexAnim.setValue(-1);
|
||||
spacerIndexAnim.setValue(-1);
|
||||
touchAbsolute.setValue(0);
|
||||
disabled.setValue(0);
|
||||
requestAnimationFrame(() => {
|
||||
setActiveKey(null);
|
||||
});
|
||||
}, [activeIndexAnim, spacerIndexAnim, touchAbsolute, disabled]);
|
||||
|
||||
const onRelease = ([index]: readonly number[]) => {
|
||||
// This shouldn't be necessary but seems to fix a bug where sometimes
|
||||
// native values wouldn't update
|
||||
isTouchActiveRef.current.native.setValue(0);
|
||||
props.onRelease?.(index);
|
||||
};
|
||||
|
||||
const onDragEnd = useCallback(
|
||||
([from, to]: readonly number[]) => {
|
||||
const { onDragEnd, data } = propsRef.current;
|
||||
if (onDragEnd) {
|
||||
const newData = [...data];
|
||||
if (from !== to) {
|
||||
newData.splice(from, 1);
|
||||
newData.splice(to, 0, data[from]);
|
||||
}
|
||||
onDragEnd({ from, to, data: newData });
|
||||
}
|
||||
resetHoverState();
|
||||
},
|
||||
[resetHoverState, propsRef]
|
||||
);
|
||||
|
||||
const onGestureRelease = useNode(
|
||||
cond(
|
||||
greaterThan(activeIndexAnim, -1),
|
||||
[
|
||||
set(disabled, 1),
|
||||
set(isTouchActiveRef.current.native, 0),
|
||||
call([activeIndexAnim], onRelease),
|
||||
cond(not(hasMoved), call([activeIndexAnim], resetHoverState)),
|
||||
],
|
||||
[call([activeIndexAnim], resetHoverState), resetTouchedCell]
|
||||
)
|
||||
);
|
||||
|
||||
const onPanStateChange = useMemo(
|
||||
() =>
|
||||
event([
|
||||
{
|
||||
nativeEvent: ({
|
||||
state,
|
||||
x,
|
||||
y,
|
||||
}: PanGestureHandlerStateChangeEvent["nativeEvent"]) =>
|
||||
block([
|
||||
cond(and(neq(state, panGestureState), not(disabled)), [
|
||||
cond(
|
||||
or(
|
||||
eq(state, GestureState.BEGAN), // Called on press in on Android, NOT on ios!
|
||||
// GestureState.BEGAN may be skipped on fast swipes
|
||||
and(
|
||||
eq(state, GestureState.ACTIVE),
|
||||
neq(panGestureState, GestureState.BEGAN)
|
||||
)
|
||||
),
|
||||
[
|
||||
set(touchAbsolute, props.horizontal ? x : y),
|
||||
set(touchInit, touchAbsolute),
|
||||
]
|
||||
),
|
||||
cond(eq(state, GestureState.ACTIVE), [
|
||||
set(
|
||||
activationDistance,
|
||||
sub(props.horizontal ? x : y, touchInit)
|
||||
),
|
||||
set(touchAbsolute, props.horizontal ? x : y),
|
||||
]),
|
||||
]),
|
||||
cond(neq(panGestureState, state), [
|
||||
set(panGestureState, state),
|
||||
cond(
|
||||
or(
|
||||
eq(state, GestureState.END),
|
||||
eq(state, GestureState.CANCELLED),
|
||||
eq(state, GestureState.FAILED)
|
||||
),
|
||||
onGestureRelease
|
||||
),
|
||||
]),
|
||||
]),
|
||||
},
|
||||
]),
|
||||
[
|
||||
activationDistance,
|
||||
props.horizontal,
|
||||
panGestureState,
|
||||
disabled,
|
||||
onGestureRelease,
|
||||
touchAbsolute,
|
||||
touchInit,
|
||||
]
|
||||
);
|
||||
|
||||
const onPanGestureEvent = useMemo(
|
||||
() =>
|
||||
event([
|
||||
{
|
||||
nativeEvent: ({
|
||||
x,
|
||||
y,
|
||||
}: PanGestureHandlerGestureEvent["nativeEvent"]) =>
|
||||
cond(
|
||||
and(
|
||||
greaterThan(activeIndexAnim, -1),
|
||||
eq(panGestureState, GestureState.ACTIVE),
|
||||
not(disabled)
|
||||
),
|
||||
[
|
||||
cond(not(hasMoved), set(hasMoved, 1)),
|
||||
set(touchAbsolute, props.horizontal ? x : y),
|
||||
]
|
||||
),
|
||||
},
|
||||
]),
|
||||
[
|
||||
activeIndexAnim,
|
||||
disabled,
|
||||
hasMoved,
|
||||
panGestureState,
|
||||
props.horizontal,
|
||||
touchAbsolute,
|
||||
]
|
||||
);
|
||||
|
||||
const scrollHandler = useMemo(() => {
|
||||
// Web doesn't seem to like animated events
|
||||
const webOnScroll = ({
|
||||
nativeEvent: {
|
||||
contentOffset: { x, y },
|
||||
},
|
||||
}: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
scrollOffset.setValue(props.horizontal ? x : y);
|
||||
};
|
||||
|
||||
const mobileOnScroll = event([
|
||||
{
|
||||
nativeEvent: ({ contentOffset }: NativeScrollEvent) =>
|
||||
block([
|
||||
set(
|
||||
scrollOffset,
|
||||
props.horizontal ? contentOffset.x : contentOffset.y
|
||||
),
|
||||
autoScrollNode,
|
||||
]),
|
||||
},
|
||||
]);
|
||||
|
||||
return isWeb ? webOnScroll : mobileOnScroll;
|
||||
}, [autoScrollNode, props.horizontal, scrollOffset]);
|
||||
|
||||
return (
|
||||
<DraggableFlatListProvider
|
||||
activeKey={activeKey}
|
||||
onDragEnd={onDragEnd}
|
||||
keyExtractor={keyExtractor}
|
||||
horizontal={!!props.horizontal}
|
||||
>
|
||||
<PanGestureHandler
|
||||
ref={panGestureHandlerRef}
|
||||
hitSlop={dragHitSlop}
|
||||
onHandlerStateChange={onPanStateChange}
|
||||
onGestureEvent={onPanGestureEvent}
|
||||
simultaneousHandlers={props.simultaneousHandlers}
|
||||
{...dynamicProps}
|
||||
>
|
||||
<Animated.View
|
||||
style={props.containerStyle}
|
||||
ref={containerRef}
|
||||
onLayout={onContainerLayout}
|
||||
onTouchEnd={onContainerTouchEnd}
|
||||
onStartShouldSetResponderCapture={onContainerTouchStart}
|
||||
//@ts-ignore
|
||||
onClick={onContainerTouchEnd}
|
||||
>
|
||||
<ScrollOffsetListener
|
||||
scrollOffset={scrollOffset}
|
||||
onScrollOffsetChange={([offset]) => {
|
||||
scrollOffsetRef.current = offset;
|
||||
props.onScrollOffsetChange?.(offset);
|
||||
}}
|
||||
/>
|
||||
<PlaceholderItem renderPlaceholder={props.renderPlaceholder} />
|
||||
<AnimatedFlatList
|
||||
{...props}
|
||||
CellRendererComponent={CellRendererComponent}
|
||||
ref={flatlistRef}
|
||||
onContentSizeChange={onListContentSizeChange}
|
||||
scrollEnabled={!activeKey && scrollEnabled}
|
||||
renderItem={renderItem}
|
||||
extraData={extraData}
|
||||
keyExtractor={keyExtractor}
|
||||
onScroll={scrollHandler}
|
||||
scrollEventThrottle={1}
|
||||
simultaneousHandlers={props.simultaneousHandlers}
|
||||
removeClippedSubviews={false}
|
||||
/>
|
||||
<Animated.Code dependencies={[]}>
|
||||
{() =>
|
||||
block([
|
||||
onChange(
|
||||
isTouchActiveRef.current.native,
|
||||
cond(not(isTouchActiveRef.current.native), onGestureRelease)
|
||||
),
|
||||
])
|
||||
}
|
||||
</Animated.Code>
|
||||
</Animated.View>
|
||||
</PanGestureHandler>
|
||||
</DraggableFlatListProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DraggableFlatList<T>(props: DraggableFlatListProps<T>) {
|
||||
return (
|
||||
<PropsProvider {...props}>
|
||||
<AnimatedValueProvider>
|
||||
<RefProvider>
|
||||
<DraggableFlatListInner {...props} />
|
||||
</RefProvider>
|
||||
</AnimatedValueProvider>
|
||||
</PropsProvider>
|
||||
);
|
||||
}
|
||||
89
src/components/PlaceholderItem.tsx
Normal file
89
src/components/PlaceholderItem.tsx
Normal file
@ -0,0 +1,89 @@
|
||||
import React, { useCallback } from "react";
|
||||
import { StyleSheet } from "react-native";
|
||||
import Animated, {
|
||||
call,
|
||||
set,
|
||||
useCode,
|
||||
useValue,
|
||||
onChange,
|
||||
greaterThan,
|
||||
cond,
|
||||
} from "react-native-reanimated";
|
||||
import { useAnimatedValues } from "../context/AnimatedValueContext";
|
||||
import { useDraggableFlatListContext } from "../context/DraggableFlatListContext";
|
||||
import { useProps } from "../context/PropsContext";
|
||||
import { useRefs } from "../context/RefContext";
|
||||
import { useNode } from "../hooks/useNode";
|
||||
import { RenderPlaceholder } from "../types";
|
||||
import { typedMemo } from "../utils";
|
||||
|
||||
type Props<T> = {
|
||||
renderPlaceholder?: RenderPlaceholder<T>;
|
||||
};
|
||||
|
||||
function PlaceholderItem<T>({ renderPlaceholder }: Props<T>) {
|
||||
const {
|
||||
activeCellSize,
|
||||
placeholderScreenOffset,
|
||||
spacerIndexAnim,
|
||||
} = useAnimatedValues();
|
||||
const { keyToIndexRef, propsRef } = useRefs<T>();
|
||||
|
||||
const { activeKey } = useDraggableFlatListContext();
|
||||
const { horizontal } = useProps();
|
||||
|
||||
// for some reason using placeholderScreenOffset directly is buggy
|
||||
const translate = useValue(0);
|
||||
|
||||
const onPlaceholderIndexChange = useCallback(
|
||||
(index: number) => {
|
||||
propsRef.current.onPlaceholderIndexChange?.(index);
|
||||
},
|
||||
[propsRef]
|
||||
);
|
||||
|
||||
useCode(
|
||||
() =>
|
||||
onChange(
|
||||
spacerIndexAnim,
|
||||
call([spacerIndexAnim], ([i]) => {
|
||||
onPlaceholderIndexChange(i);
|
||||
})
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
const translateKey = horizontal ? "translateX" : "translateY";
|
||||
const sizeKey = horizontal ? "width" : "height";
|
||||
const opacity = useNode(cond(greaterThan(spacerIndexAnim, -1), 1, 0));
|
||||
|
||||
const activeIndex = activeKey
|
||||
? keyToIndexRef.current.get(activeKey)
|
||||
: undefined;
|
||||
const activeItem =
|
||||
activeIndex === undefined ? null : propsRef.current?.data[activeIndex];
|
||||
|
||||
const animStyle = {
|
||||
opacity,
|
||||
[sizeKey]: activeCellSize,
|
||||
transform: ([
|
||||
{ [translateKey]: translate },
|
||||
] as unknown) as Animated.AnimatedTransform,
|
||||
};
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
pointerEvents={activeKey ? "auto" : "none"}
|
||||
style={[StyleSheet.absoluteFill, animStyle]}
|
||||
>
|
||||
{!activeItem || activeIndex === undefined
|
||||
? null
|
||||
: renderPlaceholder?.({ item: activeItem, index: activeIndex })}
|
||||
<Animated.Code>
|
||||
{() => set(translate, placeholderScreenOffset)}
|
||||
</Animated.Code>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
export default typedMemo(PlaceholderItem);
|
||||
62
src/components/RowItem.tsx
Normal file
62
src/components/RowItem.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import React, { useCallback, useRef } from "react";
|
||||
import { useDraggableFlatListContext } from "../context/DraggableFlatListContext";
|
||||
import { useRefs } from "../context/RefContext";
|
||||
import { RenderItem } from "../types";
|
||||
import { typedMemo } from "../utils";
|
||||
|
||||
type Props<T> = {
|
||||
extraData?: any;
|
||||
drag: (itemKey: string) => void;
|
||||
item: T;
|
||||
renderItem: RenderItem<T>;
|
||||
itemKey: string;
|
||||
debug?: boolean;
|
||||
};
|
||||
|
||||
function RowItem<T>(props: Props<T>) {
|
||||
const propsRef = useRef(props);
|
||||
propsRef.current = props;
|
||||
|
||||
const { activeKey } = useDraggableFlatListContext();
|
||||
const activeKeyRef = useRef(activeKey);
|
||||
activeKeyRef.current = activeKey;
|
||||
const { keyToIndexRef } = useRefs();
|
||||
|
||||
const drag = useCallback(() => {
|
||||
const { drag, itemKey, debug } = propsRef.current;
|
||||
if (activeKeyRef.current) {
|
||||
// already dragging an item, noop
|
||||
if (debug)
|
||||
console.log(
|
||||
"## attempt to drag item while another item is already active, noop"
|
||||
);
|
||||
}
|
||||
drag(itemKey);
|
||||
}, []);
|
||||
|
||||
const { renderItem, item, itemKey } = props;
|
||||
return (
|
||||
<MemoizedInner
|
||||
isActive={activeKey === itemKey}
|
||||
drag={drag}
|
||||
renderItem={renderItem}
|
||||
item={item}
|
||||
index={keyToIndexRef.current.get(itemKey)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default typedMemo(RowItem);
|
||||
|
||||
type InnerProps<T> = {
|
||||
isActive: boolean;
|
||||
item: T;
|
||||
index?: number;
|
||||
drag: () => void;
|
||||
renderItem: RenderItem<T>;
|
||||
};
|
||||
|
||||
function Inner<T>({ isActive, item, drag, index, renderItem }: InnerProps<T>) {
|
||||
return renderItem({ isActive, item, drag, index }) as JSX.Element;
|
||||
}
|
||||
const MemoizedInner = typedMemo(Inner);
|
||||
20
src/components/ScrollOffsetListener.tsx
Normal file
20
src/components/ScrollOffsetListener.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import Animated, { call, onChange, useCode } from "react-native-reanimated";
|
||||
import { typedMemo } from "../utils";
|
||||
|
||||
type Props = {
|
||||
scrollOffset: Animated.Value<number>;
|
||||
onScrollOffsetChange: (offset: readonly number[]) => void;
|
||||
};
|
||||
|
||||
const ScrollOffsetListener = ({
|
||||
scrollOffset,
|
||||
onScrollOffsetChange,
|
||||
}: Props) => {
|
||||
useCode(
|
||||
() => onChange(scrollOffset, call([scrollOffset], onScrollOffsetChange)),
|
||||
[]
|
||||
);
|
||||
return null;
|
||||
};
|
||||
|
||||
export default typedMemo(ScrollOffsetListener);
|
||||
31
src/constants.ts
Normal file
31
src/constants.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { Platform } from "react-native";
|
||||
import { PanGestureHandlerProperties } from "react-native-gesture-handler";
|
||||
import Animated, { useSharedValue } from "react-native-reanimated";
|
||||
|
||||
// Fire onScrollComplete when within this many px of target offset
|
||||
export const SCROLL_POSITION_TOLERANCE = 2;
|
||||
export const DEFAULT_ANIMATION_CONFIG: Animated.WithSpringConfig = {
|
||||
damping: 20,
|
||||
mass: 0.2,
|
||||
stiffness: 100,
|
||||
overshootClamping: false,
|
||||
restSpeedThreshold: 0.2,
|
||||
restDisplacementThreshold: 0.2,
|
||||
};
|
||||
|
||||
export const DEFAULT_PROPS = {
|
||||
autoscrollThreshold: 30,
|
||||
autoscrollSpeed: 100,
|
||||
animationConfig: DEFAULT_ANIMATION_CONFIG,
|
||||
scrollEnabled: true,
|
||||
dragHitSlop: 0 as PanGestureHandlerProperties["hitSlop"],
|
||||
activationDistance: 0,
|
||||
dragItemOverflow: false,
|
||||
};
|
||||
|
||||
export const isIOS = Platform.OS === "ios";
|
||||
export const isAndroid = Platform.OS === "android";
|
||||
export const isWeb = Platform.OS === "web";
|
||||
|
||||
// Is there a better way to check for v2?
|
||||
export const isReanimatedV2 = !!useSharedValue;
|
||||
165
src/context/animatedValueContext.tsx
Normal file
165
src/context/animatedValueContext.tsx
Normal file
@ -0,0 +1,165 @@
|
||||
import React, { useContext } from "react";
|
||||
import {
|
||||
add,
|
||||
and,
|
||||
block,
|
||||
greaterThan,
|
||||
max,
|
||||
min,
|
||||
set,
|
||||
sub,
|
||||
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";
|
||||
|
||||
if (!useValue) {
|
||||
throw new Error("Incompatible Reanimated version (useValue not found)");
|
||||
}
|
||||
|
||||
const AnimatedValueContext = React.createContext<
|
||||
ReturnType<typeof useSetupAnimatedValues> | undefined
|
||||
>(undefined);
|
||||
|
||||
export default function AnimatedValueProvider({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const value = useSetupAnimatedValues();
|
||||
return (
|
||||
<AnimatedValueContext.Provider value={value}>
|
||||
{children}
|
||||
</AnimatedValueContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useAnimatedValues() {
|
||||
const value = useContext(AnimatedValueContext);
|
||||
if (!value) {
|
||||
throw new Error(
|
||||
"useAnimatedValues must be called from within AnimatedValueProvider!"
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function useSetupAnimatedValues<T>() {
|
||||
const props = useProps<T>();
|
||||
const containerSize = useValue<number>(0);
|
||||
|
||||
const touchInit = useValue<number>(0); // Position of initial touch
|
||||
const activationDistance = useValue<number>(0); // Distance finger travels from initial touch to when dragging begins
|
||||
const touchAbsolute = useValue<number>(0); // Finger position on screen, relative to container
|
||||
const panGestureState = useValue<GestureState>(GestureState.UNDETERMINED);
|
||||
|
||||
const isTouchActiveNative = useValue<number>(0);
|
||||
|
||||
const hasMoved = useValue<number>(0);
|
||||
const disabled = useValue<number>(0);
|
||||
|
||||
const horizontalAnim = useValue(props.horizontal ? 1 : 0);
|
||||
|
||||
const activeIndexAnim = useValue<number>(-1); // Index of hovering cell
|
||||
const spacerIndexAnim = useValue<number>(-1); // Index of hovered-over cell
|
||||
|
||||
const activeCellSize = useValue<number>(0); // Height or width of acctive cell
|
||||
const activeCellOffset = useValue<number>(0); // Distance between active cell and edge of container
|
||||
|
||||
const isDraggingCell = useNode(
|
||||
and(isTouchActiveNative, greaterThan(activeIndexAnim, -1))
|
||||
);
|
||||
|
||||
const scrollOffset = useValue<number>(0);
|
||||
|
||||
const scrollViewSize = useValue<number>(0);
|
||||
|
||||
const touchCellOffset = useNode(sub(touchInit, activeCellOffset));
|
||||
|
||||
const hoverAnimUnconstrained = useNode(
|
||||
sub(sub(touchAbsolute, activationDistance), touchCellOffset)
|
||||
);
|
||||
|
||||
const hoverAnimConstrained = useNode(
|
||||
min(sub(containerSize, activeCellSize), max(0, hoverAnimUnconstrained))
|
||||
);
|
||||
|
||||
const hoverAnim = props.dragItemOverflow
|
||||
? hoverAnimUnconstrained
|
||||
: hoverAnimConstrained;
|
||||
|
||||
const hoverOffset = useNode(add(hoverAnim, scrollOffset));
|
||||
|
||||
const placeholderOffset = useValue<number>(0);
|
||||
const placeholderScreenOffset = useNode(sub(placeholderOffset, scrollOffset));
|
||||
|
||||
// Note: this could use a refactor as it combines touch state + cell animation
|
||||
const resetTouchedCell = useNode(
|
||||
block([
|
||||
set(touchAbsolute, 0),
|
||||
set(touchInit, 0),
|
||||
set(activeCellOffset, 0),
|
||||
set(activationDistance, 0),
|
||||
set(hasMoved, 0),
|
||||
])
|
||||
);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
activationDistance,
|
||||
activeCellOffset,
|
||||
activeCellSize,
|
||||
activeIndexAnim,
|
||||
containerSize,
|
||||
disabled,
|
||||
hasMoved,
|
||||
horizontalAnim,
|
||||
hoverAnim,
|
||||
hoverAnimConstrained,
|
||||
hoverAnimUnconstrained,
|
||||
hoverOffset,
|
||||
isDraggingCell,
|
||||
isTouchActiveNative,
|
||||
panGestureState,
|
||||
placeholderOffset,
|
||||
placeholderScreenOffset,
|
||||
resetTouchedCell,
|
||||
scrollOffset,
|
||||
scrollViewSize,
|
||||
spacerIndexAnim,
|
||||
touchAbsolute,
|
||||
touchCellOffset,
|
||||
touchInit,
|
||||
}),
|
||||
[
|
||||
activationDistance,
|
||||
activeCellOffset,
|
||||
activeCellSize,
|
||||
activeIndexAnim,
|
||||
containerSize,
|
||||
disabled,
|
||||
hasMoved,
|
||||
horizontalAnim,
|
||||
hoverAnim,
|
||||
hoverAnimConstrained,
|
||||
hoverAnimUnconstrained,
|
||||
hoverOffset,
|
||||
isDraggingCell,
|
||||
isTouchActiveNative,
|
||||
panGestureState,
|
||||
placeholderOffset,
|
||||
placeholderScreenOffset,
|
||||
resetTouchedCell,
|
||||
scrollOffset,
|
||||
scrollViewSize,
|
||||
spacerIndexAnim,
|
||||
touchAbsolute,
|
||||
touchCellOffset,
|
||||
touchInit,
|
||||
]
|
||||
);
|
||||
|
||||
return value;
|
||||
}
|
||||
32
src/context/cellContext.tsx
Normal file
32
src/context/cellContext.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import React, { useContext, useMemo } from "react";
|
||||
|
||||
type CellContextValue = {
|
||||
isActive: boolean;
|
||||
};
|
||||
|
||||
const CellContext = React.createContext<CellContextValue | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
type Props = {
|
||||
isActive: boolean;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function CellProvider({ isActive, children }: Props) {
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
isActive,
|
||||
}),
|
||||
[isActive]
|
||||
);
|
||||
return <CellContext.Provider value={value}>{children}</CellContext.Provider>;
|
||||
}
|
||||
|
||||
export function useIsActive() {
|
||||
const value = useContext(CellContext);
|
||||
if (!value) {
|
||||
throw new Error("useIsActive must be called from within CellProvider!");
|
||||
}
|
||||
return value.isActive;
|
||||
}
|
||||
49
src/context/draggableFlatListContext.tsx
Normal file
49
src/context/draggableFlatListContext.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import React, { useContext, useMemo } from "react";
|
||||
|
||||
type Props<T> = {
|
||||
activeKey: string | null;
|
||||
onDragEnd: ([from, to]: readonly number[]) => void;
|
||||
keyExtractor: (item: T, index: number) => string;
|
||||
horizontal: boolean;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
type DraggableFlatListContextValue<T> = Omit<Props<T>, "children">;
|
||||
|
||||
const DraggableFlatListContext = React.createContext<
|
||||
DraggableFlatListContextValue<any> | undefined
|
||||
>(undefined);
|
||||
|
||||
export default function DraggableFlatListProvider<T>({
|
||||
activeKey,
|
||||
onDragEnd,
|
||||
keyExtractor,
|
||||
horizontal,
|
||||
children,
|
||||
}: Props<T>) {
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
activeKey,
|
||||
keyExtractor,
|
||||
onDragEnd,
|
||||
horizontal,
|
||||
}),
|
||||
[activeKey, onDragEnd, keyExtractor, horizontal]
|
||||
);
|
||||
|
||||
return (
|
||||
<DraggableFlatListContext.Provider value={value}>
|
||||
{children}
|
||||
</DraggableFlatListContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useDraggableFlatListContext<T>() {
|
||||
const value = useContext(DraggableFlatListContext);
|
||||
if (!value) {
|
||||
throw new Error(
|
||||
"useDraggableFlatListContext must be called within DraggableFlatListProvider"
|
||||
);
|
||||
}
|
||||
return value as DraggableFlatListContextValue<T>;
|
||||
}
|
||||
24
src/context/propsContext.tsx
Normal file
24
src/context/propsContext.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import React, { useContext } from "react";
|
||||
import { DraggableFlatListProps } from "../types";
|
||||
|
||||
const PropsContext = React.createContext<
|
||||
DraggableFlatListProps<any> | undefined
|
||||
>(undefined);
|
||||
|
||||
type Props<T> = DraggableFlatListProps<T> & { children: React.ReactNode };
|
||||
|
||||
export default function PropsProvider<T>({ children, ...props }: Props<T>) {
|
||||
return (
|
||||
<PropsContext.Provider value={props}>{children}</PropsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useProps<T>() {
|
||||
const value = useContext(PropsContext) as
|
||||
| DraggableFlatListProps<T>
|
||||
| undefined;
|
||||
if (!value) {
|
||||
throw new Error("useProps must be called from within PropsProvider!");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
93
src/context/refContext.tsx
Normal file
93
src/context/refContext.tsx
Normal file
@ -0,0 +1,93 @@
|
||||
import React, { useContext, useEffect } from "react";
|
||||
import { useMemo, useRef } from "react";
|
||||
import { FlatList, PanGestureHandler } from "react-native-gesture-handler";
|
||||
import Animated from "react-native-reanimated";
|
||||
import { DEFAULT_PROPS } from "../constants";
|
||||
import { useProps } from "./PropsContext";
|
||||
import { useAnimatedValues } from "./AnimatedValueContext";
|
||||
import { CellData, DraggableFlatListProps } from "../types";
|
||||
|
||||
type RefContextValue<T> = {
|
||||
propsRef: React.MutableRefObject<DraggableFlatListProps<T>>;
|
||||
animationConfigRef: React.MutableRefObject<Animated.SpringConfig>;
|
||||
cellDataRef: React.MutableRefObject<Map<string, CellData>>;
|
||||
keyToIndexRef: React.MutableRefObject<Map<string, number>>;
|
||||
containerRef: React.RefObject<Animated.View>;
|
||||
flatlistRef: React.RefObject<FlatList<T>>;
|
||||
panGestureHandlerRef: React.RefObject<PanGestureHandler>;
|
||||
scrollOffsetRef: React.MutableRefObject<number>;
|
||||
isTouchActiveRef: React.MutableRefObject<{
|
||||
native: Animated.Value<number>;
|
||||
js: boolean;
|
||||
}>;
|
||||
};
|
||||
const RefContext = React.createContext<RefContextValue<any> | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
export default function RefProvider<T>({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const value = useSetupRefs<T>();
|
||||
return <RefContext.Provider value={value}>{children}</RefContext.Provider>;
|
||||
}
|
||||
|
||||
export function useRefs<T>() {
|
||||
const value = useContext(RefContext);
|
||||
if (!value) {
|
||||
throw new Error(
|
||||
"useRefs must be called from within a RefContext.Provider!"
|
||||
);
|
||||
}
|
||||
return value as RefContextValue<T>;
|
||||
}
|
||||
|
||||
function useSetupRefs<T>() {
|
||||
const props = useProps<T>();
|
||||
const { onRef, animationConfig = DEFAULT_PROPS.animationConfig } = props;
|
||||
|
||||
const { isTouchActiveNative } = useAnimatedValues();
|
||||
|
||||
const propsRef = useRef(props);
|
||||
propsRef.current = props;
|
||||
const animConfig = {
|
||||
...DEFAULT_PROPS.animationConfig,
|
||||
...animationConfig,
|
||||
} as Animated.SpringConfig;
|
||||
const animationConfigRef = useRef(animConfig);
|
||||
animationConfigRef.current = animConfig;
|
||||
|
||||
const cellDataRef = useRef(new Map<string, CellData>());
|
||||
const keyToIndexRef = useRef(new Map<string, number>());
|
||||
const containerRef = useRef<Animated.View>(null);
|
||||
const flatlistRef = useRef<FlatList<T>>(null);
|
||||
const panGestureHandlerRef = useRef<PanGestureHandler>(null);
|
||||
const scrollOffsetRef = useRef(0);
|
||||
const isTouchActiveRef = useRef({
|
||||
native: isTouchActiveNative,
|
||||
js: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (flatlistRef.current) onRef?.(flatlistRef.current);
|
||||
}, [onRef]);
|
||||
|
||||
const refs = useMemo(
|
||||
() => ({
|
||||
animationConfigRef,
|
||||
cellDataRef,
|
||||
containerRef,
|
||||
flatlistRef,
|
||||
isTouchActiveRef,
|
||||
keyToIndexRef,
|
||||
panGestureHandlerRef,
|
||||
propsRef,
|
||||
scrollOffsetRef,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
return refs;
|
||||
}
|
||||
241
src/hooks/useAutoScroll.tsx
Normal file
241
src/hooks/useAutoScroll.tsx
Normal file
@ -0,0 +1,241 @@
|
||||
import { useRef } from "react";
|
||||
import {
|
||||
abs,
|
||||
add,
|
||||
and,
|
||||
block,
|
||||
call,
|
||||
cond,
|
||||
eq,
|
||||
greaterOrEq,
|
||||
lessOrEq,
|
||||
max,
|
||||
not,
|
||||
onChange,
|
||||
or,
|
||||
set,
|
||||
sub,
|
||||
useCode,
|
||||
useValue,
|
||||
} from "react-native-reanimated";
|
||||
import { State as GestureState } from "react-native-gesture-handler";
|
||||
import {
|
||||
DEFAULT_PROPS,
|
||||
SCROLL_POSITION_TOLERANCE,
|
||||
isReanimatedV2,
|
||||
isAndroid,
|
||||
} from "../constants";
|
||||
import { useNode } from "../hooks/useNode";
|
||||
import { useProps } from "../context/PropsContext";
|
||||
import { useAnimatedValues } from "../context/AnimatedValueContext";
|
||||
import { useRefs } from "../context/RefContext";
|
||||
|
||||
export function useAutoScroll() {
|
||||
const { flatlistRef } = useRefs();
|
||||
const {
|
||||
autoscrollThreshold = DEFAULT_PROPS.autoscrollThreshold,
|
||||
autoscrollSpeed = DEFAULT_PROPS.autoscrollSpeed,
|
||||
} = useProps();
|
||||
|
||||
const {
|
||||
scrollOffset,
|
||||
scrollViewSize,
|
||||
containerSize,
|
||||
hoverAnim,
|
||||
isDraggingCell,
|
||||
activeCellSize,
|
||||
panGestureState,
|
||||
} = useAnimatedValues();
|
||||
|
||||
const isScrolledUp = useNode(
|
||||
lessOrEq(sub(scrollOffset, SCROLL_POSITION_TOLERANCE), 0)
|
||||
);
|
||||
const isScrolledDown = useNode(
|
||||
greaterOrEq(
|
||||
add(scrollOffset, containerSize, SCROLL_POSITION_TOLERANCE),
|
||||
scrollViewSize
|
||||
)
|
||||
);
|
||||
|
||||
const distToTopEdge = useNode(max(0, hoverAnim));
|
||||
const distToBottomEdge = useNode(
|
||||
max(0, sub(containerSize, add(hoverAnim, activeCellSize)))
|
||||
);
|
||||
|
||||
const isAtTopEdge = useNode(lessOrEq(distToTopEdge, autoscrollThreshold));
|
||||
const isAtBottomEdge = useNode(
|
||||
lessOrEq(distToBottomEdge, autoscrollThreshold!)
|
||||
);
|
||||
|
||||
const isAtEdge = useNode(or(isAtBottomEdge, isAtTopEdge));
|
||||
const autoscrollParams = [
|
||||
distToTopEdge,
|
||||
distToBottomEdge,
|
||||
scrollOffset,
|
||||
isScrolledUp,
|
||||
isScrolledDown,
|
||||
];
|
||||
|
||||
const targetScrollOffset = useValue<number>(0);
|
||||
const resolveAutoscroll = useRef<(params: readonly number[]) => void>();
|
||||
|
||||
const isAutoScrollInProgressNative = useValue<number>(0);
|
||||
|
||||
const isAutoScrollInProgress = useRef({
|
||||
js: false,
|
||||
native: isAutoScrollInProgressNative,
|
||||
});
|
||||
|
||||
const isDraggingCellJS = useRef(false);
|
||||
useCode(
|
||||
() =>
|
||||
block([
|
||||
onChange(
|
||||
isDraggingCell,
|
||||
call([isDraggingCell], ([v]) => {
|
||||
isDraggingCellJS.current = !!v;
|
||||
})
|
||||
),
|
||||
]),
|
||||
[]
|
||||
);
|
||||
|
||||
// Ensure that only 1 call to autoscroll is active at a time
|
||||
const autoscrollLooping = useRef(false);
|
||||
|
||||
const onAutoscrollComplete = (params: readonly number[]) => {
|
||||
isAutoScrollInProgress.current.js = false;
|
||||
resolveAutoscroll.current?.(params);
|
||||
};
|
||||
|
||||
const scrollToAsync = (offset: number): Promise<readonly number[]> =>
|
||||
new Promise((resolve) => {
|
||||
resolveAutoscroll.current = resolve;
|
||||
targetScrollOffset.setValue(offset);
|
||||
isAutoScrollInProgress.current.native.setValue(1);
|
||||
isAutoScrollInProgress.current.js = true;
|
||||
|
||||
const flatlistNode: FlatList<T> | null =
|
||||
"scrollToOffset" in flatlistRef
|
||||
? flatlistRef
|
||||
: "getNode" in flatlistRef
|
||||
? //@ts-ignore backwards compat
|
||||
flatlistRef.getNode()
|
||||
: null;
|
||||
flatlistNode?.scrollToOffset?.({ offset });
|
||||
});
|
||||
|
||||
const getScrollTargetOffset = (
|
||||
distFromTop: number,
|
||||
distFromBottom: number,
|
||||
scrollOffset: number,
|
||||
isScrolledUp: boolean,
|
||||
isScrolledDown: boolean
|
||||
) => {
|
||||
if (isAutoScrollInProgress.current.js) return -1;
|
||||
const scrollUp = distFromTop < autoscrollThreshold!;
|
||||
const scrollDown = distFromBottom < autoscrollThreshold!;
|
||||
if (
|
||||
!(scrollUp || scrollDown) ||
|
||||
(scrollUp && isScrolledUp) ||
|
||||
(scrollDown && isScrolledDown)
|
||||
)
|
||||
return -1;
|
||||
const distFromEdge = scrollUp ? distFromTop : distFromBottom;
|
||||
const speedPct = 1 - distFromEdge / autoscrollThreshold!;
|
||||
// Android scroll speed seems much faster than ios
|
||||
const speed = isAndroid ? autoscrollSpeed / 10 : autoscrollSpeed;
|
||||
const offset = speedPct * speed;
|
||||
const targetOffset = scrollUp
|
||||
? Math.max(0, scrollOffset - offset)
|
||||
: scrollOffset + offset;
|
||||
return targetOffset;
|
||||
};
|
||||
|
||||
const autoscroll = async (params: readonly number[]) => {
|
||||
if (autoscrollLooping.current) {
|
||||
return;
|
||||
}
|
||||
autoscrollLooping.current = true;
|
||||
try {
|
||||
let shouldScroll = true;
|
||||
let curParams = params;
|
||||
while (shouldScroll) {
|
||||
const [
|
||||
distFromTop,
|
||||
distFromBottom,
|
||||
scrollOffset,
|
||||
isScrolledUp,
|
||||
isScrolledDown,
|
||||
] = curParams;
|
||||
const targetOffset = getScrollTargetOffset(
|
||||
distFromTop,
|
||||
distFromBottom,
|
||||
scrollOffset,
|
||||
!!isScrolledUp,
|
||||
!!isScrolledDown
|
||||
);
|
||||
const scrollingUpAtTop = !!(
|
||||
isScrolledUp && targetOffset <= scrollOffset
|
||||
);
|
||||
const scrollingDownAtBottom = !!(
|
||||
isScrolledDown && targetOffset >= scrollOffset
|
||||
);
|
||||
shouldScroll =
|
||||
targetOffset >= 0 &&
|
||||
isDraggingCellJS.current &&
|
||||
!scrollingUpAtTop &&
|
||||
!scrollingDownAtBottom;
|
||||
|
||||
if (shouldScroll) {
|
||||
try {
|
||||
curParams = await scrollToAsync(targetOffset);
|
||||
} catch (err) {}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
autoscrollLooping.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
const checkAutoscroll = useNode(
|
||||
cond(
|
||||
and(
|
||||
isAtEdge,
|
||||
not(and(isAtTopEdge, isScrolledUp)),
|
||||
not(and(isAtBottomEdge, isScrolledDown)),
|
||||
eq(panGestureState, GestureState.ACTIVE),
|
||||
not(isAutoScrollInProgress.current.native)
|
||||
),
|
||||
call(autoscrollParams, autoscroll)
|
||||
)
|
||||
);
|
||||
|
||||
useCode(() => checkAutoscroll, []);
|
||||
|
||||
const onScrollNode = useNode(
|
||||
cond(
|
||||
and(
|
||||
isAutoScrollInProgress.current.native,
|
||||
or(
|
||||
// We've scrolled to where we want to be
|
||||
lessOrEq(
|
||||
abs(sub(targetScrollOffset, scrollOffset)),
|
||||
SCROLL_POSITION_TOLERANCE
|
||||
),
|
||||
// We're at the start, but still want to scroll farther up
|
||||
and(isScrolledUp, lessOrEq(targetScrollOffset, scrollOffset)),
|
||||
// We're at the end, but still want to scroll further down
|
||||
and(isScrolledDown, greaterOrEq(targetScrollOffset, scrollOffset))
|
||||
)
|
||||
),
|
||||
[
|
||||
// Finish scrolling
|
||||
set(isAutoScrollInProgress.current.native, 0),
|
||||
call(autoscrollParams, onAutoscrollComplete),
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
return onScrollNode;
|
||||
}
|
||||
107
src/hooks/useCellTranslate.tsx
Normal file
107
src/hooks/useCellTranslate.tsx
Normal file
@ -0,0 +1,107 @@
|
||||
import Animated, {
|
||||
add,
|
||||
block,
|
||||
call,
|
||||
clockRunning,
|
||||
cond,
|
||||
eq,
|
||||
onChange,
|
||||
stopClock,
|
||||
useCode,
|
||||
useValue,
|
||||
} from "react-native-reanimated";
|
||||
import { useAnimatedValues } from "../context/AnimatedValueContext";
|
||||
import { useRefs } from "../context/RefContext";
|
||||
import { setupCell, springFill } from "../procs";
|
||||
import { useSpring } from "./useSpring";
|
||||
import { useNode } from "../hooks/useNode";
|
||||
import { useDraggableFlatListContext } from "../context/DraggableFlatListContext";
|
||||
|
||||
type Params = {
|
||||
cellIndex: Animated.Value<number>;
|
||||
cellSize: Animated.Value<number>;
|
||||
cellOffset: Animated.Value<number>;
|
||||
};
|
||||
|
||||
export function useCellTranslate({ cellIndex, cellSize, cellOffset }: Params) {
|
||||
const {
|
||||
activeIndexAnim,
|
||||
activeCellSize,
|
||||
hoverAnim,
|
||||
scrollOffset,
|
||||
spacerIndexAnim,
|
||||
placeholderOffset,
|
||||
isDraggingCell,
|
||||
resetTouchedCell,
|
||||
disabled,
|
||||
} = useAnimatedValues();
|
||||
const { animationConfigRef } = useRefs();
|
||||
const { onDragEnd } = useDraggableFlatListContext();
|
||||
|
||||
const cellSpring = useSpring({ config: animationConfigRef.current });
|
||||
const { clock, state, config } = cellSpring;
|
||||
|
||||
const isAfterActive = useValue(0);
|
||||
const isClockRunning = useNode(clockRunning(clock));
|
||||
|
||||
const runSpring = useNode(springFill(clock, state, config));
|
||||
|
||||
// Even though this is the same value as hoverOffset passed via context
|
||||
// the android context value lags behind the actual value on autoscroll
|
||||
const cellHoverOffset = useNode(add(hoverAnim, scrollOffset));
|
||||
|
||||
const onFinished = useNode(
|
||||
cond(isClockRunning, [
|
||||
stopClock(clock),
|
||||
cond(eq(cellIndex, activeIndexAnim), [
|
||||
resetTouchedCell,
|
||||
call([activeIndexAnim, spacerIndexAnim], onDragEnd),
|
||||
]),
|
||||
])
|
||||
);
|
||||
|
||||
const prevTrans = useValue<number>(0);
|
||||
const prevSpacerIndex = useValue<number>(-1);
|
||||
const prevIsDraggingCell = useValue<number>(0);
|
||||
|
||||
const cellTranslate = useNode(
|
||||
setupCell(
|
||||
cellIndex,
|
||||
cellSize,
|
||||
cellOffset,
|
||||
isAfterActive,
|
||||
prevTrans,
|
||||
prevSpacerIndex,
|
||||
activeIndexAnim,
|
||||
activeCellSize,
|
||||
cellHoverOffset,
|
||||
spacerIndexAnim,
|
||||
//@ts-ignore
|
||||
config.toValue,
|
||||
state.position,
|
||||
state.time,
|
||||
state.finished,
|
||||
runSpring,
|
||||
onFinished,
|
||||
isDraggingCell,
|
||||
placeholderOffset,
|
||||
prevIsDraggingCell,
|
||||
clock,
|
||||
disabled
|
||||
)
|
||||
);
|
||||
|
||||
// This is required to continually evaluate values
|
||||
useCode(
|
||||
() =>
|
||||
block([
|
||||
onChange(cellTranslate, []),
|
||||
onChange(prevTrans, []),
|
||||
onChange(cellSize, []),
|
||||
onChange(cellOffset, []),
|
||||
]),
|
||||
[]
|
||||
);
|
||||
|
||||
return state.position;
|
||||
}
|
||||
10
src/hooks/useNode.tsx
Normal file
10
src/hooks/useNode.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { useRef } from "react";
|
||||
import Animated from "react-native-reanimated";
|
||||
|
||||
export function useNode<T>(node: Animated.Node<T>) {
|
||||
const ref = useRef<Animated.Node<T> | null>(null);
|
||||
if (ref.current === null) {
|
||||
ref.current = node;
|
||||
}
|
||||
return ref.current;
|
||||
}
|
||||
53
src/hooks/useOnCellActiveAnimation.ts
Normal file
53
src/hooks/useOnCellActiveAnimation.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import Animated, {
|
||||
block,
|
||||
clockRunning,
|
||||
cond,
|
||||
onChange,
|
||||
set,
|
||||
startClock,
|
||||
stopClock,
|
||||
useCode,
|
||||
} from "react-native-reanimated";
|
||||
import { useAnimatedValues } from "../context/AnimatedValueContext";
|
||||
import { useIsActive } from "../context/CellContext";
|
||||
import { springFill } from "../procs";
|
||||
import { useSpring } from "./useSpring";
|
||||
|
||||
type Params = {
|
||||
animationConfig: Partial<Animated.SpringConfig>;
|
||||
};
|
||||
|
||||
export function useOnCellActiveAnimation(
|
||||
{ animationConfig }: Params = { animationConfig: {} }
|
||||
) {
|
||||
const { clock, state, config } = useSpring({ config: animationConfig });
|
||||
|
||||
const { isDraggingCell } = useAnimatedValues();
|
||||
const isActive = useIsActive();
|
||||
|
||||
useCode(
|
||||
() =>
|
||||
block([
|
||||
onChange(isDraggingCell, [
|
||||
//@ts-ignore
|
||||
set(config.toValue, cond(isDraggingCell, 1, 0)),
|
||||
startClock(clock),
|
||||
]),
|
||||
cond(clockRunning(clock), [
|
||||
springFill(clock, state, config),
|
||||
cond(state.finished, [
|
||||
stopClock(clock),
|
||||
set(state.finished, 0),
|
||||
set(state.time, 0),
|
||||
set(state.velocity, 0),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
[]
|
||||
);
|
||||
|
||||
return {
|
||||
isActive,
|
||||
onActiveAnim: state.position,
|
||||
};
|
||||
}
|
||||
47
src/hooks/useSpring.tsx
Normal file
47
src/hooks/useSpring.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import { useMemo } from "react";
|
||||
import Animated, { Clock, useValue } from "react-native-reanimated";
|
||||
import { DEFAULT_ANIMATION_CONFIG } from "../constants";
|
||||
|
||||
type Params = {
|
||||
config: Partial<Animated.SpringConfig>;
|
||||
};
|
||||
|
||||
export function useSpring(
|
||||
{ config: configParam }: Params = { config: DEFAULT_ANIMATION_CONFIG }
|
||||
) {
|
||||
const toValue = useValue<number>(0);
|
||||
const clock = useMemo(() => new Clock(), []);
|
||||
|
||||
const finished = useValue<number>(0);
|
||||
const velocity = useValue<number>(0);
|
||||
const position = useValue<number>(0);
|
||||
const time = useValue<number>(0);
|
||||
|
||||
const state = useMemo(
|
||||
() => ({
|
||||
finished,
|
||||
velocity,
|
||||
position,
|
||||
time,
|
||||
}),
|
||||
[finished, velocity, position, time]
|
||||
);
|
||||
|
||||
const config = useMemo(
|
||||
() => ({
|
||||
...DEFAULT_ANIMATION_CONFIG,
|
||||
...configParam,
|
||||
toValue,
|
||||
}),
|
||||
[configParam, toValue]
|
||||
) as Animated.SpringConfig;
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
clock,
|
||||
state,
|
||||
config,
|
||||
}),
|
||||
[clock, state, config]
|
||||
);
|
||||
}
|
||||
1169
src/index.tsx
1169
src/index.tsx
File diff suppressed because it is too large
Load Diff
287
src/procs.ts
Normal file
287
src/procs.ts
Normal file
@ -0,0 +1,287 @@
|
||||
import Animated, {
|
||||
clockRunning,
|
||||
not,
|
||||
startClock,
|
||||
stopClock,
|
||||
} from "react-native-reanimated";
|
||||
import { isWeb } from "./constants";
|
||||
|
||||
const {
|
||||
set,
|
||||
cond,
|
||||
add,
|
||||
sub,
|
||||
block,
|
||||
eq,
|
||||
neq,
|
||||
and,
|
||||
divide,
|
||||
greaterThan,
|
||||
greaterOrEq,
|
||||
Value,
|
||||
spring,
|
||||
lessThan,
|
||||
lessOrEq,
|
||||
multiply,
|
||||
} = Animated;
|
||||
|
||||
if (!Animated.proc) {
|
||||
throw new Error("Incompatible Reanimated version (proc not found)");
|
||||
}
|
||||
|
||||
// clock procs don't seem to work in web, not sure if there's a perf benefit to web procs anyway?
|
||||
const proc = isWeb ? <T>(cb: T) => cb : Animated.proc;
|
||||
|
||||
export const getIsAfterActive = proc(
|
||||
(currentIndex: Animated.Node<number>, activeIndex: Animated.Node<number>) =>
|
||||
greaterThan(currentIndex, activeIndex)
|
||||
);
|
||||
|
||||
export const hardReset = proc(
|
||||
(
|
||||
position: Animated.Value<number>,
|
||||
finished: Animated.Value<number>,
|
||||
time: Animated.Value<number>,
|
||||
toValue: Animated.Value<number>
|
||||
) =>
|
||||
block([set(position, 0), set(finished, 0), set(time, 0), set(toValue, 0)])
|
||||
);
|
||||
|
||||
/**
|
||||
* The in react-native-reanimated.d.ts definition of `proc` only has generics
|
||||
* for up to 10 arguments. We cast it to accept any params to avoid errors when
|
||||
* type-checking.
|
||||
*/
|
||||
type RetypedProc = (cb: (...params: any) => Animated.Node<number>) => typeof cb;
|
||||
|
||||
export const setupCell = proc(
|
||||
(
|
||||
currentIndex: Animated.Value<number>,
|
||||
size: Animated.Node<number>,
|
||||
offset: Animated.Node<number>,
|
||||
isAfterActive: Animated.Value<number>,
|
||||
prevToValue: Animated.Value<number>,
|
||||
prevSpacerIndex: Animated.Value<number>,
|
||||
activeIndex: Animated.Node<number>,
|
||||
activeCellSize: Animated.Node<number>,
|
||||
hoverOffset: Animated.Node<number>,
|
||||
spacerIndex: Animated.Value<number>,
|
||||
toValue: Animated.Value<number>,
|
||||
position: Animated.Value<number>,
|
||||
time: Animated.Value<number>,
|
||||
finished: Animated.Value<number>,
|
||||
runSpring: Animated.Node<number>,
|
||||
onFinished: Animated.Node<number>,
|
||||
isDraggingCell: Animated.Node<number>,
|
||||
placeholderOffset: Animated.Value<number>,
|
||||
prevIsDraggingCell: Animated.Value<number>,
|
||||
clock: Animated.Clock,
|
||||
disabled: Animated.Node<number>
|
||||
) =>
|
||||
block([
|
||||
cond(
|
||||
greaterThan(activeIndex, -1),
|
||||
[
|
||||
// Only update spacer if touch is not disabled.
|
||||
// Fixes android bugs where state would update with invalid touch values on touch end.
|
||||
cond(not(disabled), [
|
||||
// Determine whether this cell is after the active cell in the list
|
||||
set(isAfterActive, getIsAfterActive(currentIndex, activeIndex)),
|
||||
|
||||
// Determining spacer index is hard to visualize, see diagram: https://i.imgur.com/jRPf5t3.jpg
|
||||
cond(
|
||||
isAfterActive,
|
||||
[
|
||||
cond(
|
||||
and(
|
||||
greaterOrEq(add(hoverOffset, activeCellSize), offset),
|
||||
lessThan(
|
||||
add(hoverOffset, activeCellSize),
|
||||
add(offset, divide(size, 2))
|
||||
)
|
||||
),
|
||||
set(spacerIndex, sub(currentIndex, 1))
|
||||
),
|
||||
cond(
|
||||
and(
|
||||
greaterOrEq(
|
||||
add(hoverOffset, activeCellSize),
|
||||
add(offset, divide(size, 2))
|
||||
),
|
||||
lessThan(
|
||||
add(hoverOffset, activeCellSize),
|
||||
add(offset, size)
|
||||
)
|
||||
),
|
||||
set(spacerIndex, currentIndex)
|
||||
),
|
||||
],
|
||||
cond(lessThan(currentIndex, activeIndex), [
|
||||
cond(
|
||||
and(
|
||||
lessThan(hoverOffset, add(offset, size)),
|
||||
greaterOrEq(hoverOffset, add(offset, divide(size, 2)))
|
||||
),
|
||||
set(spacerIndex, add(currentIndex, 1))
|
||||
),
|
||||
cond(
|
||||
and(
|
||||
greaterOrEq(hoverOffset, offset),
|
||||
lessThan(hoverOffset, add(offset, divide(size, 2)))
|
||||
),
|
||||
set(spacerIndex, currentIndex)
|
||||
),
|
||||
])
|
||||
),
|
||||
// Set placeholder offset
|
||||
cond(eq(spacerIndex, currentIndex), [
|
||||
set(
|
||||
placeholderOffset,
|
||||
cond(
|
||||
isAfterActive,
|
||||
add(sub(offset, activeCellSize), size),
|
||||
offset
|
||||
)
|
||||
),
|
||||
]),
|
||||
]),
|
||||
|
||||
cond(
|
||||
eq(currentIndex, activeIndex),
|
||||
[
|
||||
// If this cell is the active cell
|
||||
cond(
|
||||
isDraggingCell,
|
||||
[
|
||||
// Set its position to the drag position
|
||||
set(position, sub(hoverOffset, offset)),
|
||||
],
|
||||
[
|
||||
// Active item, not pressed in
|
||||
|
||||
// Set value hovering element will snap to once released
|
||||
cond(prevIsDraggingCell, [
|
||||
set(toValue, sub(placeholderOffset, offset)),
|
||||
// The clock starts automatically when toValue changes, however, we need to handle the
|
||||
// case where the item should snap back to its original location and toValue doesn't change
|
||||
cond(eq(prevToValue, toValue), [
|
||||
cond(clockRunning(clock), stopClock(clock)),
|
||||
set(time, 0),
|
||||
set(finished, 0),
|
||||
startClock(clock),
|
||||
]),
|
||||
]),
|
||||
]
|
||||
),
|
||||
],
|
||||
[
|
||||
// Not the active item
|
||||
// Translate cell down if it is before active index and active cell has passed it.
|
||||
// Translate cell up if it is after the active index and active cell has passed it.
|
||||
set(
|
||||
toValue,
|
||||
cond(
|
||||
cond(
|
||||
isAfterActive,
|
||||
lessOrEq(currentIndex, spacerIndex),
|
||||
greaterOrEq(currentIndex, spacerIndex)
|
||||
),
|
||||
cond(
|
||||
isAfterActive,
|
||||
multiply(activeCellSize, -1),
|
||||
activeCellSize
|
||||
),
|
||||
0
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
// If this cell should animate somewhere new, reset its state and start its clock
|
||||
cond(neq(toValue, prevToValue), [
|
||||
cond(clockRunning(clock), stopClock(clock)),
|
||||
set(time, 0),
|
||||
set(finished, 0),
|
||||
startClock(clock),
|
||||
]),
|
||||
],
|
||||
[
|
||||
// Reset the spacer index when drag ends
|
||||
set(spacerIndex, -1),
|
||||
set(position, 0),
|
||||
]
|
||||
),
|
||||
cond(neq(prevSpacerIndex, spacerIndex), [
|
||||
cond(eq(spacerIndex, -1), [
|
||||
// Hard reset to prevent stale state bugs
|
||||
cond(clockRunning(clock), stopClock(clock)),
|
||||
hardReset(position, finished, time, toValue),
|
||||
]),
|
||||
]),
|
||||
cond(finished, [onFinished, set(time, 0), set(finished, 0)]),
|
||||
set(prevSpacerIndex, spacerIndex),
|
||||
set(prevToValue, toValue),
|
||||
set(prevIsDraggingCell, isDraggingCell),
|
||||
cond(clockRunning(clock), runSpring),
|
||||
position,
|
||||
])
|
||||
);
|
||||
|
||||
const betterSpring = (proc as RetypedProc)(
|
||||
(
|
||||
finished: Animated.Value<number>,
|
||||
velocity: Animated.Value<number>,
|
||||
position: Animated.Value<number>,
|
||||
time: Animated.Value<number>,
|
||||
prevPosition: Animated.Value<number>,
|
||||
toValue: Animated.Value<number>,
|
||||
damping: Animated.Value<number>,
|
||||
mass: Animated.Value<number>,
|
||||
stiffness: Animated.Value<number>,
|
||||
overshootClamping: Animated.SpringConfig["overshootClamping"],
|
||||
restSpeedThreshold: Animated.Value<number>,
|
||||
restDisplacementThreshold: Animated.Value<number>,
|
||||
clock: Animated.Clock
|
||||
) =>
|
||||
spring(
|
||||
clock,
|
||||
{
|
||||
finished,
|
||||
velocity,
|
||||
position,
|
||||
time,
|
||||
// @ts-ignore -- https://github.com/software-mansion/react-native-reanimated/blob/master/src/animations/spring.js#L177
|
||||
prevPosition,
|
||||
},
|
||||
{
|
||||
toValue,
|
||||
damping,
|
||||
mass,
|
||||
stiffness,
|
||||
overshootClamping,
|
||||
restDisplacementThreshold,
|
||||
restSpeedThreshold,
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export function springFill(
|
||||
clock: Animated.Clock,
|
||||
state: Animated.SpringState,
|
||||
config: Animated.SpringConfig
|
||||
) {
|
||||
return betterSpring(
|
||||
state.finished,
|
||||
state.velocity,
|
||||
state.position,
|
||||
state.time,
|
||||
new Value(0),
|
||||
config.toValue,
|
||||
config.damping,
|
||||
config.mass,
|
||||
config.stiffness,
|
||||
config.overshootClamping,
|
||||
config.restSpeedThreshold,
|
||||
config.restDisplacementThreshold,
|
||||
clock
|
||||
);
|
||||
}
|
||||
282
src/procs.tsx
282
src/procs.tsx
@ -1,282 +0,0 @@
|
||||
import Animated from "react-native-reanimated";
|
||||
|
||||
const {
|
||||
or,
|
||||
set,
|
||||
cond,
|
||||
add,
|
||||
sub,
|
||||
block,
|
||||
eq,
|
||||
neq,
|
||||
and,
|
||||
divide,
|
||||
greaterThan,
|
||||
greaterOrEq,
|
||||
not,
|
||||
Value,
|
||||
spring,
|
||||
lessThan,
|
||||
lessOrEq,
|
||||
multiply,
|
||||
} = Animated;
|
||||
let { proc } = Animated;
|
||||
|
||||
if (!proc) {
|
||||
console.warn("Use reanimated > 1.3 for optimal perf");
|
||||
const procStub = <T,>(cb: T) => cb;
|
||||
proc = procStub;
|
||||
}
|
||||
|
||||
export const getIsAfterActive = proc(
|
||||
(currentIndex: Animated.Node<number>, activeIndex: Animated.Node<number>) =>
|
||||
greaterThan(currentIndex, activeIndex)
|
||||
);
|
||||
|
||||
export const getCellStart = proc(
|
||||
(
|
||||
isAfterActive: Animated.Node<number>,
|
||||
offset: Animated.Node<number>,
|
||||
activeCellSize: Animated.Node<number>,
|
||||
scrollOffset: Animated.Node<number>
|
||||
) =>
|
||||
sub(cond(isAfterActive, sub(offset, activeCellSize), offset), scrollOffset)
|
||||
);
|
||||
|
||||
export const getOnChangeTranslate = proc(
|
||||
(
|
||||
translate: Animated.Node<number>,
|
||||
isAfterActive: Animated.Node<number>,
|
||||
initialized: Animated.Value<number>,
|
||||
toValue: Animated.Value<number>,
|
||||
isPressedIn: Animated.Node<number>
|
||||
) =>
|
||||
block([
|
||||
cond(or(not(isAfterActive), initialized), [], set(initialized, 1)),
|
||||
cond(isPressedIn, set(toValue, translate)),
|
||||
])
|
||||
);
|
||||
|
||||
export const hardReset = proc(
|
||||
(
|
||||
position: Animated.Value<number>,
|
||||
finished: Animated.Value<number>,
|
||||
time: Animated.Value<number>,
|
||||
toValue: Animated.Value<number>
|
||||
) =>
|
||||
block([set(position, 0), set(finished, 0), set(time, 0), set(toValue, 0)])
|
||||
);
|
||||
|
||||
/**
|
||||
* The in react-native-reanimated.d.ts definition of `proc` only has generics
|
||||
* for up to 10 arguments. We cast it to accept any params to avoid errors when
|
||||
* type-checking.
|
||||
*/
|
||||
type RetypedProc = (cb: (...params: any) => Animated.Node<number>) => typeof cb;
|
||||
|
||||
export const setupCell = proc(
|
||||
(
|
||||
currentIndex: Animated.Value<number>,
|
||||
initialized: Animated.Value<number>,
|
||||
size: Animated.Node<number>,
|
||||
offset: Animated.Node<number>,
|
||||
isAfterActive: Animated.Value<number>,
|
||||
translate: Animated.Value<number>,
|
||||
prevTrans: Animated.Value<number>,
|
||||
prevSpacerIndex: Animated.Value<number>,
|
||||
activeIndex: Animated.Node<number>,
|
||||
activeCellSize: Animated.Node<number>,
|
||||
hoverOffset: Animated.Node<number>,
|
||||
scrollOffset: Animated.Node<number>,
|
||||
isHovering: Animated.Node<number>,
|
||||
hoverTo: Animated.Value<number>,
|
||||
hasMoved: Animated.Value<number>,
|
||||
spacerIndex: Animated.Value<number>,
|
||||
toValue: Animated.Value<number>,
|
||||
position: Animated.Value<number>,
|
||||
time: Animated.Value<number>,
|
||||
finished: Animated.Value<number>,
|
||||
runSpring: Animated.Node<number>,
|
||||
onHasMoved: Animated.Node<number>,
|
||||
onChangeSpacerIndex: Animated.Node<number>,
|
||||
onFinished: Animated.Node<number>,
|
||||
isPressedIn: Animated.Node<number>,
|
||||
placeholderOffset: Animated.Value<number>
|
||||
) =>
|
||||
block([
|
||||
set(isAfterActive, getIsAfterActive(currentIndex, activeIndex)),
|
||||
|
||||
// Determining spacer index is hard to visualize.
|
||||
// see diagram here: https://i.imgur.com/jRPf5t3.jpg
|
||||
cond(
|
||||
isPressedIn,
|
||||
cond(
|
||||
isAfterActive,
|
||||
[
|
||||
cond(
|
||||
and(
|
||||
greaterOrEq(add(hoverOffset, activeCellSize), offset),
|
||||
lessThan(
|
||||
add(hoverOffset, activeCellSize),
|
||||
add(offset, divide(size, 2))
|
||||
)
|
||||
),
|
||||
set(spacerIndex, sub(currentIndex, 1))
|
||||
),
|
||||
cond(
|
||||
and(
|
||||
greaterOrEq(
|
||||
add(hoverOffset, activeCellSize),
|
||||
add(offset, divide(size, 2))
|
||||
),
|
||||
lessThan(add(hoverOffset, activeCellSize), add(offset, size))
|
||||
),
|
||||
set(spacerIndex, currentIndex)
|
||||
),
|
||||
],
|
||||
cond(lessThan(currentIndex, activeIndex), [
|
||||
cond(
|
||||
and(
|
||||
lessThan(hoverOffset, add(offset, size)),
|
||||
greaterOrEq(hoverOffset, add(offset, divide(size, 2)))
|
||||
),
|
||||
set(spacerIndex, add(currentIndex, 1))
|
||||
),
|
||||
cond(
|
||||
and(
|
||||
greaterOrEq(hoverOffset, offset),
|
||||
lessThan(hoverOffset, add(offset, divide(size, 2)))
|
||||
),
|
||||
set(spacerIndex, currentIndex)
|
||||
),
|
||||
])
|
||||
)
|
||||
),
|
||||
|
||||
// Translate cell down if it is before active index and active cell has passed it.
|
||||
// Translate cell up if it is after the active index and active cell has passed it.
|
||||
cond(
|
||||
neq(currentIndex, activeIndex),
|
||||
set(
|
||||
translate,
|
||||
cond(
|
||||
cond(
|
||||
isAfterActive,
|
||||
lessOrEq(currentIndex, spacerIndex),
|
||||
greaterOrEq(currentIndex, spacerIndex)
|
||||
),
|
||||
cond(
|
||||
isHovering,
|
||||
cond(isAfterActive, multiply(activeCellSize, -1), activeCellSize),
|
||||
0
|
||||
),
|
||||
0
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
// Set value hovering element will snap to once released
|
||||
cond(
|
||||
and(isHovering, eq(spacerIndex, currentIndex)),
|
||||
set(
|
||||
hoverTo,
|
||||
sub(
|
||||
offset,
|
||||
scrollOffset,
|
||||
cond(isAfterActive, sub(activeCellSize, size), 0) // Account for cells of differing size
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
set(toValue, translate),
|
||||
cond(and(isPressedIn, neq(translate, prevTrans)), [
|
||||
set(prevTrans, translate),
|
||||
getOnChangeTranslate(
|
||||
translate,
|
||||
isAfterActive,
|
||||
initialized,
|
||||
toValue,
|
||||
isPressedIn
|
||||
),
|
||||
cond(hasMoved, onHasMoved, set(position, translate)),
|
||||
]),
|
||||
cond(neq(prevSpacerIndex, spacerIndex), [
|
||||
set(prevSpacerIndex, spacerIndex),
|
||||
cond(eq(spacerIndex, -1), [
|
||||
// Hard reset to prevent stale state bugs
|
||||
onChangeSpacerIndex,
|
||||
hardReset(position, finished, time, toValue),
|
||||
]),
|
||||
]),
|
||||
runSpring,
|
||||
cond(finished, [onFinished, set(time, 0), set(finished, 0)]),
|
||||
cond(
|
||||
eq(spacerIndex, currentIndex),
|
||||
set(
|
||||
placeholderOffset,
|
||||
cond(isAfterActive, add(sub(offset, activeCellSize), size), offset)
|
||||
)
|
||||
),
|
||||
position,
|
||||
])
|
||||
);
|
||||
|
||||
const betterSpring = (proc as RetypedProc)(
|
||||
(
|
||||
finished: Animated.Value<number>,
|
||||
velocity: Animated.Value<number>,
|
||||
position: Animated.Value<number>,
|
||||
time: Animated.Value<number>,
|
||||
prevPosition: Animated.Value<number>,
|
||||
toValue: Animated.Value<number>,
|
||||
damping: Animated.Value<number>,
|
||||
mass: Animated.Value<number>,
|
||||
stiffness: Animated.Value<number>,
|
||||
overshootClamping: Animated.SpringConfig["overshootClamping"],
|
||||
restSpeedThreshold: Animated.Value<number>,
|
||||
restDisplacementThreshold: Animated.Value<number>,
|
||||
clock: Animated.Clock
|
||||
) =>
|
||||
spring(
|
||||
clock,
|
||||
{
|
||||
finished,
|
||||
velocity,
|
||||
position,
|
||||
time,
|
||||
// @ts-ignore -- https://github.com/software-mansion/react-native-reanimated/blob/master/src/animations/spring.js#L177
|
||||
prevPosition,
|
||||
},
|
||||
{
|
||||
toValue,
|
||||
damping,
|
||||
mass,
|
||||
stiffness,
|
||||
overshootClamping,
|
||||
restDisplacementThreshold,
|
||||
restSpeedThreshold,
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export function springFill(
|
||||
clock: Animated.Clock,
|
||||
state: Animated.SpringState,
|
||||
config: Animated.SpringConfig
|
||||
) {
|
||||
return betterSpring(
|
||||
state.finished,
|
||||
state.velocity,
|
||||
state.position,
|
||||
state.time,
|
||||
new Value(0),
|
||||
config.toValue,
|
||||
config.damping,
|
||||
config.mass,
|
||||
config.stiffness,
|
||||
config.overshootClamping,
|
||||
config.restSpeedThreshold,
|
||||
config.restDisplacementThreshold,
|
||||
clock
|
||||
);
|
||||
}
|
||||
68
src/types.ts
Normal file
68
src/types.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import React from "react";
|
||||
import { FlatListProps, StyleProp, ViewStyle } from "react-native";
|
||||
import { FlatList } from "react-native-gesture-handler";
|
||||
import Animated from "react-native-reanimated";
|
||||
import { DEFAULT_PROPS } from "./constants";
|
||||
|
||||
export type DragEndParams<T> = {
|
||||
data: T[];
|
||||
from: number;
|
||||
to: number;
|
||||
};
|
||||
type Modify<T, R> = Omit<T, keyof R> & R;
|
||||
|
||||
type DefaultProps = Readonly<typeof DEFAULT_PROPS>;
|
||||
|
||||
export type DraggableFlatListProps<T> = Modify<
|
||||
FlatListProps<T>,
|
||||
{
|
||||
data: T[];
|
||||
activationDistance?: number;
|
||||
animationConfig?: Partial<Animated.SpringConfig>;
|
||||
autoscrollSpeed?: number;
|
||||
autoscrollThreshold?: number;
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
debug?: boolean;
|
||||
dragItemOverflow?: boolean;
|
||||
keyExtractor: (item: T, index: number) => string;
|
||||
onDragBegin?: (index: number) => void;
|
||||
onDragEnd?: (params: DragEndParams<T>) => void;
|
||||
onPlaceholderIndexChange?: (placeholderIndex: number) => void;
|
||||
onRef?: (ref: FlatList<T>) => void;
|
||||
onRelease?: (index: number) => void;
|
||||
onScrollOffsetChange?: (scrollOffset: number) => void;
|
||||
renderItem: RenderItem<T>;
|
||||
renderPlaceholder?: RenderPlaceholder<T>;
|
||||
simultaneousHandlers?: React.Ref<any> | React.Ref<any>[];
|
||||
} & Partial<DefaultProps>
|
||||
>;
|
||||
|
||||
export type RenderPlaceholder<T> = (params: {
|
||||
item: T;
|
||||
index: number;
|
||||
}) => JSX.Element;
|
||||
|
||||
export type RenderItemParams<T> = {
|
||||
item: T;
|
||||
index?: number; // This is technically a "last known index" since cells don't necessarily rerender when their index changes
|
||||
drag: () => void;
|
||||
isActive: boolean;
|
||||
};
|
||||
|
||||
export type RenderItem<T> = (params: RenderItemParams<T>) => React.ReactNode;
|
||||
|
||||
export type AnimatedFlatListType = <T>(
|
||||
props: Animated.AnimateProps<
|
||||
FlatListProps<T> & {
|
||||
ref: React.Ref<FlatList<T>>;
|
||||
simultaneousHandlers?: React.Ref<any> | React.Ref<any>[];
|
||||
}
|
||||
>
|
||||
) => React.ReactElement;
|
||||
|
||||
export type CellData = {
|
||||
measurements: {
|
||||
size: number;
|
||||
offset: number;
|
||||
};
|
||||
};
|
||||
5
src/utils.ts
Normal file
5
src/utils.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import React from "react";
|
||||
|
||||
// Fixes bug with useMemo + generic types:
|
||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/37087#issuecomment-542793243
|
||||
export const typedMemo: <T>(c: T) => T = React.memo;
|
||||
Loading…
Reference in New Issue
Block a user