mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2024-11-19 01:40:12 +01:00
GestureHandler
This commit is contained in:
parent
3a3e66c79c
commit
1c9fa414eb
@ -436,7 +436,7 @@ const InitRoot = () => (
|
||||
<InitStack.Screen
|
||||
name="ReorderWallets"
|
||||
component={ReorderWalletsStackRoot}
|
||||
options={{ headerShown: false, gestureEnabled: false, stackPresentation: isDesktop ? 'containedModal' : 'fullScreenModal' }}
|
||||
options={{ headerShown: false, gestureEnabled: false, stackPresentation: isDesktop ? 'containedModal' : 'modal' }}
|
||||
/>
|
||||
<InitStack.Screen
|
||||
name={isHandset ? 'Navigation' : 'DrawerRoot'}
|
||||
|
@ -314,7 +314,7 @@ const WalletsCarousel = forwardRef((props, ref) => {
|
||||
};
|
||||
|
||||
const { width } = useWindowDimensions();
|
||||
const sliderHeight = 190;
|
||||
const sliderHeight = 195;
|
||||
const itemWidth = width * 0.82 > 375 ? 375 : width * 0.82;
|
||||
return (
|
||||
<FlatList
|
||||
@ -332,7 +332,7 @@ const WalletsCarousel = forwardRef((props, ref) => {
|
||||
showsHorizontalScrollIndicator={false}
|
||||
initialNumToRender={10}
|
||||
ListHeaderComponent={ListHeaderComponent}
|
||||
style={props.horizontal ? { height: sliderHeight + 9 } : {}}
|
||||
style={props.horizontal ? { minHeight: sliderHeight + 9 } : {}}
|
||||
onScrollToIndexFailed={onScrollToIndexFailed}
|
||||
{...props}
|
||||
/>
|
||||
|
@ -0,0 +1,91 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import Animated, { interpolate, interpolateNode, multiply } from 'react-native-reanimated';
|
||||
import { useDraggableFlatListContext } from '../context/draggableFlatListContext';
|
||||
import { useNode } from '../hooks/useNode';
|
||||
import { useOnCellActiveAnimation } from '../hooks/useOnCellActiveAnimation';
|
||||
export { 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 { horizontal } = useDraggableFlatListContext<any>();
|
||||
const scale = isActive ? animScale : 1;
|
||||
return (
|
||||
<Animated.View style={[{ transform: [{ scaleX: scale }, { scaleY: scale }] }, horizontal && styles.horizontal]}>
|
||||
{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 { horizontal } = useDraggableFlatListContext<any>();
|
||||
|
||||
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, horizontal && styles.horizontal]}>{children}</Animated.View>;
|
||||
};
|
||||
|
||||
type OpacityProps = {
|
||||
activeOpacity?: number;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const OpacityDecorator = ({ activeOpacity = 0.25, children }: OpacityProps) => {
|
||||
const { isActive, onActiveAnim } = useOnCellActiveAnimation();
|
||||
const { horizontal } = useDraggableFlatListContext<any>();
|
||||
|
||||
const opacity = useNode(
|
||||
interpolateFn(onActiveAnim, {
|
||||
inputRange: [0, 1],
|
||||
outputRange: [1, activeOpacity],
|
||||
}),
|
||||
);
|
||||
|
||||
const style = {
|
||||
opacity: isActive ? opacity : 1,
|
||||
};
|
||||
|
||||
return <Animated.View style={[style, horizontal && styles.horizontal]}>{children}</Animated.View>;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
horizontal: {
|
||||
flexDirection: 'row',
|
||||
flex: 1,
|
||||
},
|
||||
});
|
@ -0,0 +1,130 @@
|
||||
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react';
|
||||
import { findNodeHandle, LayoutChangeEvent, MeasureLayoutOnSuccessCallback, StyleProp, ViewStyle } 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;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
};
|
||||
|
||||
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
|
||||
{...props}
|
||||
// Including both animated styles and non-animated styles causes react-native-web
|
||||
// to ignore updates in non-animated styles. Solution is to separate animated styles from non-animated styles
|
||||
style={[props.style, style]}
|
||||
>
|
||||
<CellProvider isActive={isActive}>{children}</CellProvider>
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
export default typedMemo(CellRendererComponent);
|
@ -0,0 +1,322 @@
|
||||
import React, { ForwardedRef, useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
||||
import { ListRenderItem, FlatListProps, NativeScrollEvent, NativeSyntheticEvent, LayoutChangeEvent } 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,
|
||||
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 = ({ nativeEvent: { layout } }: LayoutChangeEvent) => {
|
||||
containerSize.setValue(props.horizontal ? layout.width : layout.height);
|
||||
};
|
||||
|
||||
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 }) => {
|
||||
const key = keyExtractor(item, index);
|
||||
if (index !== keyToIndexRef.current.get(key)) keyToIndexRef.current.set(key, index);
|
||||
|
||||
return <RowItem item={item} itemKey={key} 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)],
|
||||
[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)), [
|
||||
set(touchAbsolute, props.horizontal ? x : y),
|
||||
]),
|
||||
},
|
||||
]),
|
||||
[activeIndexAnim, disabled, 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);
|
||||
}}
|
||||
/>
|
||||
{!!props.renderPlaceholder && <PlaceholderItem renderPlaceholder={props.renderPlaceholder} />}
|
||||
<AnimatedFlatList
|
||||
{...props}
|
||||
CellRendererComponent={CellRendererComponent}
|
||||
ref={flatListRef}
|
||||
onContentSizeChange={onListContentSizeChange}
|
||||
scrollEnabled={!activeKey && scrollEnabled}
|
||||
renderItem={renderItem}
|
||||
extraData={extraData}
|
||||
keyExtractor={keyExtractor}
|
||||
onScroll={scrollHandler}
|
||||
scrollEventThrottle={16}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
function DraggableFlatList<T>(props: DraggableFlatListProps<T>, ref: React.ForwardedRef<FlatList<T>>) {
|
||||
return (
|
||||
<PropsProvider {...props}>
|
||||
<AnimatedValueProvider>
|
||||
<RefProvider flatListRef={ref}>
|
||||
<DraggableFlatListInner {...props} />
|
||||
</RefProvider>
|
||||
</AnimatedValueProvider>
|
||||
</PropsProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// Generic forwarded ref type assertion taken from:
|
||||
// https://fettblog.eu/typescript-react-generic-forward-refs/#option-1%3A-type-assertion
|
||||
export default React.forwardRef(DraggableFlatList) as <T>(
|
||||
props: DraggableFlatListProps<T> & { ref?: React.ForwardedRef<FlatList<T>> },
|
||||
) => ReturnType<typeof DraggableFlatList>;
|
@ -0,0 +1,74 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import Animated, { call, useCode, onChange, greaterThan, cond, sub, block } 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, placeholderOffset, spacerIndexAnim, scrollOffset } = useAnimatedValues();
|
||||
const [placeholderSize, setPlaceholderSize] = useState(0);
|
||||
|
||||
const { keyToIndexRef, propsRef } = useRefs<T>();
|
||||
|
||||
const { activeKey } = useDraggableFlatListContext();
|
||||
const { horizontal } = useProps();
|
||||
|
||||
const onPlaceholderIndexChange = useCallback(
|
||||
(index: number) => {
|
||||
propsRef.current.onPlaceholderIndexChange?.(index);
|
||||
},
|
||||
[propsRef],
|
||||
);
|
||||
|
||||
useCode(
|
||||
() =>
|
||||
block([
|
||||
onChange(
|
||||
activeCellSize,
|
||||
call([activeCellSize], ([size]) => {
|
||||
// Using animated values to set height caused a bug where item wouldn't correctly update
|
||||
// so instead we mirror the animated value in component state.
|
||||
setPlaceholderSize(size);
|
||||
}),
|
||||
),
|
||||
onChange(
|
||||
spacerIndexAnim,
|
||||
call([spacerIndexAnim], ([i]) => {
|
||||
onPlaceholderIndexChange(i);
|
||||
if (i === -1) setPlaceholderSize(0);
|
||||
}),
|
||||
),
|
||||
]),
|
||||
[],
|
||||
);
|
||||
|
||||
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]: placeholderSize,
|
||||
transform: [{ [translateKey]: sub(placeholderOffset, scrollOffset) }] 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.View>
|
||||
);
|
||||
}
|
||||
|
||||
export default typedMemo(PlaceholderItem);
|
@ -0,0 +1,59 @@
|
||||
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);
|
@ -0,0 +1,14 @@
|
||||
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);
|
35
components/react-native-draggable-flatlist/constants.ts
Normal file
35
components/react-native-draggable-flatlist/constants.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { Platform } from 'react-native';
|
||||
import { PanGestureHandlerProperties } from 'react-native-gesture-handler';
|
||||
import Animated, { useSharedValue, WithSpringConfig } 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: 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;
|
||||
|
||||
if (!isReanimatedV2) {
|
||||
console.warn('Your version of react-native-reanimated is too old for react-native-draggable-flatlist. It may not work as expected.');
|
||||
}
|
@ -0,0 +1,120 @@
|
||||
import React, { useContext, useMemo } from 'react';
|
||||
import Animated, { 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 { 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 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);
|
||||
|
||||
// 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)]));
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
activationDistance,
|
||||
activeCellOffset,
|
||||
activeCellSize,
|
||||
activeIndexAnim,
|
||||
containerSize,
|
||||
disabled,
|
||||
horizontalAnim,
|
||||
hoverAnim,
|
||||
hoverAnimConstrained,
|
||||
hoverAnimUnconstrained,
|
||||
hoverOffset,
|
||||
isDraggingCell,
|
||||
isTouchActiveNative,
|
||||
panGestureState,
|
||||
placeholderOffset,
|
||||
resetTouchedCell,
|
||||
scrollOffset,
|
||||
scrollViewSize,
|
||||
spacerIndexAnim,
|
||||
touchAbsolute,
|
||||
touchCellOffset,
|
||||
touchInit,
|
||||
}),
|
||||
[
|
||||
activationDistance,
|
||||
activeCellOffset,
|
||||
activeCellSize,
|
||||
activeIndexAnim,
|
||||
containerSize,
|
||||
disabled,
|
||||
horizontalAnim,
|
||||
hoverAnim,
|
||||
hoverAnimConstrained,
|
||||
hoverAnimUnconstrained,
|
||||
hoverOffset,
|
||||
isDraggingCell,
|
||||
isTouchActiveNative,
|
||||
panGestureState,
|
||||
placeholderOffset,
|
||||
resetTouchedCell,
|
||||
scrollOffset,
|
||||
scrollViewSize,
|
||||
spacerIndexAnim,
|
||||
touchAbsolute,
|
||||
touchCellOffset,
|
||||
touchInit,
|
||||
],
|
||||
);
|
||||
|
||||
return value;
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
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;
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
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>;
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
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;
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
import React, { useContext, 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>> | React.ForwardedRef<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,
|
||||
flatListRef,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
flatListRef: React.ForwardedRef<FlatList<T>>;
|
||||
}) {
|
||||
const value = useSetupRefs<T>({ flatListRef });
|
||||
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>({ flatListRef: flatListRefProp }: { flatListRef: React.ForwardedRef<FlatList<T>> }) {
|
||||
const props = useProps<T>();
|
||||
const { 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 flatListRefInner = useRef<FlatList<T>>(null);
|
||||
const flatListRef = flatListRefProp || flatListRefInner;
|
||||
const panGestureHandlerRef = useRef<PanGestureHandler>(null);
|
||||
const scrollOffsetRef = useRef(0);
|
||||
const isTouchActiveRef = useRef({
|
||||
native: isTouchActiveNative,
|
||||
js: false,
|
||||
});
|
||||
|
||||
const refs = useMemo(
|
||||
() => ({
|
||||
animationConfigRef,
|
||||
cellDataRef,
|
||||
containerRef,
|
||||
flatListRef,
|
||||
isTouchActiveRef,
|
||||
keyToIndexRef,
|
||||
panGestureHandlerRef,
|
||||
propsRef,
|
||||
scrollOffsetRef,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
return refs;
|
||||
}
|
@ -0,0 +1,181 @@
|
||||
import { useRef } from 'react';
|
||||
import Animated, {
|
||||
abs,
|
||||
add,
|
||||
and,
|
||||
block,
|
||||
call,
|
||||
cond,
|
||||
eq,
|
||||
greaterOrEq,
|
||||
lessOrEq,
|
||||
max,
|
||||
not,
|
||||
onChange,
|
||||
or,
|
||||
set,
|
||||
sub,
|
||||
useCode,
|
||||
useValue,
|
||||
} from 'react-native-reanimated';
|
||||
import { FlatList, State as GestureState } from 'react-native-gesture-handler';
|
||||
import { DEFAULT_PROPS, SCROLL_POSITION_TOLERANCE } 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<T>() {
|
||||
const { flatListRef } = useRefs<T>();
|
||||
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;
|
||||
|
||||
function getFlatListNode(): FlatList<T> | null {
|
||||
if (!flatListRef || !('current' in flatListRef) || !flatListRef.current) return null;
|
||||
if ('scrollToOffset' in flatListRef.current) return flatListRef.current as FlatList<T>;
|
||||
if ('getNode' in flatListRef.current) {
|
||||
// @ts-ignore backwards compat
|
||||
return flatListRef.current.getNode();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const flatListNode = getFlatListNode();
|
||||
|
||||
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!;
|
||||
const offset = speedPct * autoscrollSpeed;
|
||||
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;
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
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 a workaround required to continually evaluate values
|
||||
useCode(() => block([onChange(cellTranslate, []), onChange(prevTrans, []), onChange(cellSize, []), onChange(cellOffset, [])]), []);
|
||||
|
||||
return state.position;
|
||||
}
|
10
components/react-native-draggable-flatlist/hooks/useNode.tsx
Normal file
10
components/react-native-draggable-flatlist/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;
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
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,
|
||||
};
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
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],
|
||||
);
|
||||
}
|
4
components/react-native-draggable-flatlist/index.tsx
Normal file
4
components/react-native-draggable-flatlist/index.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
import DraggableFlatList from './components/DraggableFlatList';
|
||||
export * from './components/CellDecorators';
|
||||
export * from './types';
|
||||
export default DraggableFlatList;
|
222
components/react-native-draggable-flatlist/procs.ts
Normal file
222
components/react-native-draggable-flatlist/procs.ts
Normal file
@ -0,0 +1,222 @@
|
||||
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)]),
|
||||
|
||||
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),
|
||||
],
|
||||
[
|
||||
// // Reset the spacer index when drag ends
|
||||
cond(neq(spacerIndex, -1), set(spacerIndex, -1)),
|
||||
cond(neq(position, 0), set(position, 0)),
|
||||
],
|
||||
),
|
||||
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,
|
||||
);
|
||||
}
|
64
components/react-native-draggable-flatlist/types.ts
Normal file
64
components/react-native-draggable-flatlist/types.ts
Normal file
@ -0,0 +1,64 @@
|
||||
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;
|
||||
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
components/react-native-draggable-flatlist/utils.ts
Normal file
5
components/react-native-draggable-flatlist/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;
|
@ -294,7 +294,7 @@ PODS:
|
||||
- React-Core
|
||||
- react-native-tor (0.1.7):
|
||||
- React
|
||||
- react-native-webview (11.17.0):
|
||||
- react-native-webview (11.17.1):
|
||||
- React-Core
|
||||
- react-native-widget-center (0.0.7):
|
||||
- React
|
||||
@ -749,7 +749,7 @@ SPEC CHECKSUMS:
|
||||
react-native-safe-area-context: 584dc04881deb49474363f3be89e4ca0e854c057
|
||||
react-native-tcp-socket: 199889cd48b07a0238e0feb53549607de5207116
|
||||
react-native-tor: 6a64351e96ccd6a49941989703f63ec81fc1ae7c
|
||||
react-native-webview: 10996be8bcc5e37a30dbc139fc544bb651dc8612
|
||||
react-native-webview: 162b6453d074e0b1c7025242bb7a939b6f72b9e7
|
||||
react-native-widget-center: 5e63193fce272aa3c2aa4f1a33e129b06a962f47
|
||||
React-perflogger: 25373e382fed75ce768a443822f07098a15ab737
|
||||
React-RCTActionSheet: af7796ba49ffe4ca92e7277a5d992d37203f7da5
|
||||
|
28870
package-lock.json
generated
28870
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -149,7 +149,6 @@
|
||||
"react-native-default-preference": "1.4.3",
|
||||
"react-native-device-info": "8.4.8",
|
||||
"react-native-document-picker": "https://github.com/BlueWallet/react-native-document-picker#c52e7a6d2a08f5506c23de86c1401775419f772c",
|
||||
"react-native-draggable-flatlist": "3.0.5",
|
||||
"react-native-elements": "3.4.2",
|
||||
"react-native-fingerprint-scanner": "https://github.com/BlueWallet/react-native-fingerprint-scanner#ce644673681716335d786727bab998f7e632ab5e",
|
||||
"react-native-fs": "2.18.0",
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useRef, useContext, useState } from 'react';
|
||||
import { View, Image, Text, StyleSheet, StatusBar, I18nManager, Pressable } from 'react-native';
|
||||
import { BluePrivateBalance } from '../../BlueComponents';
|
||||
import DraggableFlatList, { ScaleDecorator } from 'react-native-draggable-flatlist';
|
||||
import DraggableFlatList, { ScaleDecorator } from '../../components/react-native-draggable-flatlist';
|
||||
import LinearGradient from 'react-native-linear-gradient';
|
||||
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
|
||||
import { useTheme } from '@react-navigation/native';
|
||||
@ -10,6 +10,7 @@ import { LightningCustodianWallet, LightningLdkWallet, MultisigHDWallet } from '
|
||||
import WalletGradient from '../../class/wallet-gradient';
|
||||
import loc, { formatBalance, transactionTimeToReadable } from '../../loc';
|
||||
import { BlueStorageContext } from '../../blue_modules/storage-context';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
loading: {
|
||||
@ -99,7 +100,7 @@ const ReorderWallets = () => {
|
||||
<ScaleDecorator>
|
||||
<Pressable
|
||||
disabled={isActive}
|
||||
onLongPress={drag}
|
||||
onPressIn={drag}
|
||||
shadowOpacity={40 / 100}
|
||||
shadowOffset={{ width: 0, height: 0 }}
|
||||
shadowRadius={5}
|
||||
@ -172,8 +173,9 @@ const ReorderWallets = () => {
|
||||
</View>
|
||||
);
|
||||
return (
|
||||
<View style={[styles.root, stylesHook.root]}>
|
||||
<GestureHandlerRootView style={[styles.root, stylesHook.root]}>
|
||||
<StatusBar barStyle="default" />
|
||||
|
||||
<DraggableFlatList
|
||||
ListHeaderComponent={ListHeaderComponent}
|
||||
ref={sortableList}
|
||||
@ -186,7 +188,7 @@ const ReorderWallets = () => {
|
||||
onDragEnd={onDragEnd}
|
||||
containerStyle={styles.root}
|
||||
/>
|
||||
</View>
|
||||
</GestureHandlerRootView>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -13,5 +13,5 @@
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"exclude": ["node_modules", "babel.config.js", "metro.config.js", "jest.config.js"]
|
||||
"exclude": ["node_modules", "babel.config.js", "metro.config.js", "jest.config.js", "components/react-native-draggable-flatlist"],
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user