Last active
February 1, 2016 05:50
-
-
Save deanmcpherson/1fb20d00978bf8dbcd2c to your computer and use it in GitHub Desktop.
React native drag and drop list view in progress
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var React = require('react-native'); | |
var { | |
ListView, | |
LayoutAnimation, | |
View, | |
Animated, | |
PanResponder, | |
TouchableWithoutFeedback | |
} = React; | |
var Row = React.createClass({ | |
shouldComponentUpdate: function(props) { | |
if (props.hovering !== this.props.hovering) return true; | |
if (props.rowData.data !== this.props.rowData.data) return true; | |
return false; | |
}, | |
handleLongPress: function(e) { | |
this.refs.view.measure((frameX, frameY, frameWidth, frameHeight, pageX, pageY) => { | |
let layout = {frameX, frameY, frameWidth, frameHeight, pageX, pageY}; | |
this.props.onRowActive({ | |
layout: layout, | |
touch: e.nativeEvent, | |
rowData: this.props.rowData | |
}); | |
}); | |
}, | |
render: function() { | |
let layout = this.props.list.layoutMap[this.props.rowData.index]; | |
let activeData = this.props.list.state.active; | |
let activeIndex = activeData ? Number(activeData.rowData.index) : -5; | |
let shouldDisplayHovering = !(activeIndex == this.props.rowData.index || activeIndex+1 == this.props.rowData.index); | |
let Row = React.cloneElement(this.props.renderRow(this.props.rowData.data, this.props.rowData.section, this.props.rowData.index, null, this.props.active), {onLongPress: this.handleLongPress}); | |
return <View onLayout={this.props.onRowLayout} style={this.props.active ? {opacity: .3}: null} ref="view"> | |
{this.props.hovering && shouldDisplayHovering ? this.props.activeDivider : null} | |
{Row} | |
</View> | |
} | |
}); | |
var SortRow = React.createClass({ | |
getInitialState: function() { | |
let layout = this.props.list.state.active.layout; | |
let rowLayout = this.props.list.layoutMap[this.props.rowData.index]; | |
return { | |
style: { | |
position: 'absolute', | |
left: 0, | |
right: 0, | |
height: layout.frameHeight, | |
overflow: 'hidden', | |
backgroundColor: 'transparent', | |
marginTop: layout.pageY - 20 //Account for top bar spacing | |
} | |
} | |
}, | |
render: function() { | |
let handlers = this.props.panResponder.panHandlers; | |
return <Animated.View style={[this.state.style, this.props.list.state.pan.getLayout()]}> | |
{this.props.renderRow(this.props.rowData.data, this.props.rowData.section, this.props.rowData.index, true)} | |
</Animated.View> | |
} | |
}); | |
var SortableListView = React.createClass({ | |
getInitialState:function() { | |
let currentPanValue = {x: 0, y: 0}; | |
this.state = { | |
ds: new ListView.DataSource({rowHasChanged: (r1, r2) => { | |
let dataChanged = r1 == r2; | |
return true | |
}}), | |
sorting: false, | |
active: false, | |
pan: new Animated.ValueXY(currentPanValue) | |
}; | |
let onPanResponderMoveCb = Animated.event([null, { | |
dx: this.state.pan.x, // x,y are Animated.Value | |
dy: this.state.pan.y, | |
}]); | |
this.state.panResponder = PanResponder.create({ | |
onStartShouldSetPanResponder: () => true, | |
onMoveShouldSetResponderCapture: () => true, | |
onMoveShouldSetPanResponder: () => true, | |
onMoveShouldSetPanResponderCapture: () => true, | |
onPanResponderMove: (evt, gestureState) => { | |
gestureState.dx = 0; | |
this.moveY = gestureState.moveY; | |
onPanResponderMoveCb(evt, gestureState); | |
}, | |
onPanResponderGrant: (e, gestureState) => { | |
console.log('granted') | |
this.state.pan.setOffset(currentPanValue); | |
this.state.pan.setValue(currentPanValue); | |
}, | |
onPanResponderReject: (e, gestureState) => { | |
console.log('rejected', e) | |
}, | |
onPanResponderRelease: (e) => { | |
this.setState({active: false, sorting: false, hovering: false}); | |
} | |
}); | |
return this.state; | |
}, | |
componentDidMount: function() { | |
this.scrollResponder = this.refs.list.getScrollResponder(); | |
}, | |
scrollValue: 0, | |
scrollAnimation: function() { | |
if (this.isMounted() && this.state.active) { | |
if (this.moveY == undefined) return requestAnimationFrame(this.scrollAnimation); | |
let SCROLL_LOWER_BOUND = 100; | |
let SCROLL_HIGHER_BOUND = this.listLayout.height - SCROLL_LOWER_BOUND; | |
let MAX_SCROLL_VALUE = this.scrollContainerHeight; | |
let currentScrollValue = this.scrollValue; | |
let newScrollValue = null; | |
let SCROLL_MAX_CHANGE = 15; | |
if (this.moveY < SCROLL_LOWER_BOUND && currentScrollValue > 0) { | |
let PERCENTAGE_CHANGE = 1 - (this.moveY / SCROLL_LOWER_BOUND); | |
newScrollValue = currentScrollValue - (PERCENTAGE_CHANGE * SCROLL_MAX_CHANGE); | |
if (newScrollValue < 0) newScrollValue = 0; | |
} | |
if (this.moveY > SCROLL_HIGHER_BOUND && currentScrollValue < MAX_SCROLL_VALUE) { | |
let PERCENTAGE_CHANGE = 1 - ((this.listLayout.height - this.moveY) / SCROLL_LOWER_BOUND); | |
newScrollValue = currentScrollValue + (PERCENTAGE_CHANGE * SCROLL_MAX_CHANGE); | |
if (newScrollValue > MAX_SCROLL_VALUE) newScrollValue = MAX_SCROLL_VALUE; | |
} | |
if (newScrollValue !== null) { | |
this.scrollValue = newScrollValue; | |
this.scrollResponder.scrollWithoutAnimationTo(this.scrollValue, 0); | |
} | |
this.checkTargetElement(); | |
requestAnimationFrame(this.scrollAnimation); | |
} | |
}, | |
checkTargetElement() { | |
let scrollValue = this.scrollValue; | |
let moveY = this.moveY; | |
let targetPixel = scrollValue + moveY; | |
let i = 0; | |
let x = 0; | |
let row; | |
while (i < targetPixel) { | |
row = this.layoutMap[x]; | |
if (!row) { | |
return; | |
} | |
i += row.height; | |
x++; | |
} | |
x--; | |
if (x != this.state.hovering) { | |
LayoutAnimation.easeInEaseOut(); | |
this.setState({ | |
hovering: String(x) | |
}) | |
} | |
}, | |
layoutMap: {}, | |
handleRowActive: function(row) { | |
this.state.pan.setValue({x: 0, y: 0}); | |
LayoutAnimation.easeInEaseOut(); | |
this.setState({ | |
sorting: true, | |
active: row | |
}, this.scrollAnimation); | |
}, | |
renderActiveDivider: function() { | |
if (this.props.activeDivider) this.props.activeDivider(); | |
return <View style={{height: this.state.active && this.state.active.layout.frameHeight}} /> | |
}, | |
renderRow: function(data, section, index, highlightfn, active) { | |
let Component = active ? SortRow : Row; | |
let isActiveRow = (!active && this.state.active && this.state.active.rowData.index === index); | |
if (!active && isActiveRow) { | |
active = {active: true}; | |
} | |
return <Component | |
{...this.props} | |
activeDivider={this.renderActiveDivider() } | |
key={index} | |
active={active} | |
list={this} | |
hovering={this.state.hovering === index} | |
panResponder={this.state.panResponder} | |
rowData={{data, section, index}} | |
onRowActive={this.handleRowActive} | |
onRowLayout={layout => this.layoutMap[index] = layout.nativeEvent.layout} | |
/> | |
}, | |
renderActive: function() { | |
if (!this.state.active) return; | |
let index = this.state.active.rowData.index; | |
return this.renderRow(this.props.data[index], 's1', index, () => {}, {active: true, thumb: true}); | |
}, | |
render: function() { | |
global.list = this; | |
let dataSource = this.state.ds.cloneWithRows(this.props.data); | |
return <View style={{flex: 1}}> | |
<ListView | |
{...this.props} | |
{...this.state.panResponder.panHandlers} | |
ref="list" | |
dataSource={dataSource} | |
onScroll={e => { | |
this.scrollValue = e.nativeEvent.contentOffset.y; | |
this.scrollContainerHeight = e.nativeEvent.contentSize.height; | |
}} | |
onLayout={(e) => this.listLayout = e.nativeEvent.layout} | |
removeClippedSubviews={true} | |
scrollEnabled={!this.state.active} | |
renderRow={this.renderRow} | |
/> | |
{this.renderActive()} | |
</View> | |
} | |
}); | |
module.exports = SortableListView; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment