diff --git a/Libraries/Experimental/Incremental.js b/Libraries/Experimental/Incremental.js index 052506cc2..7170c1312 100644 --- a/Libraries/Experimental/Incremental.js +++ b/Libraries/Experimental/Incremental.js @@ -88,7 +88,6 @@ export type Props = { */ name: string; children: any; - disabled: boolean; }; class Incremental extends React.Component { props: Props; @@ -98,6 +97,7 @@ class Incremental extends React.Component { context: Context; _incrementId: number; _mounted: boolean; + _rendered: boolean; constructor(props: Props, context: Context) { super(props, context); @@ -135,8 +135,11 @@ class Incremental extends React.Component { } render(): ?ReactElement { - if (this.props.disabled || !this.context.incrementalGroupEnabled || this.state.doIncrementalRender) { + if (this._rendered || // Make sure that once we render once, we stay rendered even if incrementalGroupEnabled gets flipped. + !this.context.incrementalGroupEnabled || + this.state.doIncrementalRender) { DEBUG && console.log('render ' + this.getName()); + this._rendered = true; return this.props.children; } return null; diff --git a/Libraries/Experimental/ViewabilityHelper.js b/Libraries/Experimental/ViewabilityHelper.js new file mode 100644 index 000000000..e8ced0e61 --- /dev/null +++ b/Libraries/Experimental/ViewabilityHelper.js @@ -0,0 +1,83 @@ +/** + * 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. + * + * @providesModule ViewabilityHelper + * @flow + */ +'use strict'; + +/** +* A row is said to be in a "viewable" state when either of the following +* is true: +* - Occupying >= viewablePercentThreshold of the viewport +* - Entirely visible on screen +*/ +const ViewabilityHelper = { + computeViewableRows( + viewablePercentThreshold: number, + rowFrames: Array, + scrollOffsetY: number, + viewportHeight: number + ): Array { + var viewableRows = []; + var firstVisible = -1; + for (var idx = 0; idx < rowFrames.length; idx++) { + var frame = rowFrames[idx]; + if (!frame) { + continue; + } + var top = frame.y - scrollOffsetY; + var bottom = top + frame.height; + if ((top < viewportHeight) && (bottom > 0)) { + firstVisible = idx; + if (_isViewable( + viewablePercentThreshold, + top, + bottom, + viewportHeight + )) { + viewableRows.push(idx); + } + } else if (firstVisible >= 0) { + break; + } + } + return viewableRows; + }, +}; + +function _isViewable( + viewablePercentThreshold: number, + top: number, + bottom: number, + viewportHeight: number +): bool { + return _isEntirelyVisible(top, bottom, viewportHeight) || + _getPercentOccupied(top, bottom, viewportHeight) >= + viewablePercentThreshold; +} + +function _getPercentOccupied( + top: number, + bottom: number, + viewportHeight: number +): number { + var visibleHeight = Math.min(bottom, viewportHeight) - Math.max(top, 0); + visibleHeight = Math.max(0, visibleHeight); + return Math.max(0, visibleHeight * 100 / viewportHeight); +} + +function _isEntirelyVisible( + top: number, + bottom: number, + viewportHeight: number +): bool { + return top >= 0 && bottom <= viewportHeight && bottom > top; +} + +module.exports = ViewabilityHelper; diff --git a/Libraries/Experimental/WindowedListView.js b/Libraries/Experimental/WindowedListView.js new file mode 100644 index 000000000..973b93f16 --- /dev/null +++ b/Libraries/Experimental/WindowedListView.js @@ -0,0 +1,643 @@ +/** + * 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 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 + * 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; + /** + * Takes a data blob from the `data` array prop plus some meta info and should + * return a row. + */ + renderRow: ( + data: mixed, sectionIdx: number, rowIdx: number, key: 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) => 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; + /** + * Super dangerous and experimental - rows and all their decendents must be + * fully stateless otherwise recycling their instances may introduce nasty + * bugs. Some apps may see an improvement in perf, but sometimes perf and + * memory usage can actually get worse with this. + */ + enableDangerousRecycling: boolean; + /** + * 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; +}; +const defaultProps = { + enableDangerousRecycling: false, + initialNumToRender: 10, + maxNumToRender: 30, + numToRenderAhead: 10, + viewablePercentThreshold: 50, + renderScrollComponent: (props) => , + disableIncrementalRendering: false, + recomputeRowsBatchingPeriod: 100, +}; +class WindowedListView extends React.Component { + props: Props; + state: { + boundaryIndicatorHeight?: number; + firstRow: number; + lastRow: number; + firstVisible: number; + lastVisible: number; + }; + _scrollOffsetY: number = 0; + _frameHeight: number = 0; + _rowFrames: Array = []; + _hasCalledOnEndReached: bool = false; + _willComputeRowsToRender: bool = false; + _timeoutHandle: number = 0; + _incrementPending: bool = false; + _viewableRows: Array = []; + _cellsInProgress: Set = new Set(); + _scrollRef: ?Object; + 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, + firstVisible: -1, + lastVisible: -1, + }; + } + getScrollResponder(): ?ReactComponent { + return this._scrollRef && + this._scrollRef.getScrollResponder && + this._scrollRef.getScrollResponder(); + } + componentWillReceiveProps(newProps: Object) { + // This has to happen immediately otherwise we could crash, e.g. if the data + // array has gotten shorter. + if (newProps.data.length < this._rowFrames.length) { + this._rowFrames = this._rowFrames.splice(0, newProps.data.length); + } + this._computeRowsToRender(newProps); + } + _onScroll = (e: Object) => { + this._scrollOffsetY = e.nativeEvent.contentOffset.y; + 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 && this._rowFrames.length) { + const viewableRows = ViewabilityHelper.computeViewableRows( + this.props.viewablePercentThreshold, + this._rowFrames, + 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: {rowIndex: number, layout: Object}) => { + const {rowIndex, layout} = params; + if (DEBUG) { + const layoutPrev = this._rowFrames[rowIndex] || {}; + console.log( + 'record layout for row: ', + {i: rowIndex, h: layout.height, y: layout.y, hp: layoutPrev.height, yp: layoutPrev.y} + ); + } + this._rowFrames[rowIndex] = {...layout, offscreenLayoutDone: true}; + if (this._cellsInProgress.size === 0) { + this._enqueueComputeRowsToRender(); + } + }; + _onWillUnmountCell = (rowIndex: number) => { + if (this._rowFrames[rowIndex]) { + this._rowFrames[rowIndex].offscreenLayoutDone = false; + } + }; + /** + * 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 = ({rowIndex, inProgress}: {rowIndex: number, inProgress: boolean}) => { + if (inProgress) { + this._cellsInProgress.add(rowIndex); + } else { + this._cellsInProgress.delete(rowIndex); + } + }; + /** + * 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.setState({ + firstRow: 0, + lastRow: -1, + firstVisible: -1, + lastVisible: -1, + }); + return; + } + const rowFrames = this._rowFrames; + let firstVisible = -1; + let lastVisible = 0; + const top = this._scrollOffsetY; + const bottom = top + this._frameHeight; + for (let idx = 0; idx < rowFrames.length; idx++) { + const frame = rowFrames[idx]; + 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 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 = this.state.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 + ); + let lastRow = this.state.lastRow; + // 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; + } + } + if (this.state.firstRow !== firstRow || this.state.lastRow !== lastRow) { + console.log('WLV: row render range changed:', {firstRow, lastRow}); + } + this.setState({firstRow, lastRow}); + } + _updateVisibleRows(newFirstVisible: number, newLastVisible: number) { + if (this.state.firstVisible !== newFirstVisible || + this.state.lastVisible !== newLastVisible) { + if (this.props.onVisibleRowsChanged) { + this.props.onVisibleRowsChanged( + newFirstVisible, + newLastVisible - newFirstVisible + 1); + } + this.setState({ + firstVisible: newFirstVisible, + lastVisible: newLastVisible, + }); + } + } + render(): ReactElement { + const firstRow = this.state.firstRow; + const lastRow = this.state.lastRow; + const rowFrames = this._rowFrames; + const rows = []; + let spacerHeight = 0; + for (let ii = firstRow; ii <= lastRow; ii++) { + if (!rowFrames[ii]) { + 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) and set the spacer height + // such that it will offset all the unmounted rows before that one using the saved frame data. + if (rowFrames[ii].offscreenLayoutDone) { + const frame = rowFrames[ii - 1]; + 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 && console.log('render top spacer with height ', spacerHeight); + rows.push(); + 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( + { + const layout = e.nativeEvent.layout; + if (layout.height !== this.state.boundaryIndicatorHeight) { + this.setState({boundaryIndicatorHeight: layout.height}); + } + }}> + {this.props.renderWindowBoundaryIndicator()} + + ); + } + for (let idx = firstRow; idx <= lastRow; idx++) { + const key = '' + (this.props.enableDangerousRecycling ? (idx % this.props.maxNumToRender) : idx); + rows.push( + + ); + } + const showFooter = this._rowFrames[lastRow] && + this._rowFrames[lastRow].offscreenLayoutDone && + lastRow === this.props.data.length - 1; + if (this.props.renderFooter) { + rows.push( + + {this.props.renderFooter()} + + ); + } + if (this.props.renderWindowBoundaryIndicator) { + rows.push( + { + const layout = e.nativeEvent.layout; + if (layout.height !== this.state.boundaryIndicatorHeight) { + this.setState({boundaryIndicatorHeight: layout.height}); + } + }}> + {this.props.renderWindowBoundaryIndicator()} + + ); + } + // 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, + children: rows, + })} + + ); + } +} +WindowedListView.defaultProps = defaultProps; + +// 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 === + */ + data: mixed; + /** + * Renders the actual row contents. + */ + renderRow: (data: mixed, sectionIdx: number, rowIdx: number) => ?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: {rowIndex: number, 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: {rowIndex: number; inProgress: boolean}) => void; + /** + * Used to invalidate the layout so the parent knows it needs to compensate for the height in the placeholder size. + */ + onWillUnmount: (rowIndex: number) => void; +}; +class CellRenderer extends React.Component { + props: CellProps; + _offscreenRenderDone = false; + _timer = 0; + _lastLayout: ?Object = null; + _perfUpdateID: number = 0; + _asyncCookie: any; + componentWillMount() { + if (this.props.asyncRowPerfEventName) { + this._perfUpdateID = g_perf_update_id++; + this._asyncCookie = Systrace.beginAsyncEvent(this.props.asyncRowPerfEventName + this._perfUpdateID); + console.log(`perf_asynctest_${this.props.asyncRowPerfEventName}_start ${this._perfUpdateID} ${Date.now()}`); + } + this.props.onProgressChange({rowIndex: this.props.rowIndex, 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({ + rowIndex: this.props.rowIndex, + layout, + }); + }; + _onOffscreenRenderDone = () => { + DEBUG && console.log('_onOffscreenRenderDone for row ' + this.props.rowIndex); + this._timer = setTimeout(() => { // Flush any pending layout events. + 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({rowIndex: this.props.rowIndex, 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({rowIndex: this.props.rowIndex, layout: this._lastLayout}); + + DEBUG && console.log('\n >>>>> display row ' + this.props.rowIndex + '\n\n\n'); + if (this.props.asyncRowPerfEventName) { + Systrace.endAsyncEvent(this.props.asyncRowPerfEventName + this._perfUpdateID, this._asyncCookie); + console.log(`perf_asynctest_${this.props.asyncRowPerfEventName}_end ${this._perfUpdateID} ${Date.now()}`); + } + }, 1); + }; + componentWillUnmount() { + clearTimeout(this._timer); + this.props.onProgressChange({rowIndex: this.props.rowIndex, inProgress: false}); + this.props.onWillUnmount(this.props.rowIndex); + } + componentWillReceiveProps(newProps) { + if (newProps.includeInLayout && !this.props.includeInLayout) { + invariant(this._offscreenRenderDone, 'Should never try to add to layout before render done'); + this.refs.container.setNativeProps({style: styles.include}); + } else { + invariant(!(this.props.includeInLayout && !newProps.includeInLayout), 'Should never unset includeInLayout'); + } + } + shouldComponentUpdate(newProps) { + return newProps.data !== this.props.data; + } + render() { + let debug; + if (DEBUG) { + console.log('render cell ' + this.props.rowIndex); + const Text = require('Text'); + debug = + Row: {this.props.rowIndex} + ; + } + const style = this.props.includeInLayout ? styles.include : styles.remove; + return ( + + + {debug} + {this.props.renderRow(this.props.data, 0, this.props.rowIndex)} + {debug} + + + ); + } +} + +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;