diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..294a516
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+*.[aod]
+*.DS_Store
+.DS_Store
+*Thumbs.db
+*.iml
+.gradle
+.idea
+node_modules
+npm-debug.log
+/android/build
+/ios/**/*xcuserdata*
+/ios/**/*xcshareddata*
\ No newline at end of file
diff --git a/.npmignore b/.npmignore
new file mode 100644
index 0000000..294a516
--- /dev/null
+++ b/.npmignore
@@ -0,0 +1,12 @@
+*.[aod]
+*.DS_Store
+.DS_Store
+*Thumbs.db
+*.iml
+.gradle
+.idea
+node_modules
+npm-debug.log
+/android/build
+/ios/**/*xcuserdata*
+/ios/**/*xcshareddata*
\ No newline at end of file
diff --git a/README.md b/README.md
index 76165c8..a9d2a67 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,314 @@
# react-native-smart-sortable-sudoku-grid
-A smart sortable sudoku grid for React Native apps
+
+[](https://www.npmjs.com/package/react-native-smart-sortable-sudoku-grid)
+[](https://www.npmjs.com/package/react-native-smart-sortable-sudoku-grid)
+[](https://www.npmjs.com/package/react-native-smart-sortable-sudoku-grid)
+[](https://github.com/react-native-component/react-native-smart-sortable-sudoku-grid/blob/master/LICENSE)
+
+A smart sortable sudoku grid for React Native apps. Written in JS for cross-platform support.
+It works on iOS and Android.
+
+## Preview
+
+![react-native-smart-sortable-sudoku-grid-preview][1]
+![react-native-smart-sortable-sudoku-grid-preview-android][2]
+
+## Installation
+
+```
+npm install react-native-smart-sortable-sudoku-grid --save
+```
+
+## Full Demo
+
+see [ReactNativeComponentDemos][0]
+
+## Usage
+
+Install the SortableSudokuGrid from npm with `npm install react-native-smart-sortable-sudoku-grid --save`.
+Then, require it from your app's JavaScript files with `import SudokuGrid from 'react-native-smart-sortable-sudoku-grid'`.
+
+```js
+import React, {
+ Component,
+} from 'react'
+import {
+ ScrollView,
+ StyleSheet,
+ View,
+ Image,
+ Text,
+ TouchableOpacity,
+ TouchableWithoutFeedback,
+ Alert,
+ Animated,
+} from 'react-native'
+
+import SortableSudokuGrid from 'react-native-smart-sortable-sudoku-grid'
+
+import image_cash from '../images/cash.png'
+import image_credit from '../images/credit.png'
+import image_transfer from '../images/transfer.png'
+import image_loan from '../images/loan.png'
+import image_charge from '../images/charge.png'
+import image_payment from '../images/payment.png'
+import image_shopping from '../images/shopping.png'
+import image_service from '../images/service.png'
+import image_donate from '../images/donate.png'
+
+import image_add from '../images/add.png'
+import image_remove from '../images/remove.png'
+import image_locked from '../images/locked.png'
+
+const dataList = [
+ {
+ icon: image_cash,
+ title: 'cash',
+ },
+ {
+ icon: image_credit,
+ title: 'credit',
+ },
+ {
+ icon: image_transfer,
+ title: 'transfer',
+ },
+ {
+ icon: image_loan,
+ title: 'loan',
+ },
+ {
+ icon: image_charge,
+ title: 'charge',
+ },
+ {
+ icon: image_payment,
+ title: 'payment',
+ },
+ {
+ icon: image_shopping,
+ title: 'shopping',
+ },
+ {
+ icon: image_service,
+ title: 'service',
+ },
+ {
+ icon: image_donate,
+ title: 'donate',
+ },
+]
+
+const columnCount = 3
+
+export default class ThreeColumns extends Component {
+
+ constructor (props) {
+ super(props)
+ this.state = {
+ dataSource: [ ...dataList ],
+ candidates: [],
+ sortable: false,
+ scrollEnabled: true,
+ disabled: false,
+ managementButtonText: 'Manage',
+ opacity: new Animated.Value(0),
+ }
+ this._sortableSudokuGrid = null
+ }
+
+ render () {
+ return (
+
+
+
+ My Applications:
+
+
+
+ {this.state.managementButtonText}
+
+
+
+
+ this._sortableSudokuGrid = component }
+ containerStyle={{ backgroundColor: '#fff',}}
+ columnCount={columnCount}
+ dataSource={this.state.dataSource}
+ renderCell={this._renderGridCell}
+ sortable={this.state.sortable}
+ />
+
+
+ Candidates:
+
+
+ this._candidatesSudokuGrid = component }
+ containerStyle={{ backgroundColor: '#fff',}}
+ columnCount={columnCount}
+ dataSource={this.state.candidates}
+ renderCell={this._renderCandidateCell}
+ sortable={false}
+ />
+
+ )
+ }
+
+ _renderGridCell = (data, component) => {
+ return (
+
+
+
+ {data.title}
+
+
+
+
+
+
+
+
+
+ )
+ }
+
+ _renderCandidateCell = (data, component) => {
+ return (
+
+
+
+ {data.title}
+
+
+
+
+
+
+
+
+
+ )
+ }
+
+ _onPressCell = (data) => {
+ Alert.alert('clicked grid cell -> ' + data.title)
+ }
+
+ _onPressCandidateCell = (data) => {
+ Alert.alert('clicked candidate cell -> ' + data.title)
+ }
+
+ _onPressManagementButton = () => {
+ let scrollEnabled = !this.state.scrollEnabled
+ let disabled = !this.state.disabled
+ let managementButtonText = this.state.managementButtonText == 'Manage' ? 'Complete' : 'Manage'
+ let sortable = !this.state.sortable
+ let opacity = sortable ? new Animated.Value(1) : new Animated.Value(0)
+ this.setState({
+ scrollEnabled,
+ managementButtonText,
+ disabled,
+ sortable,
+ opacity,
+ })
+ if (!sortable) {
+ let sortedDataSource = this._sortableSudokuGrid.getSortedDataSource()
+ //console.log(`_onPressManagementButton get sorted/added/removed DataSource`)
+ //console.log(sortedDataSource)
+ let candidateDataSource = this._candidatesSudokuGrid.getSortedDataSource()
+ //console.log(`_onPressManagementButton get sorted/added/removed candidateDataSource`)
+ //console.log(candidateDataSource)
+ }
+ }
+
+ _onRemoveCellButtonPress = (component) => {
+ let cellIndex = this._sortableSudokuGrid._cells.findIndex((cell) => {
+ return cell.component === component
+ })
+
+ this._sortableSudokuGrid.removeCell({
+ cellIndex,
+ callback: (removedDataList) => {
+ if(removedDataList.length > 0) {
+ let data = removedDataList[0]
+ this._candidatesSudokuGrid.addCell({
+ data,
+ })
+ }
+ }
+ })
+ }
+
+ _onRemoveCandidatesCellButtonPress = (component) => {
+ let cellIndex = this._candidatesSudokuGrid._cells.findIndex((cell) => {
+ return cell.component === component
+ })
+
+ this._candidatesSudokuGrid.removeCell({
+ cellIndex,
+ callback: (removedDataList) => {
+ if(removedDataList.length > 0) {
+ let data = removedDataList[0]
+ this._sortableSudokuGrid.addCell({
+ data,
+ })
+ }
+ }
+ })
+ }
+
+}
+```
+
+## Props
+
+Prop | Type | Optional | Default | Description
+--------------- | ------ | -------- | ----------- | -----------
+rowWidth | number | Yes | deviceWidth | determines the width of a row.
+rowHeight | number | Yes | deviceWidth | determines the height of a row.
+columnCount | number | No | | determines how many columns a row contains.
+dataSource | array | No | | determines the datasource of grid
+renderCell | func | No | | A function that returns the grid cell component.
+containerStyle | style | Yes | | see [react-native documents][3]
+sortable | bool | Yes | | determines if the gird cell can be sortable
+
+## Method
+
+* addCell({ data, callback, }): add a new cell with data to grid
+* removeCell({ cellIndex, callback, }): remove a cell from grid by cellIndex
+* getSortedDataSource: return a sorted(added/removed) dataSource
+
+[0]: https://github.com/cyqresig/ReactNativeComponentDemos
+[1]: http://cyqresig.github.io/img/react-native-smart-sortable-sudoku-grid-preview-ios-v1.0.0.gif
+[2]: http://cyqresig.github.io/img/react-native-smart-sortable-sudoku-grid-preview-android-v1.0.0.gif
+[3]: https://facebook.github.io/react-native/docs/style.html
+
+
+
diff --git a/SortableCell.js b/SortableCell.js
new file mode 100644
index 0000000..4bec6bc
--- /dev/null
+++ b/SortableCell.js
@@ -0,0 +1,178 @@
+import React, {
+ PropTypes,
+ Component,
+} from 'react'
+import {
+ View, Text,
+ StyleSheet,
+ PanResponder,
+ Animated,
+} from 'react-native'
+import {
+ cellScale,
+ cellAnimationTypes,
+ cellTranslation,
+} from './constants'
+
+export default class SortableCell extends Component {
+
+ static propTypes = {
+ columnCount: PropTypes.number.isRequired,
+ columnWidth: PropTypes.number.isRequired,
+ rowHeight: PropTypes.number.isRequired,
+ coordinate: PropTypes.shape({
+ x: PropTypes.number.isRequired,
+ y: PropTypes.number.isRequired,
+ }).isRequired,
+ renderCell: PropTypes.func.isRequired,
+ data: PropTypes.object.isRequired,
+ index: PropTypes.number.isRequired,
+ dataList: PropTypes.array.isRequired,
+ sortable: PropTypes.bool.isRequired,
+ }
+
+ constructor (props) {
+ super(props)
+ let {x, y,} = props.coordinate
+ //console.log(`constructor -> data.title = ${props.data.title}, index = ${props.index}, x=${props.coordinate.x}, y=${props.coordinate.y}`)
+ this.state = {
+ //x: new Animated.Value(x),
+ //y: new Animated.Value(y),
+ coordinate: new Animated.ValueXY({
+ x,
+ y,
+ }),
+ scale: new Animated.Value(1),
+ zIndex: 0,
+ visible: true,
+ }
+ this._scaleAnimationInstace = null
+ this._translationAnimationInstace = null
+ }
+
+ render () {
+ let {columnWidth, rowHeight, data, index, dataList, } = this.props
+ let {x, y,} = this.state.coordinate
+ //console.log(`cell rerender data.title = ${data.title}, index = ${index}, x = ${x._value}, y = ${y._value},`)
+ //console.log(data)
+ //console.log(dataList)
+ return (
+ this.state.visible ?
+
+ {this.props.renderCell(data, this)}
+ : null
+ )
+ }
+
+ componentWillUnmount () {
+ this._stopScaleAnmation()
+ }
+
+ setCoordinate = (coordinate) => {
+ let {x, y,} = coordinate
+ this.setState({
+ coordinate: new Animated.ValueXY({
+ x,
+ y,
+ })
+ })
+ }
+
+ setZIndex = (zIndex) => {
+ this.setState({
+ zIndex,
+ })
+ }
+
+ startScaleAnimation = ({ scaleValue, callback, }) => {
+ //if (this._scaleAnimationInstace) {
+ // return
+ //}
+ this._scaleAnimationInstace = Animated.timing(
+ this.state.scale,
+ {
+ toValue: scaleValue,
+ duration: cellScale.animationDuration,
+ }
+ ).start(() => {
+ this._scaleAnimationInstace = null
+ if(scaleValue == 0) {
+ this.setState({
+ visible: false,
+ })
+ }
+ callback && callback()
+ })
+ }
+
+ getTranslationAnimation = ({ animationType, coordinate, }) => {
+ let { columnWidth, rowHeight, columnCount, } = this.props
+ let { backToOrigin, rightTranslation, leftTranslation, leftBottomTranslation, rightTopTranslation, } = cellAnimationTypes
+ let x = this.state.coordinate.x._value
+ let y = this.state.coordinate.y._value
+ let { x: originX, y: originY, } = coordinate
+ //console.log(`startTranslationAnimation -> x=${x}, y=${y}, originX = ${originX}, originY = ${originY}`)
+ switch (animationType) {
+ case backToOrigin:
+ x = originX
+ y = originY
+ break;
+ case leftTranslation:
+ //x = x - columnWidth
+ x = x - (columnWidth - (originX - x))
+ break;
+ case rightTranslation:
+ x = x + (columnWidth - (x - originX))
+ break;
+ case leftBottomTranslation:
+ x = x - ((columnCount - 1) * columnWidth - (originX - x))
+ y = y + (rowHeight - (y - originY))
+ break;
+ case rightTopTranslation:
+ x = x + ((columnCount - 1) * columnWidth - (x - originX))
+ y = y - (rowHeight - (originY - y))
+ break;
+ }
+ //console.log(`translation x = ${x}, y = ${y}, `)
+ return Animated.timing(
+ this.state.coordinate,
+ {
+ toValue: {
+ x,
+ y,
+ },
+ duration: cellTranslation.animationDuration,
+ }
+ )
+ }
+
+ startTranslationAnimation = ({ animationType, coordinate, callback, }) => {
+ //if (this._translationAnimationInstace) {
+ this.stopTranslationAnimation()
+ //}
+ this._translationAnimationInstace = this.getTranslationAnimation({ animationType, coordinate,})
+ this._translationAnimationInstace.start(() => {
+ callback && callback()
+ this._translationAnimationInstace = null
+ })
+ }
+
+ stopTranslationAnimation = () => {
+ //if (!this._translationAnimationInstace) {
+ // return
+ //}
+ this._translationAnimationInstace && this._translationAnimationInstace.stop()
+ this._translationAnimationInstace = null
+ }
+
+ _stopScaleAnmation = () => {
+ //if (!this._scaleAnimationInstace) {
+ // return
+ //}
+ this._scaleAnimationInstace && this._scaleAnimationInstace.stop()
+ this._scaleAnimationInstace = null
+ }
+
+}
diff --git a/SortableSudokuGrid.js b/SortableSudokuGrid.js
new file mode 100644
index 0000000..91a9365
--- /dev/null
+++ b/SortableSudokuGrid.js
@@ -0,0 +1,559 @@
+/*
+ * A smart sortable sudoku grid for react-native apps
+ * https://github.com/react-native-component/react-native-smart-sortable-sudoku-grid/
+ * Released under the MIT license
+ * Copyright (c) 2016 react-native-component
+ */
+
+import React, {
+ PropTypes,
+ Component,
+} from 'react'
+import {
+ View,
+ StyleSheet,
+ PanResponder,
+ Dimensions,
+ Animated,
+} from 'react-native'
+
+import SortableCell from './SortableCell'
+import Utils from './Utils'
+import {
+ cellScale,
+ cellAnimationTypes,
+ cellChangeTypes,
+ touchStart,
+ containerLayout,
+ containerHeight,
+} from './constants'
+import TimerEnhance from '../react-native-smart-timer-enhance'
+
+const { width: deviceWidth } = Dimensions.get('window');
+const styles = StyleSheet.create({
+ container: {
+ position: 'relative',
+ flex: 1,
+ },
+})
+
+class SortableSudokuGrid extends Component {
+
+ static defaultProps = {
+ sortable: false,
+ }
+
+ static propTypes = {
+ containerStyle: PropTypes.object,
+ rowWidth: PropTypes.number,
+ rowHeight: PropTypes.number,
+ columnCount: PropTypes.number.isRequired,
+ dataSource: PropTypes.array.isRequired,
+ renderCell: PropTypes.func.isRequired,
+ sortable: PropTypes.bool,
+ }
+
+ constructor (props) {
+ super(props)
+
+ let { columnCount, dataSource, rowWidth, rowHeight, sortable, } = props
+
+ this._rowWidth = rowWidth || deviceWidth
+ this._columnWidth = this._rowWidth / columnCount
+ this._rowHeight = rowHeight || this._columnWidth
+
+ let containerHeight = dataSource.length > 0 ? (Math.floor((dataSource.length - 1) / columnCount) + 1 ) * this._rowHeight : this._rowHeight
+
+ this.state = {
+ dataSource,
+ sortable,
+ containerHeight: new Animated.Value(containerHeight),
+ }
+
+ this._pageLeft = 0
+ this._pageTop = 0
+
+ this._container = null
+ this._responderTimer = null
+ this._animationInstace = null
+ this._currentStartCell = null
+ this._currentDraggingComponent = null
+ this._isRemoving = false
+ this._isAdding = false
+
+ this._touchDown = false
+ this._touchEnding = false
+
+ this._cells = []
+ }
+
+ componentWillMount () {
+
+ this._panResponder = PanResponder.create({
+ onStartShouldSetPanResponder: (e, gestureState) => {
+ //for android, fix the weird conflict between PanRespander and ScrollView
+ return this.state.sortable
+ },
+ onMoveShouldSetPanResponder: (e, gestureState) => {
+ //for ios/android, fix the conflict between PanRespander and TouchableView
+ var x = gestureState.dx;
+ var y = gestureState.dy;
+ if (x != 0 && y != 0) {
+ return true;
+ }
+ return false;
+ },
+ onPanResponderGrant: this._onTouchStart,
+ onPanResponderMove: this._onTouchMove,
+ onPanResponderRelease: this._onTouchEnd,
+ onPanResponderTerminationRequest: () => false,
+ })
+
+ }
+
+ render () {
+ //this._cells.splice(0, this._cells.length) //clear array, not valid, why?
+ //console.log(`this._cells clear => length = ${this._cells.length}`)
+ //for (let cell of this._cells) {
+ //console.log(`for of this._cells.entries() cellKey = ${cell.key}`)
+ //}
+ //let {columnCount, dataSource, containerStyle, } = this.props
+ //let containerHeight = (Math.floor(dataSource.length - 1 / columnCount) + 1) * this._rowHeight
+ return (
+
+ this._container = component }
+ {...this._panResponder.panHandlers}
+ onLayout={this._onLayout}>
+ {this._renderSortableCells()}
+
+
+ )
+ }
+
+ componentWillReceiveProps (nextProps) {
+ let sortable = nextProps.sortable
+ if (sortable !== this.props.sortable) {
+ this.setState({
+ sortable,
+ })
+ }
+ }
+
+ componentWillUnmount () {
+ if (this._animationInstace) {
+ this._animationInstace.stop()
+ this._animationInstace = null
+ }
+ }
+
+ _renderSortableCells () {
+
+ let { columnCount, } = this.props
+ let { dataSource, } = this.state
+ //console.log(`_renderSortableCells dataSource -> dataSource.length = ${dataSource.length}`)
+ //console.log(dataSource)
+
+ //this._cells = [] //clear array
+ ////console.log(`_renderSortableCells this._cells ->`)
+ ////console.log(this._cells)
+
+ return dataSource.map((data, index, dataList) => {
+ let coordinate = {
+ x: index % columnCount * this._columnWidth,
+ y: Math.floor(index / columnCount) * this._rowHeight,
+ }
+ //console.log(`dataSource.map((data, index, dataList) => index = ${index} title = ${data.title}, x=${coordinate.x}, y=${coordinate.y}`)
+
+ return (
+ {
+ //console.log(`ref cell index = ${index}, data.title = ${data.title}, length = ${this._cells.length}`)
+ //has to remove old unecessary cell here
+ if(index == dataList.length - 1 && this._cells.length > dataList.length) {
+ this._cells.splice(index + 1, this._cells.length - dataList.length)
+ }
+ //console.log(`after ref cell index = ${index}, data.title = ${data.title}, length = ${this._cells.length}`)
+ return this._cells[index] = {key: data.title, index, coordinate, component,}
+ } }
+ {...this.props}
+ key={data.title}
+ rowHeight={this._rowHeight}
+ columnWidth={this._columnWidth}
+ coordinate={coordinate}
+ data={data}
+ index={index}
+ dataList={dataList}
+ sortable={this.state.sortable}/>
+ )
+ })
+ }
+
+ _onLayout = (e) => {
+ let { delay, } = containerLayout
+ this.setTimeout(() => {
+ this._container.measure((ox, oy, width, height, px, py) => {
+ ////console.log(`ox = ${ox}, oy = ${oy}, px = ${px}, py = ${py}, width = ${width}, height = ${height}`)
+ this._pageTop = py
+ this._pageLeft = px //sometimes return incorrect value, when using navigator
+ })
+ }, delay) //delay for waiting navigator's animation
+ }
+
+ _onTouchStart = (e, gestureState) => {
+ //console.log(`_onTouchStart... this._touchDown = ${this._touchDown}`)
+ //compare this._touchDown to fix unexcepted _onTouchStart trigger in specified cases
+ if (this._touchDown || !this.state.sortable ) {
+ return
+ }
+ //console.log(`_onTouchStart not return...`)
+ let { pageX, pageY, } = e.nativeEvent
+ if (!this._responderTimer && !this._currentStartCell && !this._currentDraggingComponent && !this._isRemoving && !this._isAdding) {
+ this._touchDown = true
+ //console.log(`_onTouchStart do main logic...`)
+ let { delay, } = touchStart
+ this._responderTimer = this.setTimeout(() => {
+ ////console.log(`pageX = ${pageX}, pageY = ${pageY}, this._pageLeft = ${this._pageLeft}, this._pageTop = ${this._pageTop},`)
+ let draggingCell = this._getTouchCell({
+ x: pageX - this._pageLeft,
+ y: pageY - this._pageTop,
+ })
+ if (draggingCell == null) {
+ return
+ }
+ this._currentStartCell = draggingCell
+ this._currentDraggingComponent = this._currentStartCell.component
+ //console.log(`this._cells => `)
+ //console.log(this._cells)
+ //console.log(`_onTouchStart this._currentStartCell.component.props.index = ${this._currentStartCell.component.props.index}`)
+ draggingCell.component.setCoordinate({
+ x: pageX - this._pageLeft - this._columnWidth / 2,
+ y: pageY - this._pageTop - this._rowHeight / 2,
+ })
+ //console.log(` draggingCell.component.setZIndex(999)`)
+ draggingCell.component.setZIndex(999)
+ draggingCell.component.startScaleAnimation({
+ scaleValue: cellScale.value,
+ })
+ }, delay)
+ }
+ }
+
+ _onTouchMove = (e, gestureState) => {
+ //console.log(`_onTouchMove this._touchDown = ${this._touchDown}`)
+ //compare this._touchDown to fix unexcepted _onTouchMove trigger in specified cases
+ if (!this._touchDown || !this.state.sortable || !this._currentStartCell || !this._currentDraggingComponent) {
+ return
+ }
+ let { pageX, pageY, } = e.nativeEvent
+
+ //console.log(`_onTouchMove do main logic...`)
+ this._currentDraggingComponent.setCoordinate({
+ x: pageX - this._pageLeft - this._columnWidth / 2,
+ y: pageY - this._pageTop - this._rowHeight / 2,
+ })
+
+ let hoverCell = this._getTouchCell({
+ x: pageX - this._pageLeft,
+ y: pageY - this._pageTop,
+ })
+
+ if (hoverCell == null || hoverCell == this._currentStartCell) {
+ return
+ }
+ let currentCellIndex = this._currentStartCell.index
+ let hoverCellIndex = hoverCell.index
+
+ let { columnCount, } = this.props
+ let cellsAnimationOptions = Utils.getCellsAnimationOptions({
+ currentCellIndex,
+ hoverCellIndex,
+ columnCount,
+ cells: this._cells,
+ })
+ this._sortCells({
+ cellsAnimationOptions,
+ })
+
+ ////console.log(`_onTouchMove before this._cells[ hoverCellIndex ].component.props.index = ${this._cells[ hoverCellIndex ].component.props.index}`)
+ this._cells[ hoverCellIndex ].component = this._currentDraggingComponent
+ ////console.log(`_onTouchMove after this._cells[ hoverCellIndex ].component.props.index = ${this._cells[ hoverCellIndex ].component.props.index}`)
+ this._currentStartCell = hoverCell
+ ////console.log(`_onTouchMove end this._currentStartCell.component.props.index = ${this._currentStartCell.component.props.index}`)
+ }
+
+ _onTouchEnd = (e, gestureState) => {
+ this._touchDown = false
+
+ this.clearTimeout(this._responderTimer)
+ this._responderTimer = null
+
+ //compare this._touchEnding to fix unexcepted _onTouchEnd trigger in specified cases
+ if (this._touchEnding || !this.state.sortable || !this._currentStartCell || !this._currentDraggingComponent) {
+ return
+ }
+
+ this._touchEnding = true
+
+ //console.log(`_onTouchEnd do main logic...`)
+
+ let animationType = cellAnimationTypes.backToOrigin
+ let cellIndex = this._currentStartCell.index
+ ////console.log(`cellIndex = ${cellIndex}`)
+ let cell = this._cells[ cellIndex ]
+ let coordinate = cell.coordinate
+ ////console.log(coordinate)
+ this._currentDraggingComponent.startScaleAnimation({
+ scaleValue: 1,
+ })
+ this._currentDraggingComponent.startTranslationAnimation({
+ animationType,
+ coordinate,
+ callback: () => {
+ if(!this._currentDraggingComponent) {
+ return
+ }
+ this._currentDraggingComponent.setZIndex(0)
+ this._sortDataSource()
+ this._currentStartCell = null
+ this._currentDraggingComponent = null
+ this._touchEnding = false
+ },
+ })
+ }
+
+ _getTouchCell = (touchCoordinate) => {
+ let rowHeight = this._rowHeight
+ let columnWidth = this._columnWidth
+ for (let cell of this._cells) {
+ if (Utils.isPointInPath({
+ touchCoordinate,
+ cellCoordinate: cell.coordinate,
+ cellWidth: columnWidth,
+ cellHeight: rowHeight,
+ })) {
+ return cell
+ }
+ }
+ return null
+ }
+
+ _sortCells = ({ cellsAnimationOptions, callback, }) => {
+ let { rightTranslation, leftBottomTranslation, } = cellAnimationTypes
+ let animationParallels = callback && []
+
+ for (let cellAnimationOption of cellsAnimationOptions) {
+
+ let { cellComponent, cellIndex, animationType, } = cellAnimationOption
+ let changedIndex = ( animationType == rightTranslation
+ || animationType == leftBottomTranslation ) ? 1 : -1
+
+ //console.log(`cellsAnimationOptions.forEach cellIndex = ${cellIndex}, animationType = ${animationType}, changedIndex = ${changedIndex}`)
+
+ this._cells[ cellIndex + changedIndex ].component = cellComponent
+ let cell = this._cells[ cellIndex ]
+ let coordinate = cell.coordinate
+ if (!callback) {
+ cellComponent.startTranslationAnimation({
+ animationType,
+ coordinate,
+ })
+ }
+ else {
+ let animation = cellComponent.getTranslationAnimation({
+ animationType,
+ coordinate,
+ })
+ animationParallels.push(animation)
+ }
+
+ //console.log(cell.coordinate)
+ }
+
+ callback && Animated.parallel(animationParallels).start(() => {
+ callback && callback()
+ })
+ }
+
+ addCell = ({ data, callback, }) => {
+ if(this._touchEnding) {
+ return
+ }
+ //if (this._isAdding) {
+ // return
+ //}
+ //this._isAdding = true
+ //console.log(`addCell`)
+ let oldDataSourceLength = this.state.dataSource.length
+ let cellChangeType = cellChangeTypes.add
+ this._updateContainerHeight({
+ oldDataSourceLength,
+ cellChangeType,
+ callback: () => {
+ let dataSource = [ ...this.state.dataSource ]
+ //console.log(`addCell dataSource.length = ${dataSource.length} data =`)
+ //console.log(data)
+ dataSource.push(data)
+ this.setState({
+ dataSource,
+ })
+ }
+ })
+
+ //this._isAdding = false
+ }
+
+ removeCell = ({ cellIndex, callback, }) => {
+ if (this._touchEnding || this._isRemoving) {
+ return
+ }
+ this._isRemoving = true
+ let { columnCount, } = this.props
+ let currentCellIndex = cellIndex
+ let hoverCellIndex = this._cells.length - 1
+ //console.log(`removeCell this._cells cellIndex = ${cellIndex}`)
+ //console.log(this._cells)
+ let component = this._cells[ currentCellIndex ].component
+ let animationCallBack = () => {
+ let oldDataSourceLength = this.state.dataSource.length
+ let cellChangeType = cellChangeTypes.remove
+ this._updateContainerHeight({
+ oldDataSourceLength,
+ cellChangeType,
+ })
+ let removedData = this._removeData(cellIndex)
+ this._isRemoving = false
+ callback && callback(removedData)
+ }
+ if (currentCellIndex < this._cells.length - 1) {
+ component.startScaleAnimation({
+ scaleValue: 0,
+ })
+ //console.log(`removeCell currentCellIndex = ${currentCellIndex}, hoverCellIndex = ${hoverCellIndex}, `)
+ let cellsAnimationOptions = Utils.getCellsAnimationOptions({
+ currentCellIndex,
+ hoverCellIndex,
+ columnCount,
+ cells: this._cells,
+ })
+ //console.log(`removeCell cellsAnimationOptions -> `)
+ //console.log(cellsAnimationOptions)
+ this._sortCells({
+ cellsAnimationOptions,
+ callback: animationCallBack,
+ })
+ }
+ else { //removing last cell do not need to sort cell
+
+ component.startScaleAnimation({
+ scaleValue: 0,
+ callback: animationCallBack,
+ })
+ }
+ }
+
+ _updateContainerHeight = ({ oldDataSourceLength, cellChangeType, callback, }) => {
+ let newDataSourceLength = oldDataSourceLength + ( cellChangeType == cellChangeTypes.add ? 1 : -1 )
+ //if (oldDataSourceLength == 0 || newDataSourceLength == 0) {
+ if (cellChangeType == cellChangeTypes.remove && ( oldDataSourceLength == 0 || newDataSourceLength == 0 )) {
+ return
+ }
+ let { columnCount, } = this.props
+ let cellIndex = oldDataSourceLength - 1
+ let oldMaxRowNumber = Utils.getRowNumber({
+ cellIndex,
+ columnCount,
+ })
+ cellIndex = newDataSourceLength - 1
+ let newMaxRowNumber = Utils.getRowNumber({
+ cellIndex,
+ columnCount,
+ })
+ //console.log(`oldDataSourceLength = ${oldDataSourceLength}, newDataSourceLength = ${newDataSourceLength}`)
+ //console.log(`oldMaxRowNumber = ${oldMaxRowNumber}, newMaxRowNumber = ${newMaxRowNumber}, `)
+ if (oldMaxRowNumber == newMaxRowNumber) {
+ callback && callback()
+ return
+ }
+ let height = (Math.floor((newDataSourceLength - 1) / columnCount) + 1 ) * this._rowHeight
+ //console.log(`height = ${height}, this.state.containerHeight = ${this.state.containerHeight}`)
+ if (this._animationInstace) {
+ this._animationInstace.stop()
+ this._animationInstace = null
+ }
+ this._animationInstace = Animated.timing(
+ this.state.containerHeight,
+ {
+ toValue: height,
+ duration: containerHeight.animationDuration,
+ }
+ ).start(() => {
+ this._animationInstace = null
+ callback && callback()
+ })
+ }
+
+ _sortDataSource = () => {
+ //compare and sort dataSource
+ let dataSource = []
+ for (let cell of this._cells) {
+ ////console.log(`this._cells.entries()`)
+ ////console.log(this._cells.entries())
+ ////console.log(`index = ${index}`)
+ ////console.log(cell)
+ let orginalIndex = cell.component.props.index
+ dataSource.push(this.state.dataSource[ orginalIndex ])
+ }
+ //console.log(`_sortDataSource new dataSource ->`)
+ //for (let data of dataSource) {
+ // console.log(`data.title = ${data.title}`)
+ //}
+ //console.log(dataSource)
+ //setTimeout( () => {
+
+ //set dataSource to empty array.
+ //android will occur 'the specified child already has a parent' error if not to do it
+ //but why??? :(
+ this.setState({
+ dataSource: [],
+ })
+
+ this.setState({
+ dataSource,
+ })
+ }
+
+ _removeData = (cellIndex) => {
+ //remove data from dataSource
+ let dataSource = [ ...this.state.dataSource ]
+ let removedDataList = dataSource.splice(cellIndex, 1)
+ //console.log(`_removeData new dataSource ->`)
+ //console.log(dataSource)
+ this.setState({
+ dataSource,
+ })
+ return removedDataList
+ }
+
+ getSortedDataSource = () => {
+ //console.log(`getCellsCount this._cells.length = ${this._cells.length}`)
+ return this.state.dataSource
+ }
+
+ //enableSort = () => {
+ // this.setState({
+ // sortable: true,
+ // })
+ //}
+ //
+ //disableSort = () => {
+ // this.setState({
+ // sortable: false,
+ // })
+ //}
+
+}
+
+export default TimerEnhance(SortableSudokuGrid)
\ No newline at end of file
diff --git a/Utils.js b/Utils.js
new file mode 100644
index 0000000..7946dcd
--- /dev/null
+++ b/Utils.js
@@ -0,0 +1,120 @@
+import {
+ dragDirection,
+ cellAnimationTypes,
+} from './constants'
+
+function isPointInPath ({ touchCoordinate, cellCoordinate, cellWidth, cellHeight, }) {
+ let { x, y, } = cellCoordinate
+ let { x: coordinateX, y: coordinateY } = touchCoordinate
+ ////console.log(`isPointInPath x = ${x}, y = ${y}, coordinateX = ${coordinateX}, coordinateY = ${coordinateY}, cellWidth = ${cellWidth}, cellHeight= ${cellHeight},`)
+ if (!(coordinateX < x || coordinateX > x + cellWidth)
+ && !(coordinateY < y || coordinateY > y + cellHeight)) {
+ return true
+ }
+ return false
+}
+
+function getCellsAnimationOptions ({ currentCellIndex, hoverCellIndex, columnCount, cells }) {
+ let cellsAnimation = []
+ let { up, right, down, left, } = dragDirection
+ let { rightTranslation, leftTranslation, leftBottomTranslation, rightTopTranslation, } = cellAnimationTypes
+ let currentRowNumber = getRowNumber({
+ cellIndex: currentCellIndex,
+ columnCount,
+ })
+ let hoverRowNumber = getRowNumber({
+ cellIndex: hoverCellIndex,
+ columnCount,
+ })
+ //console.log(`getCellsAnimationOptions currentCellIndex = ${currentCellIndex}, hoverCellIndex = ${hoverCellIndex}, currentRowNumber = ${currentRowNumber},hoverRowNumber = ${hoverRowNumber} columnCount = ${columnCount},`)
+ let currentDirection = getDragDirection({
+ currentRowNumber,
+ hoverRowNumber,
+ currentCellIndex,
+ hoverCellIndex,
+ })
+ //console.log(`getCellsAnimationOptions currentDirection = ${currentDirection}`)
+ let len = Math.abs(currentCellIndex - hoverCellIndex)
+ for (let i = 0; i < len; i++) {
+ let cellIndex, animationType
+ switch (currentDirection) {
+ case up: //currentCellIndex > hoverCellIndex
+ cellIndex = hoverCellIndex + i
+ if (isLastCellOfRow({ cellIndex, columnCount, })) {
+ animationType = leftBottomTranslation
+ }
+ else {
+ animationType = rightTranslation
+ }
+ break
+ case right: //currentCellIndex > hoverCellIndex
+ //cellIndex = hoverCellIndex
+ cellIndex = hoverCellIndex - i //for supporting remove cell logic
+ animationType = leftTranslation
+ break
+ case down: //currentCellIndex < hoverCellIndex
+ cellIndex = currentCellIndex + 1 + i
+ if (isFirstCellOfRow({ cellIndex, columnCount, })) {
+ animationType = rightTopTranslation
+ }
+ else {
+ animationType = leftTranslation
+ }
+ break
+ case left: //currentCellIndex < hoverCellIndex
+ cellIndex = hoverCellIndex
+ animationType = rightTranslation
+ break
+ }
+ //console.log(`cellIndex = ${cellIndex} length = ${cells.length}`)
+ //console.log(`cells[ cellIndex ] = `)
+ //console.log(cells[ cellIndex ])
+ let cellComponent = cells[ cellIndex ].component
+ cellsAnimation.push({
+ cellIndex,
+ cellComponent,
+ animationType,
+ })
+ }
+ return cellsAnimation
+}
+
+function getDragDirection ({ currentRowNumber, hoverRowNumber, currentCellIndex, hoverCellIndex, }) {
+ let { up, right, down, left, none } = dragDirection
+ if (currentRowNumber < hoverRowNumber) {
+ return down
+ }
+ else if (currentRowNumber > hoverRowNumber) {
+ return up
+ }
+ else {
+ if (currentCellIndex > hoverCellIndex) {
+ return left
+ }
+ else if (currentCellIndex < hoverCellIndex) {
+ return right
+ }
+ else {
+ return none
+ }
+ }
+}
+
+function getRowNumber ({ cellIndex, columnCount, }) {
+ return Math.floor(cellIndex / columnCount)
+}
+
+function isFirstCellOfRow ({ cellIndex, columnCount, }) {
+ return cellIndex % columnCount == 0
+}
+
+function isLastCellOfRow ({ cellIndex, columnCount, }) {
+ return (cellIndex + 1) % columnCount == 0
+}
+
+export default {
+ isPointInPath,
+ getDragDirection,
+ getCellsAnimationOptions,
+ getRowNumber,
+}
\ No newline at end of file
diff --git a/constants.js b/constants.js
new file mode 100644
index 0000000..ddd5895
--- /dev/null
+++ b/constants.js
@@ -0,0 +1,51 @@
+
+export const cellScale = {
+ value: 1.1,
+ animationDuration: 102,
+}
+export const cellTranslation = {
+ animationDuration: 255,
+}
+export const dragDirection = {
+ up: 2,
+ right: 1,
+ left: -1,
+ down: -2,
+ none: 0,
+}
+export const cellAnimationTypes = {
+ rightTopTranslation: 2,
+ rightTranslation: 1,
+ backToOrigin: 0,
+ leftTranslation: -1,
+ leftBottomTranslation: -2,
+}
+
+export const cellChangeTypes = {
+ add: 1,
+ remove: -1,
+}
+
+export const touchStart = {
+ delay: 255,
+}
+
+export const containerLayout = {
+ delay: 510,
+}
+
+export const containerHeight = {
+ animationDuration: 255,
+}
+
+
+export default {
+ cellScale,
+ cellTranslation,
+ dragDirection,
+ cellAnimationTypes,
+ touchStart,
+ containerLayout,
+ containerHeight,
+ cellChangeTypes,
+}
\ No newline at end of file
diff --git a/note.md b/note.md
new file mode 100644
index 0000000..f678f49
--- /dev/null
+++ b/note.md
@@ -0,0 +1,45 @@
+* 排序逻辑:
+按住(需要控制1秒后才执行逻辑, 如1秒内释放了, 则逻辑不执行, 并将初始值重置), 拖动开始的cell当前对应的component放大1.2倍, 并且cell当前对应的component坐标移动, 中心点与当前按住坐标一致
+移动, 拖动开始的cell当前对应的component坐标移动, 中心点与当前移动坐标一致
+比较当前拖动停留在上面的cell下标顺序, 和当前初始的cell(一开始为拖动开始的cell, 经过停留的cell后, 值变更为该停留的cell)下标顺序, 得到拖动的逻辑方向(从下往上移动, 从上往下移动, 水平移动)
+根据拖动的逻辑方向, 以及触发停留在上面的cell, 计算得出哪些cell(顺序下标)需要执行动画
+cell执行动画, 需要注意的是, 从下往上时, 执行动画的cell向后移1位, 但行尾的cell需要特殊处理(左下斜移至下一行的行首)
+cell执行动画, 需要注意的是, 从上往下时, 执行动画的cell向前移1位, 但行首的cell需要特殊处理(右上斜移至上一行的行首)
+cell执行动画, 需要注意的是, 水平时, 当前初始的cell和当前拖动停留在上面的cell位置互换
+释放, 拖动开始的cell执行动画, 最终位置为当前拖动停留在上面的cell的位置
+
+* 删除逻辑:
+选择的当前cell进行删除, 并且执行排序逻辑中的从上往下拖动的逻辑, 停留项下标直接指定为当前cells的最后一项的下标
+
+* 新增逻辑:
+新增一项在当前cells末尾
+
+* 发现问题:
+0. 在更新数据源state, 重新触发render前, 将实例中每个cell对应的ref等信息缓存列表清空总是失败,
+每次总是恢复到初始化时的旧数据源对应的缓存列表, 但ref的回调函数中的日志输出时却没有问题.
+解决办法, 在ref回调函数中判断遍历到最后一项时, 再对该缓存列表进行处理
+
+1. TouchStart -> TouchMove -> TouchEnd事件顺序触发, 在某些情况下并非是期望的顺序, 需要根据情况自行加变量判断处理
+
+2. 通过设置state来变更列表的数据源时(例如, 当前数据源是[1, 2, 3], 下一个state是[2, 1, 3]),
+有时(android每次都)会报错或者出现最终UI渲染异常的情况(但检查state及render方法却没有发现异常, 推测可能是diff算法比较后, native实现的处理有隐藏bug导致),
+解决办法, 在设置下一个state之前, 设置中间state为空列表(中间state数据源是[]), 即可神奇的解决该问题
+
+3. PanResponder与Touchable系列的View的事件冲突, 默认情况下父容器加入了PanResponder, 子View为Touchable系列的View时将不会触发事件,
+解决办法, 手动控制onStartShouldSetPanResponder, onMoveShouldSetPanResponder
+```js
+onStartShouldSetPanResponder: (e, gestureState) => {
+ //for android, fix the weird conflict between PanRespander and ScrollView
+ return this.state.sortable
+},
+onMoveShouldSetPanResponder: (e, gestureState) => {
+ //for ios/android, fix the conflict between PanRespander and TouchableView
+ var x = gestureState.dx;
+ var y = gestureState.dy;
+ if (x != 0 && y != 0) {
+ return true;
+ }
+ return false;
+},
+```
+
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..022555e
--- /dev/null
+++ b/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "react-native-smart-sortable-sudoku-grid",
+ "version": "1.0.0",
+ "description": "A smart sortable sudoku grid for React Native apps",
+ "main": "SortableSudokuGrid.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/react-native-component/react-native-smart-sortable-sudoku-grid.git"
+ },
+ "keywords": [
+ "react-native",
+ "smart",
+ "sortable",
+ "sudoku",
+ "grid",
+ "component"
+ ],
+ "author": "HISAME SHIZUMARU",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/react-native-component/react-native-smart-sortable-sudoku-grid/issues"
+ },
+ "homepage": "https://github.com/react-native-component/react-native-smart-sortable-sudoku-grid#readme"
+}