GestureHandler

This commit is contained in:
Marcos Rodriguez Vélez 2022-01-26 10:54:32 -05:00
parent 3a3e66c79c
commit 1c9fa414eb
No known key found for this signature in database
GPG Key ID: 0D64671698D11C5C
28 changed files with 12432 additions and 18128 deletions

View File

@ -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'}

View File

@ -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}
/>

View File

@ -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,
},
});

View File

@ -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);

View File

@ -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>;

View File

@ -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);

View File

@ -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);

View File

@ -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);

View 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.');
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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>;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View 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;
}

View File

@ -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,
};
}

View File

@ -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],
);
}

View File

@ -0,0 +1,4 @@
import DraggableFlatList from './components/DraggableFlatList';
export * from './components/CellDecorators';
export * from './types';
export default DraggableFlatList;

View 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,
);
}

View 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;
};
};

View 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;

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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>
);
};

View File

@ -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"],
}