-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(DragAndDrop): added drag and drop
- Loading branch information
Showing
17 changed files
with
544 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -44,4 +44,4 @@ | |
"react-native-reanimated": ">=3.6.1", | ||
"react-native-gesture-handler": ">=2.14.1" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
## @lad-tech/mobydick-drag-and-drop |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './src'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './DragAndDrop'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
Oops, something went wrong.