Skip to content

Instantly share code, notes, and snippets.

@janicduplessis
Last active May 10, 2017 19:25
Show Gist options
  • Save janicduplessis/83de4191a01dbf816ea2a025149b8673 to your computer and use it in GitHub Desktop.
Save janicduplessis/83de4191a01dbf816ea2a025149b8673 to your computer and use it in GitHub Desktop.
// @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