react-native/Libraries/Experimental/WindowedListView.js

734 lines
28 KiB
JavaScript

/**
* Copyright (c) 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* Facebook, Inc. ("Facebook") owns all right, title and interest, including
* all intellectual property and other proprietary rights, in and to the React
* Native CustomComponents software (the "Software"). Subject to your
* compliance with these terms, you are hereby granted a non-exclusive,
* worldwide, royalty-free copyright license to (1) use and copy the Software;
* and (2) reproduce and distribute the Software as part of your own software
* ("Your Software"). Facebook reserves all rights not expressly granted to
* you in this license agreement.
*
* THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS
* OR IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED.
* IN NO EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICERS, DIRECTORS OR
* EMPLOYEES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* @providesModule WindowedListView
* @flow
*/
'use strict';
const IncrementalGroup = require('IncrementalGroup');
const React = require('React');
const ScrollView = require('ScrollView');
const Set = require('Set');
const StyleSheet = require('StyleSheet');
const Systrace = require('Systrace');
const View = require('View');
const ViewabilityHelper = require('ViewabilityHelper');
const clamp = require('clamp');
const deepDiffer = require('deepDiffer');
const infoLog = require('infoLog');
const invariant = require('invariant');
const nullthrows = require('nullthrows');
import type ReactComponent from 'ReactComponent';
const DEBUG = false;
/**
* An experimental ListView implementation designed for efficient memory usage
* when rendering huge/infinite lists. It works by rendering a subset of rows
* and replacing offscreen rows with an empty spacer, which means that it has to
* re-render rows when scrolling back up.
*
* Note that rows must be the same height when they are re-mounted as when they
* are unmounted otherwise the content will jump around. This means that any
* state that affects the height, such as tap to expand, should be stored
* outside the row component to maintain continuity.
*
* This is not a drop-in replacement for `ListView` - many features are not
* supported, including section headers, dataSources, horizontal layout, etc.
*
* Row data should be provided as a simple array corresponding to rows. `===`
* is used to determine if a row has changed and should be re-rendered.
*
* Rendering is done incrementally one row at a time to minimize the amount of
* work done per JS event tick. Individual rows can also use <Incremental>
* to further break up the work and keep the app responsive and improve scroll
* perf if rows get exceedingly complex.
*
* Note that it's possible to scroll faster than rows can be rendered. Instead
* of showing the user a bunch of un-mounted blank space, WLV sets contentInset
* to prevent scrolling into unrendered areas. Supply the
* `renderWindowBoundaryIndicator` prop to indicate the boundary to the user,
* e.g. with a row placeholder.
*/
type Props = {
/**
* A simple array of data blobs that are passed to the renderRow function in
* order. Note there is no dataSource like in the standard `ListView`.
*/
data: Array<{rowKey: string, rowData: any}>;
/**
* Takes a data blob from the `data` array prop plus some meta info and should
* return a row.
*/
renderRow: (
rowData: any, sectionIdx: number, rowIdx: number, rowKey: string
) => ?ReactElement;
/**
* Rendered when the list is scrolled faster than rows can be rendered.
*/
renderWindowBoundaryIndicator?: () => ?ReactElement;
/**
* Always rendered at the bottom of all the rows.
*/
renderFooter?: () => ?ReactElement;
/**
* Pipes through normal onScroll events from the underlying `ScrollView`.
*/
onScroll?: (event: Object) => void;
/**
* Called when the rows that are visible in the viewport change.
*/
onVisibleRowsChanged?: (firstIdx: number, count: number) => void;
/**
* Called when the viewability of rows changes, as defined by the
* `viewablePercentThreshold` prop.
*/
onViewableRowsChanged?: (viewableRows: Array<number>) => void;
/**
* The percent of a row that must be visible to consider it "viewable".
*/
viewablePercentThreshold: number;
/**
* Number of rows to render on first mount.
*/
initialNumToRender: number;
/**
* Maximum number of rows to render while scrolling, i.e. the window size.
*/
maxNumToRender: number;
/**
* Number of rows to render beyond the viewport. Note that this combined with
* `maxNumToRender` and the number of rows that can fit in one screen will
* determine how many rows to render above the viewport.
*/
numToRenderAhead: number;
/**
* Used to log perf events for async row rendering.
*/
asyncRowPerfEventName?: string;
/**
* A function that returns the scrollable component in which the list rows
* are rendered. Defaults to returning a ScrollView with the given props.
*/
renderScrollComponent: (props: ?Object) => ReactElement;
/**
* Use to disable incremental rendering when not wanted, e.g. to speed up initial render.
*/
disableIncrementalRendering: boolean;
/**
* This determines how frequently events such as scroll and layout can trigger a re-render.
*/
recomputeRowsBatchingPeriod: number;
/**
* Called when rows will be mounted/unmounted. Mounted rows always form a contiguous block so it is expressed as a
* range of start plus count.
*/
onMountedRowsWillChange?: (firstIdx: number, count: number) => void;
/**
* Change this when you want to make sure the WindowedListView will re-render, for example when the result of
* `renderScrollComponent` might change. It will be compared in `shouldComponentUpdate`.
*/
shouldUpdateToken?: string;
};
type State = {
boundaryIndicatorHeight?: number;
firstRow: number;
lastRow: number;
};
class WindowedListView extends React.Component {
props: Props;
state: State;
_firstVisible: number = -1;
_lastVisible: number = -1;
_scrollOffsetY: number = 0;
_isScrolling: boolean = false;
_frameHeight: number = 0;
_rowFrames: {[key: string]: Object} = {};
_rowRenderMode: {[key: string]: null | 'async' | 'sync'} = {};
_rowFramesDirty: boolean = false;
_hasCalledOnEndReached: boolean = false;
_willComputeRowsToRender: boolean = false;
_timeoutHandle: number = 0;
_incrementPending: boolean = false;
_viewableRows: Array<number> = [];
_cellsInProgress: Set<string> = new Set();
_scrollRef: ?Object;
static defaultProps = {
initialNumToRender: 10,
maxNumToRender: 30,
numToRenderAhead: 10,
viewablePercentThreshold: 50,
renderScrollComponent: (props) => <ScrollView {...props} />,
disableIncrementalRendering: false,
recomputeRowsBatchingPeriod: 10, // This should capture most events that will happen in one frame
};
constructor(props: Props) {
super(props);
invariant(
this.props.numToRenderAhead < this.props.maxNumToRender,
'WindowedListView: numToRenderAhead must be less than maxNumToRender'
);
this.state = {
firstRow: 0,
lastRow: Math.min(this.props.data.length, this.props.initialNumToRender) - 1,
};
}
getScrollResponder(): ?ReactComponent {
return this._scrollRef &&
this._scrollRef.getScrollResponder &&
this._scrollRef.getScrollResponder();
}
shouldComponentUpdate(newProps: Props, newState: State): boolean {
if (newState !== this.state) {
return true;
}
for (const key in newProps) {
if (key !== 'data' && newProps[key] !== this.props[key]) {
return true;
}
}
const newDataSubset = newProps.data.slice(newState.firstRow, newState.lastRow + 1);
const prevDataSubset = this.props.data.slice(this.state.firstRow, this.state.lastRow + 1);
if (newDataSubset.length !== prevDataSubset.length) {
return true;
}
for (let idx = 0; idx < newDataSubset.length; idx++) {
if (newDataSubset[idx] !== prevDataSubset[idx]) {
return true;
}
}
return false;
}
componentWillReceiveProps(newProps: Object) {
// This has to happen immediately otherwise we could crash, e.g. if the data
// array has gotten shorter.
this._computeRowsToRender(newProps);
}
_onMomentumScrollEnd = (e: Object) => {
this._onScroll(e);
};
_onScroll = (e: Object) => {
const newScrollY = e.nativeEvent.contentOffset.y;
this._isScrolling = this._scrollOffsetY !== newScrollY;
this._scrollOffsetY = newScrollY;
this._frameHeight = e.nativeEvent.layoutMeasurement.height;
// We don't want to enqueue any updates if any cells are in the middle of an incremental render,
// because it would just be wasted work.
if (this._cellsInProgress.size === 0) {
this._enqueueComputeRowsToRender();
}
if (this.props.onViewableRowsChanged && Object.keys(this._rowFrames).length) {
const viewableRows = ViewabilityHelper.computeViewableRows(
this.props.viewablePercentThreshold,
this._rowFrames,
this.props.data,
e.nativeEvent.contentOffset.y,
e.nativeEvent.layoutMeasurement.height
);
if (deepDiffer(viewableRows, this._viewableRows)) {
this._viewableRows = viewableRows;
nullthrows(this.props.onViewableRowsChanged)(this._viewableRows);
}
}
this.props.onScroll && this.props.onScroll(e);
};
// Caller does the diffing so we don't have to.
_onNewLayout = (params: {rowKey: string, layout: Object}) => {
const {rowKey, layout} = params;
if (DEBUG) {
const layoutPrev = this._rowFrames[rowKey] || {};
infoLog(
'record layout for row: ',
{k: rowKey, h: layout.height, y: layout.y, x: layout.x, hp: layoutPrev.height, yp: layoutPrev.y}
);
}
if (this._rowFrames[rowKey]) {
const deltaY = Math.abs(this._rowFrames[rowKey].y - layout.y);
const deltaH = Math.abs(this._rowFrames[rowKey].height - layout.height);
if (deltaY > 2 || deltaH > 2) {
const dataEntry = this.props.data.find((datum) => datum.rowKey === rowKey);
console.warn('layout jump: ', {dataEntry, prevLayout: this._rowFrames[rowKey], newLayout: layout});
}
}
this._rowFrames[rowKey] = {...layout, offscreenLayoutDone: true};
this._rowFramesDirty = true;
if (this._cellsInProgress.size === 0) {
this._enqueueComputeRowsToRender();
}
};
_onWillUnmountCell = (rowKey: string) => {
if (this._rowFrames[rowKey]) {
this._rowFrames[rowKey].offscreenLayoutDone = false;
this._rowRenderMode[rowKey] = null;
}
};
/**
* This is used to keep track of cells that are in the process of rendering. If any cells are in progress, then
* other updates are skipped because they will just be wasted work.
*/
_onProgressChange = ({rowKey, inProgress}: {rowKey: string, inProgress: boolean}) => {
if (inProgress) {
this._cellsInProgress.add(rowKey);
} else {
this._cellsInProgress.delete(rowKey);
}
};
/**
* Recomputing which rows to render is batched up and run asynchronously to avoid wastful updates, e.g. from multiple
* layout updates in rapid succession.
*/
_enqueueComputeRowsToRender(): void {
if (!this._willComputeRowsToRender) {
this._willComputeRowsToRender = true; // batch up computations
clearTimeout(this._timeoutHandle);
this._timeoutHandle = setTimeout(
() => {
this._willComputeRowsToRender = false;
this._incrementPending = false;
this._computeRowsToRender(this.props);
},
this.props.recomputeRowsBatchingPeriod
);
}
}
componentWillUnmount() {
clearTimeout(this._timeoutHandle);
}
_computeRowsToRender(props: Object): void {
const totalRows = props.data.length;
if (totalRows === 0) {
this._updateVisibleRows(-1, -1);
this.setState({
firstRow: 0,
lastRow: -1,
});
return;
}
const rowFrames = this._rowFrames;
let firstVisible = -1;
let lastVisible = 0;
let lastRow = this.state.lastRow;
const top = this._scrollOffsetY;
const bottom = top + this._frameHeight;
for (let idx = 0; idx < lastRow; idx++) {
const frame = rowFrames[props.data[idx].rowKey];
if (!frame) {
// No frame - sometimes happens when they come out of order, so just wait for the rest.
return;
}
if (((frame.y + frame.height) > top) && (firstVisible < 0)) {
firstVisible = idx;
}
if (frame.y < bottom) {
lastVisible = idx;
} else {
break;
}
}
if (firstVisible === -1) {
firstVisible = 0;
}
this._updateVisibleRows(firstVisible, lastVisible);
// Unfortuantely, we can't use <Incremental> to simplify our increment logic in this function because we need to
// make sure that cells are rendered in the right order one at a time when scrolling back up.
const numRendered = lastRow - this.state.firstRow + 1;
// Our last row target that we will approach incrementally
const targetLastRow = clamp(
numRendered - 1, // Don't reduce numRendered when scrolling back up
lastVisible + props.numToRenderAhead, // Primary goal
totalRows - 1, // Don't render past the end
);
// Increment the last row one at a time per JS event loop
if (!this._incrementPending) {
if (targetLastRow > this.state.lastRow) {
lastRow++;
this._incrementPending = true;
} else if (targetLastRow < this.state.lastRow) {
lastRow--;
this._incrementPending = true;
}
}
// Once last row is set, figure out the first row
const firstRow = Math.max(
0, // Don't render past the top
lastRow - props.maxNumToRender + 1, // Don't exceed max to render
lastRow - numRendered, // Don't render more than 1 additional row
);
if (lastRow >= totalRows) {
// It's possible that the number of rows decreased by more than one
// increment could compensate for. Need to make sure we don't render more
// than one new row at a time, but don't want to render past the end of
// the data.
lastRow = totalRows - 1;
}
if (props.onEndReached) {
// Make sure we call onEndReached exactly once every time we reach the
// end. Resets if scoll back up and down again.
const willBeAtTheEnd = lastRow === (totalRows - 1);
if (willBeAtTheEnd && !this._hasCalledOnEndReached) {
props.onEndReached();
this._hasCalledOnEndReached = true;
} else {
// If lastRow is changing, reset so we can call onEndReached again
this._hasCalledOnEndReached = this.state.lastRow === lastRow;
}
}
const rowsShouldChange = firstRow !== this.state.firstRow || lastRow !== this.state.lastRow;
if (this._rowFramesDirty || rowsShouldChange) {
if (rowsShouldChange) {
props.onMountedRowsWillChange && props.onMountedRowsWillChange(firstRow, lastRow - firstRow + 1);
infoLog(
'WLV: row render range will change:',
{firstRow, firstVis: this._firstVisible, lastVis: this._lastVisible, lastRow},
);
}
this._rowFramesDirty = false;
this.setState({firstRow, lastRow});
}
}
_updateVisibleRows(newFirstVisible: number, newLastVisible: number) {
if (this.props.onVisibleRowsChanged) {
if (this._firstVisible !== newFirstVisible ||
this._lastVisible !== newLastVisible) {
this.props.onVisibleRowsChanged(newFirstVisible, newLastVisible - newFirstVisible + 1);
}
}
this._firstVisible = newFirstVisible;
this._lastVisible = newLastVisible;
}
render(): ReactElement {
const {firstRow, lastRow} = this.state;
const rowFrames = this._rowFrames;
const rows = [];
let spacerHeight = 0;
// Incremental rendering is a tradeoff between throughput and responsiveness. When we have plenty of buffer (say 50%
// of the target), we render incrementally to keep the app responsive. If we are dangerously low on buffer (say
// below 25%) we always disable incremental to try to catch up as fast as possible. In the middle, we only disable
// incremental while scrolling since it's unlikely the user will try to press a button while scrolling. We also
// ignore the "buffer" size when we are bumped up against the edge of the available data.
const firstBuffer = firstRow === 0 ? Infinity : this._firstVisible - firstRow;
const lastBuffer = lastRow === this.props.data.length - 1 ? Infinity : lastRow - this._lastVisible;
const minBuffer = Math.min(firstBuffer, lastBuffer);
const disableIncrementalRendering = this.props.disableIncrementalRendering ||
(this._isScrolling && minBuffer < this.props.numToRenderAhead * 0.5) ||
(minBuffer < this.props.numToRenderAhead * 0.25);
// Render mode is sticky while the component is mounted.
for (let ii = firstRow; ii <= lastRow; ii++) {
const rowKey = this.props.data[ii].rowKey;
if (this._rowRenderMode[rowKey] === 'sync' || (disableIncrementalRendering && this._rowRenderMode[rowKey] !== 'async')) {
this._rowRenderMode[rowKey] = 'sync';
} else {
this._rowRenderMode[rowKey] = 'async';
}
}
for (let ii = firstRow; ii <= lastRow; ii++) {
const rowKey = this.props.data[ii].rowKey;
if (!rowFrames[rowKey]) {
break; // if rowFrame missing, no following ones will exist so quit early
}
// Look for the first row where offscreen layout is done (only true for mounted rows) or it will be rendered
// synchronously and set the spacer height such that it will offset all the unmounted rows before that one using
// the saved frame data.
if (rowFrames[rowKey].offscreenLayoutDone || this._rowRenderMode[rowKey] === 'sync') {
if (ii > 0) {
const prevRowKey = this.props.data[ii - 1].rowKey;
const frame = rowFrames[prevRowKey];
spacerHeight = frame ? frame.y + frame.height : 0;
}
break;
}
}
let showIndicator = false;
if (spacerHeight > (this.state.boundaryIndicatorHeight || 0) && this.props.renderWindowBoundaryIndicator) {
showIndicator = true;
spacerHeight -= this.state.boundaryIndicatorHeight || 0;
}
DEBUG && infoLog('render top spacer with height ', spacerHeight);
rows.push(<View key="sp-top" style={{height: spacerHeight}} />);
if (this.props.renderWindowBoundaryIndicator) {
// Always render it, even if removed, so that we can get the height right away and don't waste time creating/
// destroying it. Should see if there is a better spinner option that is not as expensive.
rows.push(
<View
style={!showIndicator && styles.remove}
key="ind-top"
onLayout={(e) => {
const layout = e.nativeEvent.layout;
if (layout.height !== this.state.boundaryIndicatorHeight) {
this.setState({boundaryIndicatorHeight: layout.height});
}
}}>
{this.props.renderWindowBoundaryIndicator()}
</View>
);
}
for (let idx = firstRow; idx <= lastRow; idx++) {
const rowKey = this.props.data[idx].rowKey;
const includeInLayout = this._rowRenderMode[rowKey] === 'sync' ||
(this._rowFrames[rowKey] && this._rowFrames[rowKey].offscreenLayoutDone);
rows.push(
<CellRenderer
key={rowKey}
rowKey={rowKey}
rowIndex={idx}
onNewLayout={this._onNewLayout}
onWillUnmount={this._onWillUnmountCell}
includeInLayout={includeInLayout}
onProgressChange={this._onProgressChange}
asyncRowPerfEventName={this.props.asyncRowPerfEventName}
rowData={this.props.data[idx].rowData}
renderRow={this.props.renderRow}
/>
);
}
const lastRowKey = this.props.data[lastRow].rowKey;
const showFooter = this._rowFrames[lastRowKey] &&
this._rowFrames[lastRowKey].offscreenLayoutDone &&
lastRow === this.props.data.length - 1;
if (this.props.renderFooter) {
rows.push(
<View
key="ind-footer"
style={showFooter ? styles.include : styles.remove}>
{this.props.renderFooter()}
</View>
);
}
if (this.props.renderWindowBoundaryIndicator) {
rows.push(
<View
key="ind-bot"
style={showFooter ? styles.remove : styles.include}
onLayout={(e) => {
const layout = e.nativeEvent.layout;
if (layout.height !== this.state.boundaryIndicatorHeight) {
this.setState({boundaryIndicatorHeight: layout.height});
}
}}>
{this.props.renderWindowBoundaryIndicator()}
</View>
);
}
// Prevent user from scrolling into empty space of unmounted rows.
const contentInset = {top: firstRow === 0 ? 0 : -spacerHeight};
return (
this.props.renderScrollComponent({
scrollEventThrottle: 50,
removeClippedSubviews: true,
...this.props,
contentInset,
ref: (ref) => { this._scrollRef = ref; },
onScroll: this._onScroll,
onMomentumScrollEnd: this._onMomentumScrollEnd,
children: rows,
})
);
}
}
// performance testing id, unique for each component mount cycle
let g_perf_update_id = 0;
type CellProps = {
/**
* Row-specific data passed to renderRow and used in shouldComponentUpdate with ===
*/
rowData: mixed;
rowKey: string;
/**
* Renders the actual row contents.
*/
renderRow: (
rowData: mixed, sectionIdx: number, rowIdx: number, rowKey: string
) => ?ReactElement;
/**
* Index of the row, passed through to other callbacks.
*/
rowIndex: number;
/**
* Used for marking async begin/end events for row rendering.
*/
asyncRowPerfEventName: ?string;
/**
* Initially false to indicate the cell should be rendered "offscreen" with position: absolute so that incremental
* rendering doesn't cause things to jump around. Once onNewLayout is called after offscreen rendering has completed,
* includeInLayout will be set true and the finished cell can be dropped into place.
*
* This is coordinated outside this component so the parent can syncronize this re-render with managing the
* placeholder sizing.
*/
includeInLayout: boolean;
/**
* Updates the parent with the latest layout. Only called when incremental rendering is done and triggers the parent
* to re-render this row with includeInLayout true.
*/
onNewLayout: (params: {rowKey: string, layout: Object}) => void;
/**
* Used to track when rendering is in progress so the parent can avoid wastedful re-renders that are just going to be
* invalidated once the cell finishes.
*/
onProgressChange: (progress: {rowKey: string; inProgress: boolean}) => void;
/**
* Used to invalidate the layout so the parent knows it needs to compensate for the height in the placeholder size.
*/
onWillUnmount: (rowKey: string) => void;
};
class CellRenderer extends React.Component {
props: CellProps;
_containerRef: View;
_offscreenRenderDone = false;
_timeout = 0;
_lastLayout: ?Object = null;
_perfUpdateID: number = 0;
_asyncCookie: any;
_includeInLayoutLatch: boolean = false;
componentWillMount() {
if (this.props.asyncRowPerfEventName) {
this._perfUpdateID = g_perf_update_id++;
this._asyncCookie = Systrace.beginAsyncEvent(this.props.asyncRowPerfEventName + this._perfUpdateID);
infoLog(`perf_asynctest_${this.props.asyncRowPerfEventName}_start ${this._perfUpdateID} ${Date.now()}`);
}
if (this.props.includeInLayout) {
this._includeInLayoutLatch = true;
}
this.props.onProgressChange({rowKey: this.props.rowKey, inProgress: true});
}
_onLayout = (e) => {
const layout = e.nativeEvent.layout;
const layoutChanged = deepDiffer(this._lastLayout, layout);
this._lastLayout = layout;
if (!this._offscreenRenderDone || !layoutChanged) {
return; // Don't send premature or duplicate updates
}
this.props.onNewLayout({
rowKey: this.props.rowKey,
layout,
});
};
_updateParent() {
invariant(!this._offscreenRenderDone, 'should only finish rendering once');
this._offscreenRenderDone = true;
// If this is not called before calling onNewLayout, the number of inProgress cells will remain non-zero,
// and thus the onNewLayout call will not fire the needed state change update.
this.props.onProgressChange({rowKey: this.props.rowKey, inProgress: false});
// If an onLayout event hasn't come in yet, then we skip here and assume it will come in later. This happens
// when Incremental is disabled and _onOffscreenRenderDone is called faster than layout can happen.
this._lastLayout && this.props.onNewLayout({rowKey: this.props.rowKey, layout: this._lastLayout});
DEBUG && infoLog('\n >>>>> display row ' + this.props.rowIndex + '\n\n\n');
if (this.props.asyncRowPerfEventName) {
// Note this doesn't include the native render time but is more accurate than also including the JS render
// time of anything that has been queued up.
Systrace.endAsyncEvent(this.props.asyncRowPerfEventName + this._perfUpdateID, this._asyncCookie);
infoLog(`perf_asynctest_${this.props.asyncRowPerfEventName}_end ${this._perfUpdateID} ${Date.now()}`);
}
}
_onOffscreenRenderDone = () => {
DEBUG && infoLog('_onOffscreenRenderDone for row ' + this.props.rowIndex);
if (this._includeInLayoutLatch) {
this._updateParent(); // rendered straight into layout, so no need to flush
} else {
this._timeout = setTimeout(() => this._updateParent(), 1); // Flush any pending layout events.
}
};
componentWillUnmount() {
clearTimeout(this._timeout);
this.props.onProgressChange({rowKey: this.props.rowKey, inProgress: false});
this.props.onWillUnmount(this.props.rowKey);
}
componentWillReceiveProps(newProps) {
if (newProps.includeInLayout && !this.props.includeInLayout) {
invariant(this._offscreenRenderDone, 'Should never try to add to layout before render done');
this._includeInLayoutLatch = true; // Once we render in layout, make sure it sticks.
this._containerRef.setNativeProps({style: styles.include});
}
}
shouldComponentUpdate(newProps: CellProps) {
return newProps.rowData !== this.props.rowData;
}
_setRef = (ref) => {
this._containerRef = ref;
};
render() {
let debug;
if (DEBUG) {
infoLog('render cell ' + this.props.rowIndex);
const Text = require('Text');
debug = <Text style={{backgroundColor: 'lightblue'}}>
Row: {this.props.rowIndex}
</Text>;
}
const style = this._includeInLayoutLatch ? styles.include : styles.remove;
return (
<IncrementalGroup
disabled={this._includeInLayoutLatch}
onDone={this._onOffscreenRenderDone}
name={`WLVCell_${this.props.rowIndex}`}>
<View
ref={this._setRef}
style={style}
onLayout={this._onLayout}>
{debug}
{this.props.renderRow(this.props.rowData, 0, this.props.rowIndex, this.props.rowKey)}
{debug}
</View>
</IncrementalGroup>
);
}
}
const removedXOffset = DEBUG ? 123 : 0;
const styles = StyleSheet.create({
include: {
position: 'relative',
left: 0,
right: 0,
opacity: 1,
},
remove: {
position: 'absolute',
left: removedXOffset,
right: -removedXOffset,
opacity: DEBUG ? 0.1 : 0,
},
});
module.exports = WindowedListView;