From 0dd5e2f301c181495829225124d28171e759a775 Mon Sep 17 00:00:00 2001 From: cyqresig Date: Thu, 15 Dec 2016 23:59:47 +0800 Subject: [PATCH] finished first version --- .gitignore | 12 + .npmignore | 12 + README.md | 314 +++++++++++++++++++++++- SortableCell.js | 178 ++++++++++++++ SortableSudokuGrid.js | 559 ++++++++++++++++++++++++++++++++++++++++++ Utils.js | 120 +++++++++ constants.js | 51 ++++ note.md | 45 ++++ package.json | 27 ++ 9 files changed, 1317 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 SortableCell.js create mode 100644 SortableSudokuGrid.js create mode 100644 Utils.js create mode 100644 constants.js create mode 100644 note.md create mode 100644 package.json 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" +}