react-native-largelist
Version:
The best performance large list component which is much better than SectionList for React Native.
338 lines (321 loc) • 10.8 kB
JavaScript
/*
*
* Created by Stone
* https://github.com/bolan9999
* Email: shanshang130@gmail.com
* Date: 2019/2/20
*
*/
import React from "react";
import type {LargeListPropType, Offset, Size, WaterfallListType} from "./Types";
import {SpringScrollView} from "react-native-spring-scrollview";
import {
Animated,
Dimensions,
StyleSheet,
View,
LayoutAnimation,
Platform,
} from "react-native";
import {styles} from "./styles";
import {idx} from "./idx";
import {WaterfallItem} from "./WaterfallItem";
const screenLayout = Dimensions.get("window");
const screenHeight = Math.max(screenLayout.width, screenLayout.height);
export class WaterfallList extends React.PureComponent<WaterfallListType> {
_size: Size;
_headerLayout;
_footerLayout;
_orgOnHeaderLayout;
_orgOnFooterLayout;
_contentOffsetY = 0;
_nativeOffset;
_offset: Animated.Value;
_itemRefs: [];
_shouldUpdateContent = true;
_scrollView = React.createRef();
constructor(props) {
super(props);
this._nativeOffset = {
x: new Animated.Value(0),
y: new Animated.Value(0),
...this.props.onNativeContentOffsetExtract,
};
this._offset = this._nativeOffset.y;
}
_renderEmpty() {
return (
<SpringScrollView
contentStyle={{flex: 1}}
{...this.props}
ref={this._scrollView}
onSizeChange={this._onSizeChange}
onNativeContentOffsetExtract={this._nativeOffset}
onScroll={this._onScroll}
>
{this._renderHeader && this._renderHeader()}
{this.props.renderEmpty && this.props.renderEmpty()}
{this.props.renderFooter && this.props.renderFooter()}
</SpringScrollView>
);
}
render() {
let {numColumns, heightForItem, data, preferColumnWidth} = this.props;
const columnSummaries = [];
let sumHeight = null;
if (this.props.renderEmpty && data.length === 0) {
return this._renderEmpty();
}
if (this._shouldRenderContent()) {
this._itemRefs = [];
sumHeight = idx(() => this._headerLayout.height, 0);
if (!numColumns)
numColumns = Math.floor(this._size.width / preferColumnWidth);
for (let i = 0; i < numColumns; ++i) {
columnSummaries.push({
sumHeight: 0,
indexes: [],
tops: [],
heights: [],
cells: [],
inputs: [],
outputs: [],
itemIndexes: [],
inputItemIndexes: [],
});
}
data.forEach((item, index) => {
const height = heightForItem(item, index);
let minHeight = Number.MAX_SAFE_INTEGER;
let minHeightIndex = 0;
columnSummaries.forEach((summary, idx) => {
if (minHeight > summary.sumHeight) {
minHeight = summary.sumHeight;
minHeightIndex = idx;
}
});
const lastHeight = idx(
() =>
columnSummaries[minHeightIndex].heights[
columnSummaries[minHeightIndex].heights.length - 1
],
0,
);
columnSummaries[minHeightIndex].sumHeight += height;
columnSummaries[minHeightIndex].heights.push(height);
columnSummaries[minHeightIndex].indexes.push(index);
const lastTop = idx(
() =>
columnSummaries[minHeightIndex].tops[
columnSummaries[minHeightIndex].tops.length - 1
],
idx(() => this._headerLayout.height, 0),
);
columnSummaries[minHeightIndex].tops.push(lastTop + lastHeight);
});
let maxHeight = Number.MIN_SAFE_INTEGER;
columnSummaries.forEach(summary => {
if (maxHeight < summary.sumHeight) {
maxHeight = summary.sumHeight;
}
const viewport = [];
summary.tops.forEach((top, index) => {
const first = viewport[0];
if (
first !== undefined &&
top + summary.heights[index] - first > screenHeight
) {
viewport.splice(0, 1);
}
viewport.push(top);
while (summary.cells.length < viewport.length + 2)
summary.cells.push(summary.cells.length);
});
});
sumHeight += maxHeight;
sumHeight += idx(() => this._footerLayout.height, 0);
if (sumHeight <= this._size.height)
sumHeight = this._size.height + StyleSheet.hairlineWidth;
columnSummaries.forEach(summary => {
summary.indexes.forEach((itemIndex, index) => {
const cellIndex = index % summary.cells.length;
if (!summary.inputs[cellIndex]) {
summary.inputs[cellIndex] = [Number.MIN_SAFE_INTEGER];
summary.inputItemIndexes[cellIndex] = [itemIndex];
}
if (!summary.outputs[cellIndex])
summary.outputs[cellIndex] = [idx(() => summary.tops[index], 0)];
if (!summary.itemIndexes[cellIndex])
summary.itemIndexes[cellIndex] = [];
summary.inputs[cellIndex].push(
summary.tops[index] - this._size.height,
summary.tops[index] - this._size.height + 0.1,
);
summary.inputItemIndexes[cellIndex].push(itemIndex, itemIndex);
summary.outputs[cellIndex].push(
summary.outputs[cellIndex][summary.outputs[cellIndex].length - 1],
);
summary.outputs[cellIndex].push(
idx(
() => summary.tops[index],
summary.outputs[cellIndex][summary.outputs[cellIndex].length - 1],
),
);
summary.itemIndexes[cellIndex].push(itemIndex);
});
summary.inputs.forEach((inputs, index) => {
inputs.push(Number.MAX_SAFE_INTEGER);
summary.inputItemIndexes[index].push(
summary.inputItemIndexes[index][
summary.inputItemIndexes[index].length - 1
],
);
});
summary.outputs.forEach(outputs =>
outputs.push(outputs[outputs.length - 1]),
);
});
columnSummaries.forEach(summary => {
const itemRefs = [];
summary.itemIndexes.forEach(() => {
itemRefs.push(React.createRef());
});
this._itemRefs.push(itemRefs);
});
}
return (
<SpringScrollView
{...this.props}
contentStyle={{height: sumHeight}}
ref={this._scrollView}
onScroll={this._onScroll}
onNativeContentOffsetExtract={this._nativeOffset}
onSizeChange={this._onSizeChange}
>
{this._renderHeader()}
{columnSummaries.map((summary, index) =>
summary.itemIndexes.map((itemIndex, cellIndex) => (
<WaterfallItem
{...this.props}
columnIdx={index}
key={summary.inputItemIndexes[cellIndex][0]}
ref={this._itemRefs[index][cellIndex]}
offset={this._contentOffsetY}
itemIndexes={summary.inputItemIndexes[cellIndex]}
input={summary.inputs[cellIndex]}
output={summary.outputs[cellIndex]}
style={StyleSheet.flatten([
styles.leftTop,
{
width: this._size.width / numColumns,
transform: [
{
translateY: this._offset.interpolate({
inputRange: summary.inputs[cellIndex],
outputRange: summary.outputs[cellIndex],
}),
},
{translateX: (this._size.width / numColumns) * index},
],
},
])}
/>
)),
)}
{this._renderFooter()}
</SpringScrollView>
);
}
_renderHeader() {
const {renderHeader} = this.props;
if (!renderHeader) return null;
const transform = {
transform: [{translateY: this._shouldRenderContent() ? 0 : 10000}],
};
const header = React.Children.only(renderHeader());
this._orgOnHeaderLayout = header.onLayout;
return React.cloneElement(header, {
style: StyleSheet.flatten([header.props.style, transform]),
onLayout: this._onHeaderLayout,
});
}
_renderFooter() {
const {renderFooter} = this.props;
if (!renderFooter) return null;
const transform = {
transform: [{translateY: this._shouldRenderContent() ? 0 : 10000}],
};
const footer = React.Children.only(renderFooter());
this._orgOnFooterLayout = footer.onLayout;
return React.cloneElement(footer, {
style: StyleSheet.flatten([styles.footer, footer.props.style, transform]),
onLayout: this._onFooterLayout,
});
}
_shouldRenderContent() {
const {renderHeader, renderFooter} = this.props;
return (
this._size &&
(!renderHeader || this._headerLayout) &&
(!renderFooter || this._footerLayout)
);
}
_onSizeChange = size => {
this._size = size;
if (this._shouldRenderContent()) this.forceUpdate();
};
_onHeaderLayout = e => {
if (
this._headerLayout &&
this._headerLayout.height === e.nativeEvent.layout.height
)
return;
this._headerLayout = e.nativeEvent.layout;
this._orgOnHeaderLayout && this._orgOnHeaderLayout(e);
if (this._shouldRenderContent()) this.forceUpdate();
};
_onFooterLayout = e => {
if (
this._footerLayout &&
this._footerLayout.height === e.nativeEvent.layout.height
)
return;
this._footerLayout = e.nativeEvent.layout;
this._orgOnFooterLayout && this._orgOnFooterLayout(e);
if (this._shouldRenderContent()) this.forceUpdate();
};
_onScroll = e => {
this._contentOffsetY = e.nativeEvent.contentOffset.y;
const now = new Date().getTime();
if (this._lastTick - now > 30) {
this._lastTick = now;
return;
}
this._lastTick = now;
this._shouldUpdateContent &&
this._itemRefs.forEach(column =>
column.forEach(itemRef =>
idx(() => itemRef.current.updateOffset(this._contentOffsetY)),
),
);
this._shouldUpdateContent && this.props.onScroll && this.props.onScroll(e);
};
scrollTo(offset: Offset, animated: boolean = true): Promise<void> {
if (!this._scrollView.current)
return Promise.reject("WaterfallList has not been initialized yet!");
this._shouldUpdateContent = false;
this._itemRefs.forEach(column =>
column.forEach(itemRef => idx(itemRef.current.updateOffset(offset.y))),
);
return this._scrollView.current.scrollTo(offset, animated).then(() => {
this._shouldUpdateContent = true;
return Promise.resolve();
});
}
endRefresh() {
idx(() => this._scrollView.current.endRefresh());
}
endLoading() {
idx(() => this._scrollView.current.endLoading());
}
}