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 + +[![npm](https://img.shields.io/npm/v/react-native-smart-sortable-sudoku-grid.svg)](https://www.npmjs.com/package/react-native-smart-sortable-sudoku-grid) +[![npm](https://img.shields.io/npm/dm/react-native-smart-sortable-sudoku-grid.svg)](https://www.npmjs.com/package/react-native-smart-sortable-sudoku-grid) +[![npm](https://img.shields.io/npm/dt/react-native-smart-sortable-sudoku-grid.svg)](https://www.npmjs.com/package/react-native-smart-sortable-sudoku-grid) +[![npm](https://img.shields.io/npm/l/react-native-smart-sortable-sudoku-grid.svg)](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" +}