├── .gitignore ├── example.gif ├── example2.gif ├── .babelrc ├── .npmignore ├── src ├── index.ts ├── utils.ts ├── block.tsx └── draggable-grid.tsx ├── .prettierrc.js ├── tsconfig.json ├── test └── utils.test.ts ├── package.json ├── README_CN.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | built 4 | demo 5 | .watchmanconfig 6 | yarn-error.log -------------------------------------------------------------------------------- /example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/th3rdwave/react-native-draggable-grid/HEAD/example.gif -------------------------------------------------------------------------------- /example2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/th3rdwave/react-native-draggable-grid/HEAD/example2.gif -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react-native" 4 | ], 5 | "retainLines": true, 6 | "sourceMaps": true 7 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | built 4 | demo 5 | .watchmanconfig 6 | example.gif 7 | yarn-error.log 8 | example2.gif 9 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { IDraggableGridProps, DraggableGrid } from './draggable-grid' 4 | 5 | export { DraggableGrid } 6 | export type { IDraggableGridProps } 7 | 8 | export default DraggableGrid 9 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "printWidth": 100, 3 | "semi": false, 4 | "singleQuote": true, 5 | "trailingComma": "all", 6 | "bracketSpacing": true, 7 | "jsxBracketSameLine": true, 8 | "arrowParens": "avoid", 9 | "insertPragma": true, 10 | "tabWidth": 2, 11 | "useTabs": false 12 | }; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "strict": true, 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "jsx": "react-native", 8 | "lib": ["dom", "es5", "es6", "scripthost"], 9 | "target": "es5", 10 | "outDir": "built", 11 | "sourceMap": true, 12 | "skipLibCheck": true 13 | }, 14 | "exclude": ["node_modules", "demo"] 15 | } 16 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | function findKey(map: { [key: string]: T }, fn: (item: T) => boolean) { 4 | const keys = Object.keys(map) 5 | for (let i = 0; i < keys.length; i++) { 6 | if (fn(map[keys[i]])) { 7 | return keys[i] 8 | } 9 | } 10 | return undefined 11 | } 12 | 13 | function findIndex(arr: T[], fn: (item: T) => boolean) { 14 | for (let i = 0; i < arr.length; i++) { 15 | if (fn(arr[i])) { 16 | return i 17 | } 18 | } 19 | return -1 20 | } 21 | 22 | function differenceBy(arr1: any[], arr2: any[], key: string) { 23 | const result: any[] = [] 24 | arr1.forEach(item1 => { 25 | const keyValue = item1[key] 26 | if (!arr2.some(item2 => item2[key] === keyValue)) { 27 | result.push(item1) 28 | } 29 | }) 30 | return result 31 | } 32 | 33 | export { findKey, findIndex, differenceBy } 34 | -------------------------------------------------------------------------------- /test/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { findIndex, findKey, differenceBy } from '../src/utils' 2 | 3 | describe('utils 方法测试', () => { 4 | const arr = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }] 5 | it('findIndex 方法', () => { 6 | expect(findIndex(arr, item => item.id === 1)).toBe(0) 7 | expect(findIndex(arr, item => item.id === 5)).toBe(-1) 8 | }) 9 | it('findKey 方法', () => { 10 | const map = { 11 | 1: { val: 1 }, 12 | 2: { val: 2 }, 13 | 3: { val: 3 }, 14 | } 15 | expect(findKey(map, item => item.val === 1)).toBe('1') 16 | expect(findKey(map, item => item.val === 5)).toBe(undefined) 17 | }) 18 | it('differenceBy 方法', () => { 19 | const arr2 = [{ id: 1 }, { id: 3 }, { id: 5 }] 20 | expect(differenceBy(arr, arr2, 'id')).toMatchObject([{ id: 2 }, { id: 4 }]) 21 | expect(differenceBy(arr2, arr, 'id')).toMatchObject([{ id: 5 }]) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /src/block.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import * as React from 'react' 4 | import { Animated, StyleProp, StyleSheet } from 'react-native' 5 | import { FunctionComponent } from 'react' 6 | import { TouchableWithoutFeedback } from 'react-native-gesture-handler' 7 | 8 | interface BlockProps { 9 | style?: StyleProp 10 | dragStartAnimationStyle: StyleProp 11 | onPress?: () => void 12 | onLongPress: () => void 13 | onPressOut: () => void 14 | children?: React.ReactNode 15 | } 16 | 17 | export const Block: FunctionComponent = ({ 18 | style, 19 | dragStartAnimationStyle, 20 | onPress, 21 | onLongPress, 22 | onPressOut, 23 | children, 24 | }) => { 25 | return ( 26 | 27 | 28 | {children} 29 | 30 | 31 | ) 32 | } 33 | 34 | const styles = StyleSheet.create({ 35 | blockContainer: { 36 | alignItems: 'center', 37 | }, 38 | }) 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@th3rdwave/react-native-draggable-grid", 3 | "keywords": [ 4 | "drag", 5 | "sortable", 6 | "grid" 7 | ], 8 | "version": "3.0.5", 9 | "description": "A draggable grid for react native", 10 | "main": "src/index.ts", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/SHISME/react-native-draggable-grid" 14 | }, 15 | "scripts": { 16 | "built": "rm -rf built && tsc", 17 | "watch": "tsc -w --outDir demo/built", 18 | "test": "jest" 19 | }, 20 | "jest": { 21 | "verbose": true, 22 | "moduleFileExtensions": [ 23 | "ts", 24 | "tsx", 25 | "js" 26 | ], 27 | "testPathIgnorePatterns": [ 28 | "/node_modules/" 29 | ], 30 | "transform": { 31 | "^.+\\.(js)$": "/node_modules/babel-jest", 32 | "\\.(ts|tsx)$": "ts-jest" 33 | }, 34 | "testMatch": [ 35 | "/test/**/?(*.)(spec|test).ts?(x)" 36 | ] 37 | }, 38 | "pre-commit": [ 39 | "test" 40 | ], 41 | "author": "shisme", 42 | "license": "ISC", 43 | "devDependencies": { 44 | "@types/jest": "^25.1.5", 45 | "@types/react": "^16.9.32", 46 | "@types/react-native": "*", 47 | "babel-jest": "^25.2.6", 48 | "jest": "^25.2.6", 49 | "jest-react-native": "^18.0.0", 50 | "pre-commit": "^1.2.2", 51 | "react": "^16.13.1", 52 | "react-native": "^0.62.0", 53 | "react-native-gesture-handler": "^1.6.1", 54 | "ts-jest": "^25.3.0", 55 | "prettier": "^2.0.2", 56 | "typescript": "^3.8.3" 57 | }, 58 | "peerDependencies": { 59 | "react": "*", 60 | "react-native": "*", 61 | "react-native-gesture-handler": "*" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | # react-native-draggable-grid 2 | 3 | ## 例子 4 | [example](https://github.com/SHISME/react-native-draggable-grid-instance) 5 | 6 |

7 | Issue Stats 8 |

9 | 10 | 11 | ## 教程 12 | 13 | ## 安装 14 | 15 | ```bash 16 | npm install react-native-draggable-grid --save 17 | ``` 18 | 19 | ## 用法 20 | 21 | ```javascript 22 | 23 | import React from 'react'; 24 | import { 25 | View, 26 | StyleSheet, 27 | Text, 28 | } from 'react-native'; 29 | import { DraggableGrid } from 'react-native-draggable-grid'; 30 | 31 | interface MyTestProps { 32 | 33 | } 34 | 35 | interface MyTestState { 36 | data:{key:string, name:string}[]; 37 | } 38 | 39 | export class MyTest extends React.Component{ 40 | 41 | constructor(props:MyTestProps) { 42 | super(props); 43 | this.state = { 44 | data:[ 45 | {name:'1',key:'one'}, 46 | {name:'2',key:'two'}, 47 | {name:'3',key:'three'}, 48 | {name:'4',key:'four'}, 49 | {name:'5',key:'five'}, 50 | {name:'6',key:'six'}, 51 | {name:'7',key:'seven'}, 52 | {name:'8',key:'eight'}, 53 | {name:'9',key:'night'}, 54 | {name:'0',key:'zero'}, 55 | ], 56 | }; 57 | } 58 | 59 | public render_item(item:{name:string, key:string}) { 60 | return ( 61 | 65 | {item.name} 66 | 67 | ); 68 | } 69 | 70 | render() { 71 | return ( 72 | 73 | { 78 | this.setState({data});// 因为使用了 hooks 的缘故,每次释放时会重新render,render会触发检查props和draggble grid缓存的排序的差异,并且使用props的顺序,所以需要每次释放的时候重新设置props的顺序 79 | }} 80 | /> 81 | 82 | ); 83 | } 84 | } 85 | 86 | const styles = StyleSheet.create({ 87 | button:{ 88 | width:150, 89 | height:100, 90 | backgroundColor:'blue', 91 | }, 92 | wrapper:{ 93 | paddingTop:100, 94 | width:'100%', 95 | height:'100%', 96 | justifyContent:'center', 97 | }, 98 | item:{ 99 | width:100, 100 | height:100, 101 | borderRadius:8, 102 | backgroundColor:'red', 103 | justifyContent:'center', 104 | alignItems:'center', 105 | }, 106 | item_text:{ 107 | fontSize:40, 108 | color:'#FFFFFF', 109 | }, 110 | }); 111 | 112 | 113 | ``` 114 | 115 | ## Props 116 | 117 | | 参数名 | 参数类型 | 是否必要 | 描述 | 118 | | :-------- | :---- | :------- | :---------- | 119 | | numColumns | number | yes | 一行要渲染多少个选项| 120 | | data | array | yes | 数据必须有唯一的id, 子组件的渲染依赖于这个id| 121 | | renderItem |(item, order:number) => ReactElement| yes | 渲染子组件| 122 | | itemHeight | number | no | 设置子组件高度,如果没有设置,会设置成和动态计算出的 itemWidth 一样大 | 123 | | dragStartAnimation | object | no | 自定义拖动时启动的动画 | 124 | | style | object | no | 容器的样式 | 125 | 126 | ## Event Props 127 | 128 | 129 | | 参数名 | 类型 | 是否必要 | 描述 | 130 | | :-------- | :---- | :------- | :---------- | 131 | | onItemPress | (item) => void | no | 子组件点击时的回调 | 132 | | onDragStart | (startDragItem) => void | no | 开始拖动是的回调 | 133 | | onDragRelease | (data) => void | no | 拖动释放时的回调,会返回排序之后的数据 | 134 | | onResetSort | (data) => void | no | 拖动时重新排序的回调,会返回排序后的数据 | 135 | 136 | ## Item Props 137 | 138 | | parameter | type | required | description | 139 | | :-------- | :---- | :------- | :---------- | 140 | | disabledDrag | boolean | no | 禁止选项拖动 | 141 | | disabledReSorted | boolean | no | 禁止选项被重新排序 | 142 | 143 | 设置 `disabledResorted` 为 `true`, 那么那个选项拖动时排序的时候就会被忽略 144 | 145 |

146 | Issue Stats 147 |

148 | 149 | 150 | ## 自定义拖动开始时的动画 151 | 152 | 如果你想自定义拖动开始时的动画,你可以这样使用 153 | 154 | ```javascript 155 | 156 | render() { 157 | return ( 158 | 159 | 170 | 171 | ); 172 | } 173 | 174 | private onDragStart = () => { 175 | this.state.animatedValue.setValue(1); 176 | Animated.timing(this.state.animatedValue, { 177 | toValue:3, 178 | duration:400, 179 | }).start(); 180 | } 181 | 182 | ``` 183 | 184 | ## 通过 props 重新排序 185 | 186 | 如果你想删除,或者添加,或者重新排序数据,你可以通过修改data来达到目的,组件会根据新的 props.data 来计算出如何排序,删除,添加子组件 187 | 188 | > 需要注意的是,data 中的数据必须要有key,而且必须唯一 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-draggable-grid 2 | 3 | [![996.icu](https://img.shields.io/badge/link-996.icu-red.svg)](https://996.icu) [![LICENSE](https://img.shields.io/badge/license-NPL%20(The%20996%20Prohibited%20License)-blue.svg)](https://github.com/996icu/996.ICU/blob/master/LICENSE) 4 | 5 | 6 | [中文文档](./README_CN.md) 7 | 8 | ## Demo 9 | 10 |

11 | Issue Stats 12 |

13 | 14 | 15 | ## Getting Started 16 | 17 | ## Installation 18 | 19 | ```bash 20 | npm install react-native-draggable-grid --save 21 | ``` 22 | 23 | ## Usage 24 | 25 | ```javascript 26 | 27 | import React from 'react'; 28 | import { 29 | View, 30 | StyleSheet, 31 | Text, 32 | } from 'react-native'; 33 | import { DraggableGrid } from 'react-native-draggable-grid'; 34 | 35 | interface MyTestProps { 36 | 37 | } 38 | 39 | interface MyTestState { 40 | data:{key:string, name:string}[]; 41 | } 42 | 43 | export class MyTest extends React.Component{ 44 | 45 | constructor(props:MyTestProps) { 46 | super(props); 47 | this.state = { 48 | data:[ 49 | {name:'1',key:'one'}, 50 | {name:'2',key:'two'}, 51 | {name:'3',key:'three'}, 52 | {name:'4',key:'four'}, 53 | {name:'5',key:'five'}, 54 | {name:'6',key:'six'}, 55 | {name:'7',key:'seven'}, 56 | {name:'8',key:'eight'}, 57 | {name:'9',key:'night'}, 58 | {name:'0',key:'zero'}, 59 | ], 60 | }; 61 | } 62 | 63 | public render_item(item:{name:string, key:string}) { 64 | return ( 65 | 69 | {item.name} 70 | 71 | ); 72 | } 73 | 74 | render() { 75 | return ( 76 | 77 | { 82 | this.setState({data});// need reset the props data sort after drag release 83 | }} 84 | /> 85 | 86 | ); 87 | } 88 | } 89 | 90 | const styles = StyleSheet.create({ 91 | button:{ 92 | width:150, 93 | height:100, 94 | backgroundColor:'blue', 95 | }, 96 | wrapper:{ 97 | paddingTop:100, 98 | width:'100%', 99 | height:'100%', 100 | justifyContent:'center', 101 | }, 102 | item:{ 103 | width:100, 104 | height:100, 105 | borderRadius:8, 106 | backgroundColor:'red', 107 | justifyContent:'center', 108 | alignItems:'center', 109 | }, 110 | item_text:{ 111 | fontSize:40, 112 | color:'#FFFFFF', 113 | }, 114 | }); 115 | 116 | 117 | ``` 118 | 119 | ## Props 120 | 121 | | parameter | type | required | description | 122 | | :-------- | :---- | :------- | :---------- | 123 | | numColumns | number | yes | how many items should be render on one row| 124 | | data | array | yes | data's item must have unique key,item's render will depend on the key| 125 | | renderItem |(item, order:number) => ReactElement| yes | Takes an item from data and renders it into the list | 126 | | itemHeight | number | no | if not set this, it will the same as itemWidth | 127 | | dragStartAnimation | object | no | custom drag start animation | 128 | | style | object | no | grid styles | 129 | 130 | ## Event Props 131 | 132 | 133 | | parameter | type | required | description | 134 | | :-------- | :---- | :------- | :---------- | 135 | | onItemPress | (item) => void | no | Function will execute when item on press | 136 | | onDragStart | (startDragItem) => void | no | Function will execute when item start drag | 137 | | onDragRelease | (data) => void | no | Function will execute when item release, and will return the new ordered data | 138 | | onResetSort | (data) => void | no | Function will execute when dragged item change sort | 139 | 140 | ## Item Props 141 | 142 | | parameter | type | required | description | 143 | | :-------- | :---- | :------- | :---------- | 144 | | disabledDrag | boolean | no | It will disable drag for the item | 145 | | disabledReSorted | boolean | no | It will disable resort the item | 146 | 147 | if you set disabledResorted be true, it will look like that 148 | 149 |

150 | Issue Stats 151 |

152 | 153 | 154 | ## Custom Drag Start Animation 155 | 156 | If you want to use your custom animation, you can do like this 157 | 158 | ```javascript 159 | 160 | render() { 161 | return ( 162 | 163 | 174 | 175 | ); 176 | } 177 | 178 | private onDragStart = () => { 179 | this.state.animatedValue.setValue(1); 180 | Animated.timing(this.state.animatedValue, { 181 | toValue:3, 182 | duration:400, 183 | }).start(); 184 | } 185 | 186 | ``` 187 | 188 | ## Resort item 189 | 190 | if you want resort item yourself,you only need change the data's sort, and the draggable-grid will auto resort by your data. 191 | 192 | > the data's key must unique 193 | -------------------------------------------------------------------------------- /src/draggable-grid.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import * as React from 'react' 4 | import { useState, useEffect } from 'react' 5 | import { 6 | PanGestureHandler, 7 | PanGestureHandlerStateChangeEvent, 8 | State as GestureState, 9 | } from 'react-native-gesture-handler' 10 | import { Animated, StyleSheet, StyleProp, ViewStyle } from 'react-native' 11 | import { Block } from './block' 12 | import { findKey, findIndex, differenceBy } from './utils' 13 | 14 | const USE_NATIVE_DRIVER = true 15 | 16 | export interface IOnLayoutEvent { 17 | nativeEvent: { layout: { x: number; y: number; width: number; height: number } } 18 | } 19 | 20 | interface IBaseItemType { 21 | key: string 22 | disabledDrag?: boolean 23 | disabledReSorted?: boolean 24 | } 25 | 26 | export interface IDraggableGridProps { 27 | numColumns: number 28 | data: DataType[] 29 | renderItem: (item: DataType, order: number) => React.ReactElement 30 | style?: ViewStyle 31 | itemHeight?: number 32 | dragStartAnimation?: StyleProp 33 | onItemPress?: (item: DataType) => void 34 | onDragStart?: (item: DataType) => void 35 | onDragRelease?: (newSortedData: DataType[]) => void 36 | onResetSort?: (newSortedData: DataType[]) => void 37 | } 38 | interface IPositionOffset { 39 | x: number 40 | y: number 41 | } 42 | interface IOrderMapItem { 43 | order: number 44 | } 45 | interface IItem { 46 | key: string 47 | itemData: DataType 48 | currentPosition: Animated.AnimatedValueXY 49 | gestureEvent: any 50 | } 51 | 52 | function useAnimatedValue(initialValue: number) { 53 | return React.useMemo(() => new Animated.Value(initialValue), []) 54 | } 55 | 56 | export const DraggableGrid = function ( 57 | props: IDraggableGridProps, 58 | ) { 59 | const [blockHeight, setBlockHeight] = useState(0) 60 | const [blockWidth, setBlockWidth] = useState(0) 61 | const gridHeight = useAnimatedValue(0) 62 | const [hadInitBlockSize, setHadInitBlockSize] = useState(false) 63 | const dragStartAnimatedValue = useAnimatedValue(0) 64 | const [gridLayout, setGridLayout] = useState({ 65 | x: 0, 66 | y: 0, 67 | width: 0, 68 | height: 0, 69 | }) 70 | const [activeItemIndex, setActiveItemIndex] = useState() 71 | const [longPressActive, setLongPressActive] = useState(false) 72 | 73 | const panHandlerActive = React.useRef(false) 74 | const isDragging = React.useRef(false) 75 | const activeBlockOffset = React.useRef({ x: 0, y: 0 }) 76 | const blockPositions = React.useRef([]) 77 | const orderMap = React.useRef<{ 78 | [itemKey: string]: IOrderMapItem 79 | }>({}) 80 | const itemMap = React.useRef<{ 81 | [itemKey: string]: any 82 | }>({}) 83 | const items = React.useRef[]>([]) 84 | 85 | const assessGridSize = (event: IOnLayoutEvent) => { 86 | if (!hadInitBlockSize) { 87 | let blockWidth = event.nativeEvent.layout.width / props.numColumns 88 | let blockHeight = props.itemHeight || blockWidth 89 | setBlockWidth(blockWidth) 90 | setBlockHeight(blockHeight) 91 | setGridLayout(event.nativeEvent.layout) 92 | setHadInitBlockSize(true) 93 | } 94 | } 95 | 96 | function initBlockPositions() { 97 | items.current.forEach((item, index) => { 98 | blockPositions.current[index] = getBlockPositionByOrder(index) 99 | }) 100 | } 101 | 102 | function getBlockPositionByOrder(order: number) { 103 | if (blockPositions.current[order]) { 104 | return blockPositions.current[order] 105 | } 106 | const columnOnRow = order % props.numColumns 107 | const y = blockHeight * Math.floor(order / props.numColumns) 108 | const x = columnOnRow * blockWidth 109 | return { 110 | x, 111 | y, 112 | } 113 | } 114 | 115 | function resetGridHeight() { 116 | const rowCount = Math.ceil(props.data.length / props.numColumns) 117 | gridHeight.setValue(rowCount * blockHeight) 118 | } 119 | 120 | function onBlockPress(itemIndex: number) { 121 | props.onItemPress && props.onItemPress(items.current[itemIndex].itemData) 122 | } 123 | 124 | function onStartDrag(gestureState: PanGestureHandlerStateChangeEvent['nativeEvent']) { 125 | const activeItem = getActiveItem() 126 | if (!activeItem) return 127 | props.onDragStart && props.onDragStart(activeItem.itemData) 128 | isDragging.current = true 129 | const { translationX, translationY } = gestureState 130 | const activeOrigin = blockPositions.current[orderMap.current[activeItem.key].order] 131 | const x = activeOrigin.x 132 | const y = activeOrigin.y 133 | activeItem.currentPosition.setOffset({ 134 | x, 135 | y, 136 | }) 137 | activeBlockOffset.current = { 138 | x: translationX, 139 | y: translationY, 140 | } 141 | } 142 | 143 | function onHandMove(event: { x: number; y: number }) { 144 | const activeItem = getActiveItem() 145 | if (!activeItem || !isDragging.current) return 146 | const { x: moveX, y: moveY } = event 147 | const xChokeAmount = Math.max( 148 | 0, 149 | activeBlockOffset.current.x + moveX - (gridLayout.width - blockWidth), 150 | ) 151 | const xMinChokeAmount = Math.min(0, activeBlockOffset.current.x + moveX) 152 | 153 | const dragPosition = { 154 | x: moveX + xChokeAmount + xMinChokeAmount, 155 | y: moveY, 156 | } 157 | const originPosition = blockPositions.current[orderMap.current[activeItem.key].order] 158 | const dragPositionToActivePositionDistance = getDistance(dragPosition, originPosition) 159 | 160 | let closetItemIndex = activeItemIndex as number 161 | let closetDistance = dragPositionToActivePositionDistance 162 | 163 | items.current.forEach((item, index) => { 164 | if (item.itemData.disabledReSorted) return 165 | if (index != activeItemIndex) { 166 | const dragPositionToItemPositionDistance = getDistance( 167 | dragPosition, 168 | blockPositions.current[orderMap.current[item.key].order], 169 | ) 170 | if ( 171 | dragPositionToItemPositionDistance < closetDistance && 172 | dragPositionToItemPositionDistance < blockWidth 173 | ) { 174 | closetItemIndex = index 175 | closetDistance = dragPositionToItemPositionDistance 176 | } 177 | } 178 | }) 179 | if (activeItemIndex != closetItemIndex) { 180 | const closetOrder = orderMap.current[items.current[closetItemIndex].key].order 181 | resetBlockPositionByOrder(orderMap.current[activeItem.key].order, closetOrder) 182 | orderMap.current[activeItem.key].order = closetOrder 183 | props.onResetSort && props.onResetSort(getSortData()) 184 | } 185 | } 186 | 187 | function onHandRelease() { 188 | const activeItem = getActiveItem() 189 | if (!activeItem) return 190 | props.onDragRelease && props.onDragRelease(getSortData()) 191 | setLongPressActive(false) 192 | activeItem.currentPosition.flattenOffset() 193 | moveBlockToBlockOrderPosition(activeItem.key) 194 | isDragging.current = false 195 | Animated.timing(dragStartAnimatedValue, { 196 | toValue: 0, 197 | duration: 200, 198 | useNativeDriver: USE_NATIVE_DRIVER, 199 | }).start(({ finished }) => { 200 | if (finished) { 201 | setActiveItemIndex(undefined) 202 | } 203 | }) 204 | } 205 | 206 | function onHandlerStateChange(event: PanGestureHandlerStateChangeEvent) { 207 | if (event.nativeEvent.state === GestureState.ACTIVE) { 208 | panHandlerActive.current = true 209 | onStartDrag(event.nativeEvent) 210 | } else if (event.nativeEvent.oldState === GestureState.ACTIVE) { 211 | panHandlerActive.current = false 212 | onHandRelease() 213 | } 214 | } 215 | 216 | function resetBlockPositionByOrder(activeItemOrder: number, insertedPositionOrder: number) { 217 | let disabledReSortedItemCount = 0 218 | if (activeItemOrder > insertedPositionOrder) { 219 | for (let i = activeItemOrder - 1; i >= insertedPositionOrder; i--) { 220 | const key = getKeyByOrder(i) 221 | const item = itemMap.current[key] 222 | if (item && item.disabledReSorted) { 223 | disabledReSortedItemCount++ 224 | } else { 225 | orderMap.current[key].order += disabledReSortedItemCount + 1 226 | disabledReSortedItemCount = 0 227 | moveBlockToBlockOrderPosition(key) 228 | } 229 | } 230 | } else { 231 | for (let i = activeItemOrder + 1; i <= insertedPositionOrder; i++) { 232 | const key = getKeyByOrder(i) 233 | const item = itemMap.current[key] 234 | if (item && item.disabledReSorted) { 235 | disabledReSortedItemCount++ 236 | } else { 237 | orderMap.current[key].order -= disabledReSortedItemCount + 1 238 | disabledReSortedItemCount = 0 239 | moveBlockToBlockOrderPosition(key) 240 | } 241 | } 242 | } 243 | } 244 | 245 | function moveBlockToBlockOrderPosition(itemKey: string) { 246 | const itemIndex = findIndex(items.current, item => item.key === itemKey) 247 | const item = items.current[itemIndex] 248 | item.currentPosition.flattenOffset() 249 | const toValue = blockPositions.current[orderMap.current[itemKey].order] 250 | Animated.timing(item.currentPosition, { 251 | toValue, 252 | duration: 200, 253 | useNativeDriver: USE_NATIVE_DRIVER, 254 | }).start(({ finished }) => { 255 | if (finished) { 256 | item.currentPosition.setOffset(toValue) 257 | item.currentPosition.setValue({ x: 0, y: 0 }) 258 | } 259 | }) 260 | } 261 | 262 | function getKeyByOrder(order: number) { 263 | return findKey(orderMap.current, (item: IOrderMapItem) => item.order === order) as string 264 | } 265 | 266 | function getSortData() { 267 | const sortData: DataType[] = [] 268 | items.current.forEach(item => { 269 | sortData[orderMap.current[item.key].order] = item.itemData 270 | }) 271 | return sortData 272 | } 273 | 274 | function getDistance(startOffset: IPositionOffset, endOffset: IPositionOffset) { 275 | const xDistance = startOffset.x + activeBlockOffset.current.x - endOffset.x 276 | const yDistance = startOffset.y + activeBlockOffset.current.y - endOffset.y 277 | return Math.sqrt(Math.pow(xDistance, 2) + Math.pow(yDistance, 2)) 278 | } 279 | 280 | function setActiveBlock(itemIndex: number, item: DataType) { 281 | if (item.disabledDrag) return 282 | setLongPressActive(true) 283 | setActiveItemIndex(itemIndex) 284 | } 285 | 286 | function startDragStartAnimation() { 287 | if (!props.dragStartAnimation) { 288 | dragStartAnimatedValue.setValue(0) 289 | Animated.timing(dragStartAnimatedValue, { 290 | toValue: 1, 291 | duration: 100, 292 | useNativeDriver: USE_NATIVE_DRIVER, 293 | }).start() 294 | } 295 | } 296 | 297 | function getBlockStyle(itemIndex: number) { 298 | return [ 299 | { 300 | justifyContent: 'center' as const, 301 | alignItems: 'center' as const, 302 | }, 303 | hadInitBlockSize && { 304 | width: blockWidth, 305 | height: blockHeight, 306 | position: 'absolute' as const, 307 | top: 0, 308 | left: 0, 309 | transform: items.current[itemIndex].currentPosition.getTranslateTransform(), 310 | }, 311 | ] 312 | } 313 | 314 | function getDragStartAnimation(itemIndex: number) { 315 | if (activeItemIndex != itemIndex) { 316 | return 317 | } 318 | 319 | return props.dragStartAnimation || getDefaultDragStartAnimation() 320 | } 321 | 322 | function getActiveItem() { 323 | if (activeItemIndex === undefined) return false 324 | return items.current[activeItemIndex] 325 | } 326 | 327 | function getDefaultDragStartAnimation() { 328 | return { 329 | transform: [ 330 | { 331 | scale: dragStartAnimatedValue.interpolate({ 332 | inputRange: [0, 1], 333 | outputRange: [1, 1.1], 334 | }), 335 | }, 336 | ], 337 | shadowColor: '#000000', 338 | shadowOpacity: dragStartAnimatedValue.interpolate({ 339 | inputRange: [0, 1], 340 | outputRange: [0, 0.2], 341 | }), 342 | shadowRadius: dragStartAnimatedValue.interpolate({ 343 | inputRange: [0, 1], 344 | outputRange: [0, 6], 345 | }), 346 | shadowOffset: { 347 | width: 1, 348 | height: 1, 349 | }, 350 | } 351 | } 352 | 353 | function addItem(item: DataType, index: number) { 354 | blockPositions.current.push(getBlockPositionByOrder(items.current.length)) 355 | orderMap.current[item.key] = { 356 | order: index, 357 | } 358 | itemMap.current[item.key] = item 359 | const currentPosition = new Animated.ValueXY({ x: 0, y: 0 }) 360 | currentPosition.setOffset(getBlockPositionByOrder(index)) 361 | items.current.push({ 362 | key: item.key, 363 | itemData: item, 364 | currentPosition, 365 | gestureEvent: Animated.event( 366 | [ 367 | { 368 | nativeEvent: { 369 | translationX: currentPosition.x, 370 | translationY: currentPosition.y, 371 | }, 372 | }, 373 | ], 374 | { useNativeDriver: USE_NATIVE_DRIVER }, 375 | ), 376 | }) 377 | } 378 | 379 | function removeItem(item: IItem) { 380 | const itemIndex = findIndex(items.current, curItem => curItem.key === item.key) 381 | items.current.splice(itemIndex, 1) 382 | blockPositions.current.pop() 383 | delete orderMap.current[item.key] 384 | } 385 | 386 | function diffData() { 387 | props.data.forEach((item, index) => { 388 | if (orderMap.current[item.key]) { 389 | if (orderMap.current[item.key].order != index) { 390 | orderMap.current[item.key].order = index 391 | moveBlockToBlockOrderPosition(item.key) 392 | } 393 | const currentItem = items.current.find(i => i.key === item.key) 394 | if (currentItem) { 395 | currentItem.itemData = item 396 | } 397 | itemMap.current[item.key] = item 398 | } else { 399 | addItem(item, index) 400 | } 401 | }) 402 | const deleteItems = differenceBy(items.current, props.data, 'key') 403 | deleteItems.forEach(item => { 404 | removeItem(item) 405 | }) 406 | } 407 | 408 | useEffect(() => { 409 | startDragStartAnimation() 410 | }, [activeItemIndex]) 411 | useEffect(() => { 412 | if (hadInitBlockSize) { 413 | initBlockPositions() 414 | } 415 | }, [gridLayout]) 416 | useEffect(() => { 417 | resetGridHeight() 418 | }) 419 | useEffect(() => { 420 | if (activeItemIndex === undefined) { 421 | return 422 | } 423 | const item = items.current[activeItemIndex] 424 | const listenerId = item.currentPosition.addListener(onHandMove) 425 | return () => { 426 | item.currentPosition.removeListener(listenerId) 427 | } 428 | }, [activeItemIndex]) 429 | if (hadInitBlockSize) { 430 | diffData() 431 | } 432 | 433 | const itemList = items.current.map((item, itemIndex) => { 434 | return ( 435 | 445 | 447 | setActiveBlock(itemIndex, item.itemData)} 450 | onPressOut={() => { 451 | setTimeout(() => { 452 | if (!panHandlerActive.current) { 453 | onHandRelease() 454 | } 455 | }, 100) 456 | }} 457 | dragStartAnimationStyle={getDragStartAnimation(itemIndex)}> 458 | {props.renderItem(item.itemData, orderMap.current[item.key].order)} 459 | 460 | 461 | 462 | ) 463 | }) 464 | 465 | return ( 466 | 475 | {hadInitBlockSize && itemList} 476 | 477 | ) 478 | } 479 | 480 | const styles = StyleSheet.create({ 481 | draggableGrid: { 482 | flex: 1, 483 | flexDirection: 'row', 484 | flexWrap: 'wrap', 485 | }, 486 | }) 487 | --------------------------------------------------------------------------------