Skip to content

Commit

Permalink
feat(DragAndDrop): added drag and drop
Browse files Browse the repository at this point in the history
  • Loading branch information
vpsmolina committed Apr 2, 2024
1 parent a26f010 commit 0b5c89c
Show file tree
Hide file tree
Showing 17 changed files with 544 additions and 2 deletions.
2 changes: 1 addition & 1 deletion packages/chart/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,4 @@
"react-native-reanimated": ">=3.6.1",
"react-native-gesture-handler": ">=2.14.1"
}
}
}
1 change: 1 addition & 0 deletions packages/dragAndDrop/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
## @lad-tech/mobydick-drag-and-drop
1 change: 1 addition & 0 deletions packages/dragAndDrop/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './src';
45 changes: 45 additions & 0 deletions packages/dragAndDrop/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"name": "@lad-tech/mobydick-drag-and-drop",
"version": "7.24.0",
"description": "React Native components library focused on usability, accessibility and developer experience",
"main": "lib/index",
"module": "lib/index",
"types": "lib/index.d.ts",
"react-native": "src/index",
"source": "src/index",
"keywords": [
"mobydick",
"lad-tech",
"mobydick-chart",
"chart",
"react-native",
"reanimated"
],
"files": [
"lib",
"src",
"index.ts"
],
"repository": {
"type": "git",
"url": "https://github.com/lad-tech/mobydick.git",
"directory": "packages/dragDrop"
},
"publishConfig": {
"registry": "https://registry.npmjs.org",
"access": "public"
},
"scripts": {
"build": "tsc",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Kirill Lebedev <[email protected]>",
"license": "MIT",
"peerDependencies": {
"react-native": ">=0.66.4",
"react": ">=17.0.2",
"@lad-tech/mobydick-core": "7.24.0",
"react-native-reanimated": ">=3.6.1",
"react-native-gesture-handler": ">=2.14.1"
}
}
163 changes: 163 additions & 0 deletions packages/dragAndDrop/src/DragAndDrop/DragAndDrop.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import {FC, PropsWithChildren} from 'react';
import {
PanGestureHandler,
PanGestureHandlerGestureEvent,
} from 'react-native-gesture-handler';
import Animated, {
useAnimatedGestureHandler,
useAnimatedReaction,
useAnimatedStyle,
useSharedValue,
withTiming,
scrollTo,
} from 'react-native-reanimated';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {Dimensions, StyleSheet} from 'react-native';

import {IContextType, IDragAndDropProps} from './types';
import {animationConfig, getOrder, getPosition} from './utils';

const DragAndDrop: FC<PropsWithChildren<IDragAndDropProps>> = ({
children,
positions,
id,
scrollY,
scrollView,
columns,
itemWidth,
itemHeight,
}) => {
const position = getPosition({
index: positions.value[id]!,
col: columns,
height: itemHeight,
width: itemWidth,
});

const x = useSharedValue(position.x);
const y = useSharedValue(position.y);

const isGestureActive = useSharedValue(false);
const inset = useSafeAreaInsets();

const containerHeight =
Dimensions.get('window').height - inset.top - inset.bottom;
const contentHeight =
(Object.keys(positions.value).length / columns) * itemHeight;

useAnimatedReaction(
() => positions.value[id]!,
newOrder => {
const newPositions = getPosition({
index: newOrder,
col: columns,
height: itemHeight,
width: itemWidth,
});
x.value = withTiming(newPositions.x, animationConfig);
y.value = withTiming(newPositions.y, animationConfig);
},
);

const onGestureEvent = useAnimatedGestureHandler<
PanGestureHandlerGestureEvent,
IContextType
>({
onStart: (_, context) => {
context.x = x.value;
context.y = y.value;
isGestureActive.value = true;
},
onActive: (event, context) => {
x.value = event.translationX + context.x;
y.value = event.translationY + context.y;
// 1. We calculate where the tile should be
const newOrder = getOrder({
tx: x.value,
ty: y.value,
max: Object.keys(positions.value).length - 1,
col: columns,
width: itemWidth,
height: itemHeight,
});

// 2. We swap the positions
const oldOlder = positions.value[id];
if (newOrder !== oldOlder) {
const idToSwap = Object.keys(positions.value).find(
key => positions.value[key] === newOrder,
);
if (idToSwap) {
// Spread operator is not supported in worklets
// And Object.assign doesn't seem to be working on alpha.6
const newPositions = JSON.parse(JSON.stringify(positions.value));
newPositions[id] = newOrder;
newPositions[idToSwap] = oldOlder;
positions.value = newPositions;
}
}

// 3. Scroll
const lowerBound = scrollY.value;
const upperBound = lowerBound + containerHeight - itemWidth;
const maxScroll = contentHeight - containerHeight;
const leftToScrollDown = maxScroll - scrollY.value;

if (y.value < lowerBound) {
const diff = Math.min(lowerBound - y.value, lowerBound);
scrollY.value -= diff;
context.y -= diff;
y.value = context.y + event.translationY;
scrollTo(scrollView, 0, scrollY.value, false);
}
if (y.value > upperBound) {
const diff = Math.min(y.value - upperBound, leftToScrollDown);
scrollY.value += diff;
context.y += diff;
y.value = context.y + event.translationY;
scrollTo(scrollView, 0, scrollY.value, false);
}
},
onEnd: () => {
const destination = getPosition({
index: positions.value[id]!,
col: columns,
height: itemHeight,
width: itemWidth,
});
x.value = withTiming(
destination.x,
animationConfig,
() => (isGestureActive.value = false),
);

y.value = withTiming(destination.y, animationConfig);
},
});

const animatedStyle = useAnimatedStyle(() => {
const zIndex = isGestureActive.value ? 100 : 0;
const scale = isGestureActive.value ? 1.1 : 1;
return {
position: 'absolute',
top: 0,
left: 0,
width: itemWidth,
height: itemHeight,
zIndex,
transform: [{translateX: x.value}, {translateY: y.value}, {scale}],
};
});

return (
<Animated.View style={animatedStyle}>
<PanGestureHandler onGestureEvent={onGestureEvent}>
<Animated.View style={StyleSheet.absoluteFill}>
{children}
</Animated.View>
</PanGestureHandler>
</Animated.View>
);
};

export default DragAndDrop;
76 changes: 76 additions & 0 deletions packages/dragAndDrop/src/DragAndDrop/DragAndDropList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import {GestureHandlerRootView} from 'react-native-gesture-handler';
import Animated, {
useAnimatedRef,
useAnimatedScrollHandler,
useSharedValue,
} from 'react-native-reanimated';
import {useStyles, createStyles, View} from '@lad-tech/mobydick-core';

import {IDragAndDropListProps, IPositions} from './types';
import DragAndDrop from './DragAndDrop';

const DragAndDropList = <T,>({
list,
renderItem,
itemWidth,
itemHeight,
columns,
sideMargin,
}: IDragAndDropListProps<T>) => {
const [styles] = useStyles(createStyleFn);
const scrollY = useSharedValue(0);
const scrollView = useAnimatedRef<Animated.ScrollView>();

const positions = useSharedValue<IPositions>(
Object.assign({}, ...list.map((_item: T, index) => ({[index]: index}))),
);

const onScroll = useAnimatedScrollHandler({
onScroll: ({contentOffset: {y}}) => {
scrollY.value = y;
},
});

return (
<View style={[styles.container]}>
<GestureHandlerRootView style={{flex: 1}}>
<Animated.ScrollView
onScroll={onScroll}
ref={scrollView}
contentContainerStyle={{
height: Math.ceil(list.length / columns) * itemHeight,
alignItems: 'center',
marginHorizontal: sideMargin,
}}
showsVerticalScrollIndicator={false}
bounces={false}
scrollEventThrottle={16}>
{list.map((item, index) => {
return (
<DragAndDrop
key={index}
positions={positions}
id={index}
itemWidth={itemWidth}
itemHeight={itemHeight}
columns={columns}
sideMargin={sideMargin}
scrollView={scrollView}
scrollY={scrollY}>
{renderItem(item, index, list)}
</DragAndDrop>
);
})}
</Animated.ScrollView>
</GestureHandlerRootView>
</View>
);
};

const createStyleFn = createStyles(() => ({
container: {
flex: 1,
},
}));

export default DragAndDropList;
5 changes: 5 additions & 0 deletions packages/dragAndDrop/src/DragAndDrop/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import DragAndDrop from './DragAndDrop';
import DragDropList from './DragAndDropList';

export {DragAndDrop, DragDropList};
export * from './types';
28 changes: 28 additions & 0 deletions packages/dragAndDrop/src/DragAndDrop/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Animated, {AnimatedRef, SharedValue} from 'react-native-reanimated';

export type IContextType = {
x: number;
y: number;
};

export interface IPositions {
[id: string]: number;
}

interface IView {
sideMargin: number;
itemWidth: number;
itemHeight: number;
columns: number;
}
export interface IDragAndDropListProps<T> extends IView {
list: T[];
renderItem: (item: T, index: number, data: Array<T>) => JSX.Element;
}

export interface IDragAndDropProps extends IView {
positions: SharedValue<IPositions>;
id: number;
scrollY: Animated.SharedValue<number>;
scrollView: AnimatedRef<Animated.ScrollView>;
}
47 changes: 47 additions & 0 deletions packages/dragAndDrop/src/DragAndDrop/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {Easing} from 'react-native-reanimated';

export const getPosition = ({
index,
col,
width,
height,
}: {
index: number;
col: number;
width: number;
height: number;
}) => {
'worklet';
return {
x: (index % col) * width,
y: Math.floor(index / col) * height,
};
};
export const getOrder = ({
tx,
ty,
max,
col,
width,
height,
}: {
tx: number;
ty: number;
max: number;
col: number;
width: number;
height: number;
}) => {
'worklet';

const x = Math.round(tx / width) * width;
const y = Math.round(ty / height) * height;
const row = Math.max(y, 0) / height;
const columns = Math.max(x, 0) / width;
return Math.min(row * col + columns, max);
};

export const animationConfig = {
easing: Easing.inOut(Easing.ease),
duration: 350,
};
1 change: 1 addition & 0 deletions packages/dragAndDrop/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './DragAndDrop';
6 changes: 6 additions & 0 deletions packages/dragAndDrop/svg.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
declare module '*.svg' {
import React from 'react';
import {SvgProps} from 'react-native-svg';
const content: React.FC<SvgProps>;
export default content;
}
Loading

0 comments on commit 0b5c89c

Please sign in to comment.