Created
October 15, 2015 17:39
-
-
Save sahrens/bf41095cb64526d2a56f to your computer and use it in GitHub Desktop.
Early RecyclingListView prototype
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
/** | |
* Copyright 2004-present Facebook. All Rights Reserved. | |
* | |
* @providesModule RecyclingListView | |
* @flow | |
*/ | |
'use strict'; | |
var React = require('React'); | |
var ScrollView = require('ScrollView'); | |
var Text = require('Text'); | |
var View = require('View'); | |
var TRAILING_BUFFER = 2; // TODO: adaptive | |
var DEBUG = false; | |
class CellRenderer extends React.Component { | |
shouldComponentUpdate(newProps) { | |
return newProps.shouldUpdate; | |
} | |
render() { | |
var debug = DEBUG && <Text style={{backgroundColor: 'lightblue'}}> | |
React Key: {this.props.reactKey}, | |
Row: {this.props.rowIndex}, | |
Id: {this.props.data.__dataID__} | |
</Text>; | |
// var start = Date.now(); | |
// while (Date.now() < start + 1000) {} // burn cpu to test perf effect. | |
return ( | |
<View | |
ref={(ref) => this.viewRef = ref} | |
onLayout={(e) => { | |
this.props.onMeasure(e.nativeEvent.layout); | |
}}> | |
{debug} | |
{this.props.render()} | |
</View> | |
); | |
} | |
} | |
CellRenderer.propTypes = { | |
shouldUpdate: React.PropTypes.bool, | |
render: React.PropTypes.func, | |
}; | |
/** | |
* A windowed ListView for memory savings and perf. Only a fixed number of rows are rendered. By default, | |
* row components are recycled when they go off screen, which breaks useless features like `setState`. | |
* `disableRecycling` will keep the windowing but create/destroy rows - state will be lost, but at least | |
* won't be garbled? | |
* | |
* I have an idea for a `RecyclableMixin` or wrapper component that would allow `setState` to work | |
* transparently, but `state` would need to be accessed via `getState`. | |
*/ | |
class RecyclingListView extends React.Component { | |
_rowCache: Array<number>; | |
constructor(props: Object) { | |
super(props); | |
this.onScroll = this.onScroll.bind(this); | |
this.lastScrollEvent = {rowFrames: []}; | |
this.numToRender = this.props.numToRender; | |
this.computeRowsToRender = this.computeRowsToRender.bind(this); | |
this.computeRowsToRenderSync = this.computeRowsToRenderSync.bind(this); | |
this.scrollOffsetY = 0; | |
this.rowFrames = []; | |
this.calledOnEndReached = false; | |
this.state = { | |
firstRow: 0, | |
lastRow: Math.min(this.numToRender, this.props.data.length) - 1, | |
}; | |
} | |
getScrollResponder(): ?ReactComponent { | |
return this.scrollRef && | |
this.scrollRef.getScrollResponder && | |
this.scrollRef.getScrollResponder(); | |
} | |
onScroll(e: Object) { | |
var scrollOffsetY = e.nativeEvent.contentOffset.y; | |
if (this.scrollOffsetY !== scrollOffsetY) { | |
this.scrollOffsetY = scrollOffsetY; | |
this.computeRowsToRender(); | |
} | |
} | |
componentWillReceiveProps() { | |
this.computeRowsToRender(); | |
} | |
_onMeasure(idx: number, layout: Object) { | |
if (!this.rowFrames[idx] || | |
layout.height !== this.rowFrames[idx].height || | |
layout.y !== this.rowFrames[idx].y) { | |
this.rowFrames[idx] = {...layout}; | |
this.computeRowsToRender(); | |
} | |
} | |
computeRowsToRender(): void { | |
if (!this.willComputeRowsToRender) { | |
this.willComputeRowsToRender = true; // batch up computations | |
setImmediate(this.computeRowsToRenderSync); | |
} | |
} | |
computeRowsToRenderSync(): void { | |
this.willComputeRowsToRender = false; | |
var rowFrames = this.rowFrames; | |
var totalRows = this.props.data.length; | |
var firstVisible; | |
var top = this.scrollOffsetY; | |
for (var idx = 0; idx < rowFrames.length; idx++) { | |
var frame = rowFrames[idx]; | |
if (!frame) { | |
console.warn('waa? No frame :( Should come soon.'); | |
return; | |
} | |
if ((frame.y + frame.height) > top) { | |
firstVisible = idx; | |
break; | |
} | |
} | |
if (firstVisible === undefined) { | |
firstVisible = rowFrames.length; | |
console.warn('overrun', {firstVisible, numTorender: this.numToRender}); | |
} | |
var firstRow = Math.max((firstVisible || 0) - TRAILING_BUFFER, 0); | |
var lastRow = Math.min(firstRow + this.numToRender - 1, totalRows - 1); | |
if (lastRow - firstRow < this.numToRender - 1) { | |
// If we're close to the bottom, we still want to render numToRender rows | |
firstRow = Math.max(0, lastRow - this.numToRender + 1); | |
} | |
if (this.props.onEndReached) { | |
// This logic is a little tricky to make sure we call onEndReached exactly | |
// once every time we reach the end. | |
var willBeAtTheEnd = lastRow === (totalRows - 1); | |
if (willBeAtTheEnd && !this.calledOnEndReached) { | |
this.props.onEndReached(); | |
this.calledOnEndReached = true; | |
} else { | |
// If the last row is changing, then we haven't called onEndReached for | |
// the new end. | |
this.calledOnEndReached = this.state.lastRow === lastRow; | |
} | |
} | |
if (this.state.firstRow !== firstRow || this.state.lastRow !== lastRow) { | |
var count = lastRow - firstRow + 1; | |
if (count !== this.numToRender && firstRow !== 0 && this.numToRender < totalRows) { | |
console.warn('Should always render same number of rows for optimal perf.'); | |
} | |
console.log('lastRow: ' + lastRow); | |
// console.log('computedRows', {count, firstVisible, firstRow, lastRow, top}); | |
this.setState({firstRow, lastRow}); | |
} | |
} | |
render() { | |
this._rowCache = this._rowCache || []; | |
var firstRow = this.state.firstRow; | |
var lastRow = this.state.lastRow; | |
var rowFrames = this.rowFrames; | |
var rows = []; | |
var height = 0; | |
var avgHeight = 100; | |
if (rowFrames[firstRow]) { | |
var firstFrame = rowFrames[firstRow]; | |
height = firstFrame.y; | |
avgHeight = height / (firstRow + 1); // TODO: use the last row with a frame | |
// console.log('compute avgHeight1', {avgHeight, height, firstRow}); | |
} | |
var rowsMeta = []; | |
var topHeight = height; | |
rows.push(<View key={'sp-top'} style={{height}} />); | |
var fullRowCount = lastRow - firstRow + 1; | |
if (this.props.disableRecycling) { | |
fullRowCount += firstRow; // Still only renders fixed number of rows, but doesn't reuse react keys | |
} | |
for (var idx = firstRow; idx <= lastRow; idx++) { | |
height += rowFrames[idx] ? rowFrames[idx].height : avgHeight; | |
var data = this.props.data[idx]; | |
var key = idx % fullRowCount; | |
var shouldUpdate = data !== this._rowCache[key]; | |
rows.push( | |
<CellRenderer | |
key={key} | |
reactKey={key} | |
rowIndex={idx} | |
data={data} | |
onMeasure={this._onMeasure.bind(this, idx)} | |
shouldUpdate={shouldUpdate} | |
render={this.props.renderRow.bind( | |
null, data, 0, idx, key | |
)} | |
/> | |
); | |
this._rowCache[key] = data; | |
rowsMeta.push({key, idx, savedFrame: rowFrames[idx], dataId: data.__dataID__, shouldUpdate}); | |
} | |
if (this.props.renderFooter) { | |
rows.push(this.props.renderFooter()); | |
} | |
if (lastRow && height) { | |
avgHeight = height / lastRow; | |
} | |
height = (this.props.data.length - lastRow - 1) * avgHeight; // This allows overrun for huge lists - maybe we don't want this? | |
rows.push(<View key={'sp-bottom'} style={{height}} />); | |
DEBUG && console.log('rec list render', {rowsMeta, avgHeight, topHeight, botHeight: height, rowFrames, firstRow, lastRow, firstVisible: this.firstVisible, countAll: rows.length}); | |
return ( | |
<ScrollView | |
scrollEventThrottle={50} | |
removeClippedSubviews={true} | |
{...this.props} | |
ref={(ref) => { this.scrollRef = ref; }} | |
onScroll={this.onScroll}> | |
{rows} | |
</ScrollView> | |
); | |
} | |
} | |
RecyclingListView.propTypes = { | |
data: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, | |
renderRow: React.PropTypes.func.isRequired, | |
disableRecycling: React.PropTypes.bool, // destroy offscreen rows instead of recycling. Means you can use setState normally. | |
}; | |
RecyclingListView.defaultProps = { | |
numToRender: 10, | |
}; | |
module.exports = RecyclingListView; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment