Last active
May 10, 2017 19:25
-
-
Save janicduplessis/83de4191a01dbf816ea2a025149b8673 to your computer and use it in GitHub Desktop.
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
// @flow | |
import React, { Component } from 'react'; | |
import { | |
VirtualizedList, | |
View, | |
ScrollView, | |
StyleSheet, | |
// $FlowFixMe | |
findNodeHandle, | |
} from 'react-native'; | |
type Column = { | |
index: number, | |
totalHeight: number, | |
data: Array<any>, | |
heights: Array<number>, | |
}; | |
const _stateFromProps = ({ numColumns, data, getHeightForItem }) => { | |
const columns: Array<Column> = Array.from({ | |
length: numColumns, | |
}).map((col, i) => ({ | |
index: i, | |
totalHeight: 0, | |
data: [], | |
heights: [], | |
})); | |
data.forEach((item, index) => { | |
const height = getHeightForItem({ item, index }); | |
const column = columns.reduce( | |
(prev, cur) => (cur.totalHeight < prev.totalHeight ? cur : prev), | |
columns[0], | |
); | |
column.data.push(item); | |
column.heights.push(height); | |
column.totalHeight += height; | |
}); | |
return { columns }; | |
}; | |
type Props = { | |
data: Array<any>, | |
numColumns: number, | |
renderItem: Function, | |
getHeightForItem: ({ item: any, index: number }) => number, | |
ListHeaderComponent?: ?ReactClass<any>, | |
keyExtractor: (item: any, index: number) => string, | |
// onEndReached will get called once per column, not ideal but should not cause | |
// issues with isLoading checks. | |
onEndReached?: ?(info: { distanceFromEnd: number }) => void, | |
}; | |
/** | |
* Implement a windowed masonry style list with variable number of columns. This | |
* uses one VirtualList per column that renders into a `View` using `renderScrollComponent` | |
* and pipes events from the `ScrollView` it uses to each of the `VirtualList` so it can | |
* update as if it was rendering inside a `ScrollView`. Note that this abuses private APIs | |
* and could break on new RN updates. | |
* | |
* Current limitations: | |
* - Need to implement `getHeightForItem` since we need the dimensions of every cell. | |
* It would be possible to avoid that by doing measuring here. | |
*/ | |
export default class MasonryList extends Component { | |
props: Props; | |
state = { ..._stateFromProps(this.props), headerHeight: null }; | |
_listRefs: Array<VirtualizedList> = []; | |
_scrollRef: ?ScrollView; | |
componentWillReceiveProps(newProps: Props) { | |
this.setState(_stateFromProps(newProps)); | |
} | |
getScrollResponder() { | |
if (this._scrollRef && this._scrollRef.getScrollResponder) { | |
return this._scrollRef.getScrollResponder(); | |
} | |
return null; | |
} | |
getScrollableNode() { | |
if (this._scrollRef && this._scrollRef.getScrollableNode) { | |
return this._scrollRef.getScrollableNode(); | |
} else { | |
return findNodeHandle(this._scrollRef); | |
} | |
} | |
_onLayout = event => { | |
this._listRefs.forEach(list => list._onLayout(event)); | |
}; | |
_onContentSizeChange = (width, height) => { | |
this._listRefs.forEach(list => list._onContentSizeChange(width, height)); | |
}; | |
_onScroll = event => { | |
this._listRefs.forEach(list => list._onScroll(event)); | |
}; | |
_onScrollBeginDrag = event => { | |
this._listRefs.forEach(list => list._onScrollBeginDrag(event)); | |
}; | |
_onHeaderLayout = event => { | |
this.setState({ headerHeight: event.nativeEvent.layout.height }); | |
}; | |
_getItemLayout = (columnIndex, rowIndex) => { | |
const column = this.state.columns[columnIndex]; | |
let offset = 0; | |
for (let ii = 0; ii < rowIndex; ii += 1) { | |
offset += column.heights[ii]; | |
} | |
return { length: column.heights[rowIndex], offset, index: rowIndex }; | |
}; | |
_renderScrollComponent = () => { | |
return <View style={styles.column} />; | |
}; | |
_renderPlaceholderHeader = () => { | |
// For the list header we want it to span all columns so we render it as position absolute | |
// on top of everything. To make sure VirtualLists still work properly we create an empty | |
// header of the same size as the real one. | |
return <View style={{ height: this.state.headerHeight }} />; | |
}; | |
render() { | |
const { | |
renderItem, | |
ListHeaderComponent, | |
keyExtractor, | |
onEndReached, | |
...others | |
} = this.props; | |
let headerElement; | |
if (ListHeaderComponent) { | |
headerElement = ( | |
<View | |
onLayout={this._onHeaderLayout} | |
style={{ | |
position: 'absolute', | |
top: 0, | |
left: 0, | |
right: 0, | |
}} | |
> | |
<ListHeaderComponent /> | |
</View> | |
); | |
} | |
return ( | |
<ScrollView | |
{...others} | |
ref={ref => (this._scrollRef = ref)} | |
onContentSizeChange={this._onContentSizeChange} | |
onLayout={this._onLayout} | |
onScroll={this._onScroll} | |
onScrollBeginDrag={this._onScrollBeginDrag} | |
> | |
<View style={styles.contentContainer}> | |
{(!headerElement || this.state.headerHeight !== null) && | |
this.state.columns.map(col => ( | |
<VirtualizedList | |
ref={ref => (this._listRefs[col.index] = ref)} | |
key={`$col_${col.index}`} | |
data={col.data} | |
getItemLayout={(data, index) => | |
this._getItemLayout(col.index, index)} | |
renderItem={({ item, index }) => | |
renderItem({ item, index, column: col.index })} | |
renderScrollComponent={this._renderScrollComponent} | |
keyExtractor={keyExtractor} | |
ListHeaderComponent={ | |
headerElement && this._renderPlaceholderHeader | |
} | |
onEndReached={onEndReached} | |
/> | |
))} | |
</View> | |
{headerElement} | |
</ScrollView> | |
); | |
} | |
} | |
const styles = StyleSheet.create({ | |
contentContainer: { | |
flexDirection: 'row', | |
}, | |
column: { | |
flex: 1, | |
}, | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment