initial commit taken from gitlab.lrz.de

This commit is contained in:
privatereese
2018-08-24 18:09:42 +02:00
parent ae54ed4c48
commit fc05486403
28494 changed files with 2159823 additions and 0 deletions

View File

@@ -0,0 +1,186 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @providesModule Incremental
* @flow
*/
'use strict';
const InteractionManager = require('InteractionManager');
const React = require('React');
const PropTypes = require('prop-types');
const infoLog = require('infoLog');
const DEBUG = false;
/**
* WARNING: EXPERIMENTAL. Breaking changes will probably happen a lot and will
* not be reliably announced. The whole thing might be deleted, who knows? Use
* at your own risk.
*
* React Native helps make apps smooth by doing all the heavy lifting off the
* main thread, in JavaScript. That works great a lot of the time, except that
* heavy operations like rendering may block the JS thread from responding
* quickly to events like taps, making the app feel sluggish.
*
* `<Incremental>` solves this by slicing up rendering into chunks that are
* spread across multiple event loops. Expensive components can be sliced up
* recursively by wrapping pieces of them and their descendants in
* `<Incremental>` components. `<IncrementalGroup>` can be used to make sure
* everything in the group is rendered recursively before calling `onDone` and
* moving on to another sibling group (e.g. render one row at a time, even if
* rendering the top level row component produces more `<Incremental>` chunks).
* `<IncrementalPresenter>` is a type of `<IncrementalGroup>` that keeps it's
* children invisible and out of the layout tree until all rendering completes
* recursively. This means the group will be presented to the user as one unit,
* rather than pieces popping in sequentially.
*
* `<Incremental>` only affects initial render - `setState` and other render
* updates are unaffected.
*
* The chunks are rendered sequentially using the `InteractionManager` queue,
* which means that rendering will pause if it's interrupted by an interaction,
* such as an animation or gesture.
*
* Note there is some overhead, so you don't want to slice things up too much.
* A target of 100-200ms of total work per event loop on old/slow devices might
* be a reasonable place to start.
*
* Below is an example that will incrementally render all the parts of `Row` one
* first, then present them together, then repeat the process for `Row` two, and
* so on:
*
* render: function() {
* return (
* <ScrollView>
* {Array(10).fill().map((rowIdx) => (
* <IncrementalPresenter key={rowIdx}>
* <Row>
* {Array(20).fill().map((widgetIdx) => (
* <Incremental key={widgetIdx}>
* <SlowWidget />
* </Incremental>
* ))}
* </Row>
* </IncrementalPresenter>
* ))}
* </ScrollView>
* );
* };
*
* If SlowWidget takes 30ms to render, then without `Incremental`, this would
* block the JS thread for at least `10 * 20 * 30ms = 6000ms`, but with
* `Incremental` it will probably not block for more than 50-100ms at a time,
* allowing user interactions to take place which might even unmount this
* component, saving us from ever doing the remaining rendering work.
*/
export type Props = {
/**
* Called when all the descendants have finished rendering and mounting
* recursively.
*/
onDone?: () => void,
/**
* Tags instances and associated tasks for easier debugging.
*/
name: string,
children?: any,
};
type DefaultProps = {
name: string,
};
type State = {
doIncrementalRender: boolean,
};
class Incremental extends React.Component<Props, State> {
props: Props;
state: State;
context: Context;
_incrementId: number;
_mounted: boolean;
_rendered: boolean;
static defaultProps = {
name: '',
};
static contextTypes = {
incrementalGroup: PropTypes.object,
incrementalGroupEnabled: PropTypes.bool,
};
constructor(props: Props, context: Context) {
super(props, context);
this._mounted = false;
this.state = {
doIncrementalRender: false,
};
}
getName(): string {
const ctx = this.context.incrementalGroup || {};
return ctx.groupId + ':' + this._incrementId + '-' + this.props.name;
}
UNSAFE_componentWillMount() {
const ctx = this.context.incrementalGroup;
if (!ctx) {
return;
}
this._incrementId = ++(ctx.incrementalCount);
InteractionManager.runAfterInteractions({
name: 'Incremental:' + this.getName(),
gen: () => new Promise(resolve => {
if (!this._mounted || this._rendered) {
resolve();
return;
}
DEBUG && infoLog('set doIncrementalRender for ' + this.getName());
this.setState({doIncrementalRender: true}, resolve);
}),
}).then(() => {
DEBUG && infoLog('call onDone for ' + this.getName());
this._mounted && this.props.onDone && this.props.onDone();
}).catch((ex) => {
ex.message = `Incremental render failed for ${this.getName()}: ${ex.message}`;
throw ex;
}).done();
}
render(): React.Node {
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 && infoLog('render ' + this.getName());
this._rendered = true;
return this.props.children;
}
return null;
}
componentDidMount() {
this._mounted = true;
if (!this.context.incrementalGroup) {
this.props.onDone && this.props.onDone();
}
}
componentWillUnmount() {
this._mounted = false;
}
}
export type Context = {
incrementalGroupEnabled: boolean,
incrementalGroup: ?{
groupId: string,
incrementalCount: number,
},
};
module.exports = Incremental;

View File

@@ -0,0 +1,189 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @providesModule IncrementalExample
* @flow
*/
'use strict';
const React = require('react');
const ReactNative = require('react-native');
const {
InteractionManager,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
} = ReactNative;
const Incremental = require('Incremental');
const IncrementalGroup = require('IncrementalGroup');
const IncrementalPresenter = require('IncrementalPresenter');
const JSEventLoopWatchdog = require('JSEventLoopWatchdog');
/* $FlowFixMe(>=0.54.0 site=react_native_oss) This comment suppresses an error
* found when Flow v0.54 was deployed. To see the error delete this comment and
* run Flow. */
const performanceNow = require('fbjs/lib/performanceNow');
InteractionManager.setDeadline(1000);
JSEventLoopWatchdog.install({thresholdMS: 200});
let totalWidgets = 0;
class SlowWidget extends React.Component<$FlowFixMeProps, {ctorTimestamp: number, timeToMount: number}> {
constructor(props, context) {
super(props, context);
this.state = {
ctorTimestamp: performanceNow(),
timeToMount: 0,
};
}
render() {
this.state.timeToMount === 0 && burnCPU(20);
return (
<View style={styles.widgetContainer}>
<Text style={styles.widgetText}>
{`${this.state.timeToMount || '?'} ms`}
</Text>
</View>
);
}
componentDidMount() {
const timeToMount = performanceNow() - this.state.ctorTimestamp;
this.setState({timeToMount});
totalWidgets++;
}
}
let imHandle;
function startInteraction() {
imHandle = InteractionManager.createInteractionHandle();
}
function stopInteraction() {
InteractionManager.clearInteractionHandle(imHandle);
}
function Block(props: Object) {
const IncrementalContainer = props.stream ? IncrementalGroup : IncrementalPresenter;
return (
<IncrementalContainer name={'b_' + props.idx}>
<TouchableOpacity
onPressIn={startInteraction}
onPressOut={stopInteraction}>
<View style={styles.block}>
<Text>
{props.idx + ': ' + (props.stream ? 'Streaming' : 'Presented')}
</Text>
{props.children}
</View>
</TouchableOpacity>
</IncrementalContainer>
);
}
const Row = (props: Object) => <View style={styles.row} {...props} />;
class IncrementalExample extends React.Component<mixed, {stats: ?Object}> {
static title = '<Incremental*>';
static description = 'Enables incremental rendering of complex components.';
start: number;
constructor(props: mixed, context: mixed) {
super(props, context);
this.start = performanceNow();
this.state = {
stats: null,
};
(this: any)._onDone = this._onDone.bind(this);
}
_onDone() {
const onDoneElapsed = performanceNow() - this.start;
setTimeout(() => {
const stats = {
onDoneElapsed,
totalWidgets,
...JSEventLoopWatchdog.getStats(),
setTimeoutElapsed: performanceNow() - this.start,
};
stats.avgStall = stats.totalStallTime / stats.stallCount;
this.setState({stats});
console.log('onDone:', stats);
}, 0);
}
render(): React.Node {
return (
<IncrementalGroup
disabled={false}
name="root"
onDone={this._onDone}>
<ScrollView style={styles.scrollView}>
<Text style={styles.headerText}>
Press and hold on a row to pause rendering.
</Text>
{this.state.stats && <Text>
Finished: {JSON.stringify(this.state.stats, null, 2)}
</Text>}
{Array(8).fill().map((_, blockIdx) => {
return (
<Block key={blockIdx} idx={blockIdx} stream={blockIdx < 2}>
{Array(4).fill().map((_b, rowIdx) => (
<Row key={rowIdx}>
{Array(14).fill().map((_c, widgetIdx) => (
<Incremental key={widgetIdx} name={'w_' + widgetIdx}>
<SlowWidget idx={widgetIdx} />
</Incremental>
))}
</Row>
))}
</Block>
);
})}
</ScrollView>
</IncrementalGroup>
);
}
}
function burnCPU(milliseconds) {
const start = performanceNow();
while (performanceNow() < (start + milliseconds)) {}
}
var styles = StyleSheet.create({
scrollView: {
margin: 10,
backgroundColor: 'white',
flex: 1,
},
headerText: {
fontSize: 20,
margin: 10,
},
block: {
borderRadius: 6,
borderWidth: 2,
borderColor: '#a52a2a',
padding: 14,
margin: 5,
backgroundColor: 'white',
},
row: {
flexDirection: 'row',
},
widgetContainer: {
backgroundColor: '#dddddd',
padding: 2,
margin: 2,
},
widgetText: {
color: 'black',
fontSize: 4,
},
});
module.exports = IncrementalExample;

View File

@@ -0,0 +1,82 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @providesModule IncrementalGroup
* @flow
*/
'use strict';
const Incremental = require('Incremental');
const React = require('React');
const PropTypes = require('prop-types');
const infoLog = require('infoLog');
let _groupCounter = -1;
const DEBUG = false;
import type {Props, Context} from 'Incremental';
/**
* WARNING: EXPERIMENTAL. Breaking changes will probably happen a lot and will
* not be reliably announced. The whole thing might be deleted, who knows? Use
* at your own risk.
*
* `<Incremental>` components must be wrapped in an `<IncrementalGroup>` (e.g.
* via `<IncrementalPresenter>`) in order to provide the incremental group
* context, otherwise they will do nothing.
*
* See Incremental.js for more info.
*/
class IncrementalGroup extends React.Component<Props & {disabled?: boolean}> {
context: Context;
_groupInc: string;
UNSAFE_componentWillMount() {
this._groupInc = `g${++_groupCounter}-`;
DEBUG && infoLog(
'create IncrementalGroup with id ' + this.getGroupId()
);
}
getGroupId(): string {
const ctx = this.context.incrementalGroup;
const prefix = ctx ? ctx.groupId + ':' : '';
return prefix + this._groupInc + this.props.name;
}
getChildContext(): Context {
if (this.props.disabled || this.context.incrementalGroupEnabled === false) {
return {
incrementalGroupEnabled: false,
incrementalGroup: null,
};
}
return {
incrementalGroupEnabled: true,
incrementalGroup: {
groupId: this.getGroupId(),
incrementalCount: -1,
},
};
}
render(): React.Node {
return (
<Incremental
onDone={this.props.onDone}
children={this.props.children}
/>
);
}
}
IncrementalGroup.contextTypes = {
incrementalGroup: PropTypes.object,
incrementalGroupEnabled: PropTypes.bool,
};
IncrementalGroup.childContextTypes = IncrementalGroup.contextTypes;
module.exports = IncrementalGroup;

View File

@@ -0,0 +1,97 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @providesModule IncrementalPresenter
* @flow
*/
'use strict';
const IncrementalGroup = require('IncrementalGroup');
const React = require('React');
const PropTypes = require('prop-types');
const View = require('View');
const ViewPropTypes = require('ViewPropTypes');
import type {Context} from 'Incremental';
/**
* WARNING: EXPERIMENTAL. Breaking changes will probably happen a lot and will
* not be reliably announced. The whole thing might be deleted, who knows? Use
* at your own risk.
*
* `<IncrementalPresenter>` can be used to group sets of `<Incremental>` renders
* such that they are initially invisible and removed from layout until all
* descendants have finished rendering, at which point they are drawn all at once
* so the UI doesn't jump around during the incremental rendering process.
*
* See Incremental.js for more info.
*/
type Props = {
name: string,
disabled?: boolean,
onDone?: () => void,
onLayout?: (event: Object) => void,
style?: mixed,
children?: any,
}
class IncrementalPresenter extends React.Component<Props> {
context: Context;
_isDone: boolean;
static propTypes = {
name: PropTypes.string,
disabled: PropTypes.bool,
onDone: PropTypes.func,
onLayout: PropTypes.func,
style: ViewPropTypes.style,
};
static contextTypes = {
incrementalGroup: PropTypes.object,
incrementalGroupEnabled: PropTypes.bool,
};
constructor(props: Props, context: Context) {
super(props, context);
this._isDone = false;
(this: any).onDone = this.onDone.bind(this);
}
onDone() {
this._isDone = true;
if (this.props.disabled !== true &&
this.context.incrementalGroupEnabled !== false) {
// Avoid expensive re-renders and use setNativeProps
this.refs.view.setNativeProps(
{style: [this.props.style, {opacity: 1, position: 'relative'}]}
);
}
this.props.onDone && this.props.onDone();
}
render() {
if (this.props.disabled !== true &&
this.context.incrementalGroupEnabled !== false &&
!this._isDone) {
var style = [this.props.style, {opacity: 0, position: 'absolute'}];
} else {
var style = this.props.style;
}
return (
<IncrementalGroup
onDone={this.onDone}
name={this.props.name}
disabled={this.props.disabled}>
<View
children={this.props.children}
ref="view"
style={style}
onLayout={this.props.onLayout}
/>
</IncrementalGroup>
);
}
}
module.exports = IncrementalPresenter;

View File

@@ -0,0 +1,187 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @providesModule SwipeableFlatList
* @flow
* @format
*/
'use strict';
import type {Props as FlatListProps} from 'FlatList';
import type {renderItemType} from 'VirtualizedList';
const PropTypes = require('prop-types');
const React = require('React');
const SwipeableRow = require('SwipeableRow');
const FlatList = require('FlatList');
type SwipableListProps = {
/**
* To alert the user that swiping is possible, the first row can bounce
* on component mount.
*/
bounceFirstRowOnMount: boolean,
// Maximum distance to open to after a swipe
maxSwipeDistance: number | (Object => number),
// Callback method to render the view that will be unveiled on swipe
renderQuickActions: renderItemType,
};
type Props<ItemT> = SwipableListProps & FlatListProps<ItemT>;
type State = {
openRowKey: ?string,
};
/**
* A container component that renders multiple SwipeableRow's in a FlatList
* implementation. This is designed to be a drop-in replacement for the
* standard React Native `FlatList`, so use it as if it were a FlatList, but
* with extra props, i.e.
*
* <SwipeableListView renderRow={..} renderQuickActions={..} {..FlatList props} />
*
* SwipeableRow can be used independently of this component, but the main
* benefit of using this component is
*
* - It ensures that at most 1 row is swiped open (auto closes others)
* - It can bounce the 1st row of the list so users know it's swipeable
* - Increase performance on iOS by locking list swiping when row swiping is occurring
* - More to come
*/
class SwipeableFlatList<ItemT> extends React.Component<Props<ItemT>, State> {
props: Props<ItemT>;
state: State;
_flatListRef: ?FlatList<ItemT> = null;
_shouldBounceFirstRowOnMount: boolean = false;
static propTypes = {
...FlatList.propTypes,
/**
* To alert the user that swiping is possible, the first row can bounce
* on component mount.
*/
bounceFirstRowOnMount: PropTypes.bool.isRequired,
// Maximum distance to open to after a swipe
maxSwipeDistance: PropTypes.oneOfType([PropTypes.number, PropTypes.func])
.isRequired,
// Callback method to render the view that will be unveiled on swipe
renderQuickActions: PropTypes.func.isRequired,
};
static defaultProps = {
...FlatList.defaultProps,
bounceFirstRowOnMount: true,
renderQuickActions: () => null,
};
constructor(props: Props<ItemT>, context: any): void {
super(props, context);
this.state = {
openRowKey: null,
};
this._shouldBounceFirstRowOnMount = this.props.bounceFirstRowOnMount;
}
render(): React.Node {
return (
<FlatList
{...this.props}
ref={ref => {
this._flatListRef = ref;
}}
onScroll={this._onScroll}
renderItem={this._renderItem}
/>
);
}
_onScroll = (e): void => {
// Close any opens rows on ListView scroll
if (this.state.openRowKey) {
this.setState({
openRowKey: null,
});
}
this.props.onScroll && this.props.onScroll(e);
};
_renderItem = (info: Object): ?React.Element<any> => {
const slideoutView = this.props.renderQuickActions(info);
const key = this.props.keyExtractor(info.item, info.index);
// If renderQuickActions is unspecified or returns falsey, don't allow swipe
if (!slideoutView) {
return this.props.renderItem(info);
}
let shouldBounceOnMount = false;
if (this._shouldBounceFirstRowOnMount) {
this._shouldBounceFirstRowOnMount = false;
shouldBounceOnMount = true;
}
return (
<SwipeableRow
slideoutView={slideoutView}
isOpen={key === this.state.openRowKey}
maxSwipeDistance={this._getMaxSwipeDistance(info)}
onOpen={() => this._onOpen(key)}
onClose={() => this._onClose(key)}
shouldBounceOnMount={shouldBounceOnMount}
onSwipeEnd={this._setListViewScrollable}
onSwipeStart={this._setListViewNotScrollable}>
{this.props.renderItem(info)}
</SwipeableRow>
);
};
// This enables rows having variable width slideoutView.
_getMaxSwipeDistance(info: Object): number {
if (typeof this.props.maxSwipeDistance === 'function') {
return this.props.maxSwipeDistance(info);
}
return this.props.maxSwipeDistance;
}
_setListViewScrollableTo(value: boolean) {
if (this._flatListRef) {
this._flatListRef.setNativeProps({
scrollEnabled: value,
});
}
}
_setListViewScrollable = () => {
this._setListViewScrollableTo(true);
};
_setListViewNotScrollable = () => {
this._setListViewScrollableTo(false);
};
_onOpen(key: any): void {
this.setState({
openRowKey: key,
});
}
_onClose(key: any): void {
this.setState({
openRowKey: null,
});
}
}
module.exports = SwipeableFlatList;

View File

@@ -0,0 +1,211 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @providesModule SwipeableListView
* @flow
*/
'use strict';
const ListView = require('ListView');
const PropTypes = require('prop-types');
const React = require('React');
const SwipeableListViewDataSource = require('SwipeableListViewDataSource');
const SwipeableRow = require('SwipeableRow');
type DefaultProps = {
bounceFirstRowOnMount: boolean,
renderQuickActions: Function,
};
type Props = {
bounceFirstRowOnMount: boolean,
dataSource: SwipeableListViewDataSource,
maxSwipeDistance: number | (rowData: any, sectionID: string, rowID: string) => number,
onScroll?: ?Function,
renderRow: Function,
renderQuickActions: Function,
};
type State = {
dataSource: Object,
};
/**
* A container component that renders multiple SwipeableRow's in a ListView
* implementation. This is designed to be a drop-in replacement for the
* standard React Native `ListView`, so use it as if it were a ListView, but
* with extra props, i.e.
*
* let ds = SwipeableListView.getNewDataSource();
* ds.cloneWithRowsAndSections(dataBlob, ?sectionIDs, ?rowIDs);
* // ..
* <SwipeableListView renderRow={..} renderQuickActions={..} {..ListView props} />
*
* SwipeableRow can be used independently of this component, but the main
* benefit of using this component is
*
* - It ensures that at most 1 row is swiped open (auto closes others)
* - It can bounce the 1st row of the list so users know it's swipeable
* - More to come
*/
class SwipeableListView extends React.Component<Props, State> {
props: Props;
state: State;
_listViewRef: ?React.Element<any> = null;
_shouldBounceFirstRowOnMount: boolean = false;
static getNewDataSource(): Object {
return new SwipeableListViewDataSource({
getRowData: (data, sectionID, rowID) => data[sectionID][rowID],
getSectionHeaderData: (data, sectionID) => data[sectionID],
rowHasChanged: (row1, row2) => row1 !== row2,
sectionHeaderHasChanged: (s1, s2) => s1 !== s2,
});
}
static propTypes = {
/**
* To alert the user that swiping is possible, the first row can bounce
* on component mount.
*/
bounceFirstRowOnMount: PropTypes.bool.isRequired,
/**
* Use `SwipeableListView.getNewDataSource()` to get a data source to use,
* then use it just like you would a normal ListView data source
*/
dataSource: PropTypes.instanceOf(SwipeableListViewDataSource).isRequired,
// Maximum distance to open to after a swipe
maxSwipeDistance: PropTypes.oneOfType([
PropTypes.number,
PropTypes.func,
]).isRequired,
// Callback method to render the swipeable view
renderRow: PropTypes.func.isRequired,
// Callback method to render the view that will be unveiled on swipe
renderQuickActions: PropTypes.func.isRequired,
};
static defaultProps = {
bounceFirstRowOnMount: false,
renderQuickActions: () => null,
};
constructor(props: Props, context: any): void {
super(props, context);
this._shouldBounceFirstRowOnMount = this.props.bounceFirstRowOnMount;
this.state = {
dataSource: this.props.dataSource,
};
}
UNSAFE_componentWillReceiveProps(nextProps: Props): void {
if (this.state.dataSource.getDataSource() !== nextProps.dataSource.getDataSource()) {
this.setState({
dataSource: nextProps.dataSource,
});
}
}
render(): React.Node {
return (
<ListView
{...this.props}
ref={(ref) => {
this._listViewRef = ref;
}}
dataSource={this.state.dataSource.getDataSource()}
onScroll={this._onScroll}
renderRow={this._renderRow}
/>
);
}
_onScroll = (e): void => {
// Close any opens rows on ListView scroll
if (this.props.dataSource.getOpenRowID()) {
this.setState({
dataSource: this.state.dataSource.setOpenRowID(null),
});
}
this.props.onScroll && this.props.onScroll(e);
}
/**
* This is a work-around to lock vertical `ListView` scrolling on iOS and
* mimic Android behaviour. Locking vertical scrolling when horizontal
* scrolling is active allows us to significantly improve framerates
* (from high 20s to almost consistently 60 fps)
*/
_setListViewScrollable(value: boolean): void {
if (this._listViewRef && typeof this._listViewRef.setNativeProps === 'function') {
this._listViewRef.setNativeProps({
scrollEnabled: value,
});
}
}
// Passing through ListView's getScrollResponder() function
getScrollResponder(): ?Object {
if (this._listViewRef && typeof this._listViewRef.getScrollResponder === 'function') {
return this._listViewRef.getScrollResponder();
}
}
// This enables rows having variable width slideoutView.
_getMaxSwipeDistance(rowData: Object, sectionID: string, rowID: string): number {
if (typeof this.props.maxSwipeDistance === 'function') {
return this.props.maxSwipeDistance(rowData, sectionID, rowID);
}
return this.props.maxSwipeDistance;
}
_renderRow = (rowData: Object, sectionID: string, rowID: string): React.Element<any> => {
const slideoutView = this.props.renderQuickActions(rowData, sectionID, rowID);
// If renderQuickActions is unspecified or returns falsey, don't allow swipe
if (!slideoutView) {
return this.props.renderRow(rowData, sectionID, rowID);
}
let shouldBounceOnMount = false;
if (this._shouldBounceFirstRowOnMount) {
this._shouldBounceFirstRowOnMount = false;
shouldBounceOnMount = rowID === this.props.dataSource.getFirstRowID();
}
return (
<SwipeableRow
slideoutView={slideoutView}
isOpen={rowData.id === this.props.dataSource.getOpenRowID()}
maxSwipeDistance={this._getMaxSwipeDistance(rowData, sectionID, rowID)}
key={rowID}
onOpen={() => this._onOpen(rowData.id)}
onClose={() => this._onClose(rowData.id)}
onSwipeEnd={() => this._setListViewScrollable(true)}
onSwipeStart={() => this._setListViewScrollable(false)}
shouldBounceOnMount={shouldBounceOnMount}>
{this.props.renderRow(rowData, sectionID, rowID)}
</SwipeableRow>
);
};
_onOpen(rowID: string): void {
this.setState({
dataSource: this.state.dataSource.setOpenRowID(rowID),
});
}
_onClose(rowID: string): void {
this.setState({
dataSource: this.state.dataSource.setOpenRowID(null),
});
}
}
module.exports = SwipeableListView;

View File

@@ -0,0 +1,113 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @providesModule SwipeableListViewDataSource
*/
'use strict';
const ListViewDataSource = require('ListViewDataSource');
/**
* Data source wrapper around ListViewDataSource to allow for tracking of
* which row is swiped open and close opened row(s) when another row is swiped
* open.
*
* See https://github.com/facebook/react-native/pull/5602 for why
* ListViewDataSource is not subclassed.
*/
class SwipeableListViewDataSource {
_previousOpenRowID: string;
_openRowID: string;
_dataBlob: any;
_dataSource: ListViewDataSource;
rowIdentities: Array<Array<string>>;
sectionIdentities: Array<string>;
constructor(params: Object) {
this._dataSource = new ListViewDataSource({
getRowData: params.getRowData,
getSectionHeaderData: params.getSectionHeaderData,
rowHasChanged: (row1, row2) => {
/**
* Row needs to be re-rendered if its swiped open/close status is
* changed, or its data blob changed.
*/
return (
(row1.id !== this._previousOpenRowID && row2.id === this._openRowID) ||
(row1.id === this._previousOpenRowID && row2.id !== this._openRowID) ||
params.rowHasChanged(row1, row2)
);
},
sectionHeaderHasChanged: params.sectionHeaderHasChanged,
});
}
cloneWithRowsAndSections(
dataBlob: any,
sectionIdentities: ?Array<string>,
rowIdentities: ?Array<Array<string>>
): SwipeableListViewDataSource {
this._dataSource = this._dataSource.cloneWithRowsAndSections(
dataBlob,
sectionIdentities,
rowIdentities
);
this._dataBlob = dataBlob;
this.rowIdentities = this._dataSource.rowIdentities;
this.sectionIdentities = this._dataSource.sectionIdentities;
return this;
}
// For the actual ListView to use
getDataSource(): ListViewDataSource {
return this._dataSource;
}
getOpenRowID(): ?string {
return this._openRowID;
}
getFirstRowID(): ?string {
/**
* If rowIdentities is specified, find the first data row from there since
* we don't want to attempt to bounce section headers. If unspecified, find
* the first data row from _dataBlob.
*/
if (this.rowIdentities) {
return this.rowIdentities[0] && this.rowIdentities[0][0];
}
return Object.keys(this._dataBlob)[0];
}
getLastRowID(): ?string {
if (this.rowIdentities && this.rowIdentities.length) {
const lastSection = this.rowIdentities[this.rowIdentities.length - 1];
if (lastSection && lastSection.length) {
return lastSection[lastSection.length - 1];
}
}
return Object.keys(this._dataBlob)[this._dataBlob.length - 1];
}
setOpenRowID(rowID: string): SwipeableListViewDataSource {
this._previousOpenRowID = this._openRowID;
this._openRowID = rowID;
this._dataSource = this._dataSource.cloneWithRowsAndSections(
this._dataBlob,
this.sectionIdentities,
this.rowIdentities
);
return this;
}
}
module.exports = SwipeableListViewDataSource;

View File

@@ -0,0 +1,73 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @providesModule SwipeableQuickActionButton
* @flow
*/
'use strict';
const Image = require('Image');
const PropTypes = require('prop-types');
const React = require('React');
const Text = require('Text');
const TouchableHighlight = require('TouchableHighlight');
const View = require('View');
const ViewPropTypes = require('ViewPropTypes');
import type {ImageSource} from 'ImageSource';
/**
* Standard set of quick action buttons that can, if the user chooses, be used
* with SwipeableListView. Each button takes an image and text with optional
* formatting.
*/
class SwipeableQuickActionButton extends React.Component<{
accessibilityLabel?: string,
imageSource: ImageSource | number,
imageStyle?: ?ViewPropTypes.style,
onPress?: Function,
style?: ?ViewPropTypes.style,
testID?: string,
text?: ?(string | Object | Array<string | Object>),
textStyle?: ?ViewPropTypes.style,
}> {
static propTypes = {
accessibilityLabel: PropTypes.string,
imageSource: Image.propTypes.source.isRequired,
imageStyle: Image.propTypes.style,
onPress: PropTypes.func,
style: ViewPropTypes.style,
testID: PropTypes.string,
text: PropTypes.string,
textStyle: Text.propTypes.style,
};
render(): React.Node {
if (!this.props.imageSource && !this.props.text) {
return null;
}
return (
<TouchableHighlight
onPress={this.props.onPress}
testID={this.props.testID}
underlayColor="transparent">
<View style={this.props.style}>
<Image
accessibilityLabel={this.props.accessibilityLabel}
source={this.props.imageSource}
style={this.props.imageStyle}
/>
<Text style={this.props.textStyle}>
{this.props.text}
</Text>
</View>
</TouchableHighlight>
);
}
}
module.exports = SwipeableQuickActionButton;

View File

@@ -0,0 +1,71 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @providesModule SwipeableQuickActions
* @flow
*/
'use strict';
const React = require('React');
const StyleSheet = require('StyleSheet');
const View = require('View');
const ViewPropTypes = require('ViewPropTypes');
/**
* A thin wrapper around standard quick action buttons that can, if the user
* chooses, be used with SwipeableListView. Sample usage is as follows, in the
* renderQuickActions callback:
*
* <SwipeableQuickActions>
* <SwipeableQuickActionButton {..props} />
* <SwipeableQuickActionButton {..props} />
* </SwipeableQuickActions>
*/
class SwipeableQuickActions extends React.Component<{style?: $FlowFixMe}> {
static propTypes = {
style: ViewPropTypes.style,
};
render(): React.Node {
// $FlowFixMe found when converting React.createClass to ES6
const children = this.props.children;
let buttons = [];
// Multiple children
if (children instanceof Array) {
for (let i = 0; i < children.length; i++) {
buttons.push(children[i]);
// $FlowFixMe found when converting React.createClass to ES6
if (i < this.props.children.length - 1) { // Not last button
buttons.push(<View key={i} style={styles.divider} />);
}
}
} else { // 1 child
buttons = children;
}
return (
<View style={[styles.background, this.props.style]}>
{buttons}
</View>
);
}
}
const styles = StyleSheet.create({
background: {
flex: 1,
flexDirection: 'row',
justifyContent: 'flex-end',
},
divider: {
width: 4,
},
});
module.exports = SwipeableQuickActions;

View File

@@ -0,0 +1,387 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @providesModule SwipeableRow
* @flow
*/
'use strict';
const Animated = require('Animated');
const I18nManager = require('I18nManager');
const PanResponder = require('PanResponder');
const React = require('React');
const PropTypes = require('prop-types');
const StyleSheet = require('StyleSheet');
/* $FlowFixMe(>=0.54.0 site=react_native_oss) This comment suppresses an error
* found when Flow v0.54 was deployed. To see the error delete this comment and
* run Flow. */
const TimerMixin = require('react-timer-mixin');
const View = require('View');
const createReactClass = require('create-react-class');
const emptyFunction = require('fbjs/lib/emptyFunction');
const IS_RTL = I18nManager.isRTL;
// NOTE: Eventually convert these consts to an input object of configurations
// Position of the left of the swipable item when closed
const CLOSED_LEFT_POSITION = 0;
// Minimum swipe distance before we recognize it as such
const HORIZONTAL_SWIPE_DISTANCE_THRESHOLD = 10;
// Minimum swipe speed before we fully animate the user's action (open/close)
const HORIZONTAL_FULL_SWIPE_SPEED_THRESHOLD = 0.3;
// Factor to divide by to get slow speed; i.e. 4 means 1/4 of full speed
const SLOW_SPEED_SWIPE_FACTOR = 4;
// Time, in milliseconds, of how long the animated swipe should be
const SWIPE_DURATION = 300;
/**
* On SwipeableListView mount, the 1st item will bounce to show users it's
* possible to swipe
*/
const ON_MOUNT_BOUNCE_DELAY = 700;
const ON_MOUNT_BOUNCE_DURATION = 400;
// Distance left of closed position to bounce back when right-swiping from closed
const RIGHT_SWIPE_BOUNCE_BACK_DISTANCE = 30;
const RIGHT_SWIPE_BOUNCE_BACK_DURATION = 300;
/**
* Max distance of right swipe to allow (right swipes do functionally nothing).
* Must be multiplied by SLOW_SPEED_SWIPE_FACTOR because gestureState.dx tracks
* how far the finger swipes, and not the actual animation distance.
*/
const RIGHT_SWIPE_THRESHOLD = 30 * SLOW_SPEED_SWIPE_FACTOR;
/**
* Creates a swipable row that allows taps on the main item and a custom View
* on the item hidden behind the row. Typically this should be used in
* conjunction with SwipeableListView for additional functionality, but can be
* used in a normal ListView. See the renderRow for SwipeableListView to see how
* to use this component separately.
*/
const SwipeableRow = createReactClass({
displayName: 'SwipeableRow',
_panResponder: {},
_previousLeft: CLOSED_LEFT_POSITION,
mixins: [TimerMixin],
propTypes: {
children: PropTypes.any,
isOpen: PropTypes.bool,
preventSwipeRight: PropTypes.bool,
maxSwipeDistance: PropTypes.number.isRequired,
onOpen: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
onSwipeEnd: PropTypes.func.isRequired,
onSwipeStart: PropTypes.func.isRequired,
// Should bounce the row on mount
shouldBounceOnMount: PropTypes.bool,
/**
* A ReactElement that is unveiled when the user swipes
*/
slideoutView: PropTypes.node.isRequired,
/**
* The minimum swipe distance required before fully animating the swipe. If
* the user swipes less than this distance, the item will return to its
* previous (open/close) position.
*/
swipeThreshold: PropTypes.number.isRequired,
},
getInitialState(): Object {
return {
currentLeft: new Animated.Value(this._previousLeft),
/**
* In order to render component A beneath component B, A must be rendered
* before B. However, this will cause "flickering", aka we see A briefly
* then B. To counter this, _isSwipeableViewRendered flag is used to set
* component A to be transparent until component B is loaded.
*/
isSwipeableViewRendered: false,
rowHeight: (null: ?number),
};
},
getDefaultProps(): Object {
return {
isOpen: false,
preventSwipeRight: false,
maxSwipeDistance: 0,
onOpen: emptyFunction,
onClose: emptyFunction,
onSwipeEnd: emptyFunction,
onSwipeStart: emptyFunction,
swipeThreshold: 30,
};
},
UNSAFE_componentWillMount(): void {
this._panResponder = PanResponder.create({
onMoveShouldSetPanResponderCapture: this._handleMoveShouldSetPanResponderCapture,
onPanResponderGrant: this._handlePanResponderGrant,
onPanResponderMove: this._handlePanResponderMove,
onPanResponderRelease: this._handlePanResponderEnd,
onPanResponderTerminationRequest: this._onPanResponderTerminationRequest,
onPanResponderTerminate: this._handlePanResponderEnd,
onShouldBlockNativeResponder: (event, gestureState) => false,
});
},
componentDidMount(): void {
if (this.props.shouldBounceOnMount) {
/**
* Do the on mount bounce after a delay because if we animate when other
* components are loading, the animation will be laggy
*/
this.setTimeout(() => {
this._animateBounceBack(ON_MOUNT_BOUNCE_DURATION);
}, ON_MOUNT_BOUNCE_DELAY);
}
},
UNSAFE_componentWillReceiveProps(nextProps: Object): void {
/**
* We do not need an "animateOpen(noCallback)" because this animation is
* handled internally by this component.
*/
if (this.props.isOpen && !nextProps.isOpen) {
this._animateToClosedPosition();
}
},
render(): React.Element<any> {
// The view hidden behind the main view
let slideOutView;
if (this.state.isSwipeableViewRendered && this.state.rowHeight) {
slideOutView = (
<View style={[
styles.slideOutContainer,
{height: this.state.rowHeight},
]}>
{this.props.slideoutView}
</View>
);
}
// The swipeable item
const swipeableView = (
<Animated.View
onLayout={this._onSwipeableViewLayout}
style={{transform: [{translateX: this.state.currentLeft}]}}>
{this.props.children}
</Animated.View>
);
return (
<View
{...this._panResponder.panHandlers}>
{slideOutView}
{swipeableView}
</View>
);
},
close(): void {
this.props.onClose();
this._animateToClosedPosition();
},
_onSwipeableViewLayout(event: Object): void {
this.setState({
isSwipeableViewRendered: true,
rowHeight: event.nativeEvent.layout.height,
});
},
_handleMoveShouldSetPanResponderCapture(
event: Object,
gestureState: Object,
): boolean {
// Decides whether a swipe is responded to by this component or its child
return gestureState.dy < 10 && this._isValidSwipe(gestureState);
},
_handlePanResponderGrant(event: Object, gestureState: Object): void {
},
_handlePanResponderMove(event: Object, gestureState: Object): void {
if (this._isSwipingExcessivelyRightFromClosedPosition(gestureState)) {
return;
}
this.props.onSwipeStart();
if (this._isSwipingRightFromClosed(gestureState)) {
this._swipeSlowSpeed(gestureState);
} else {
this._swipeFullSpeed(gestureState);
}
},
_isSwipingRightFromClosed(gestureState: Object): boolean {
const gestureStateDx = IS_RTL ? -gestureState.dx : gestureState.dx;
return this._previousLeft === CLOSED_LEFT_POSITION && gestureStateDx > 0;
},
_swipeFullSpeed(gestureState: Object): void {
this.state.currentLeft.setValue(this._previousLeft + gestureState.dx);
},
_swipeSlowSpeed(gestureState: Object): void {
this.state.currentLeft.setValue(
this._previousLeft + gestureState.dx / SLOW_SPEED_SWIPE_FACTOR,
);
},
_isSwipingExcessivelyRightFromClosedPosition(gestureState: Object): boolean {
/**
* We want to allow a BIT of right swipe, to allow users to know that
* swiping is available, but swiping right does not do anything
* functionally.
*/
const gestureStateDx = IS_RTL ? -gestureState.dx : gestureState.dx;
return (
this._isSwipingRightFromClosed(gestureState) &&
gestureStateDx > RIGHT_SWIPE_THRESHOLD
);
},
_onPanResponderTerminationRequest(
event: Object,
gestureState: Object,
): boolean {
return false;
},
_animateTo(
toValue: number,
duration: number = SWIPE_DURATION,
callback: Function = emptyFunction,
): void {
Animated.timing(
this.state.currentLeft,
{
duration,
toValue,
useNativeDriver: true,
},
).start(() => {
this._previousLeft = toValue;
callback();
});
},
_animateToOpenPosition(): void {
const maxSwipeDistance = IS_RTL ? -this.props.maxSwipeDistance : this.props.maxSwipeDistance;
this._animateTo(-maxSwipeDistance);
},
_animateToOpenPositionWith(
speed: number,
distMoved: number,
): void {
/**
* Ensure the speed is at least the set speed threshold to prevent a slow
* swiping animation
*/
speed = (
speed > HORIZONTAL_FULL_SWIPE_SPEED_THRESHOLD ?
speed :
HORIZONTAL_FULL_SWIPE_SPEED_THRESHOLD
);
/**
* Calculate the duration the row should take to swipe the remaining distance
* at the same speed the user swiped (or the speed threshold)
*/
const duration = Math.abs((this.props.maxSwipeDistance - Math.abs(distMoved)) / speed);
const maxSwipeDistance = IS_RTL ? -this.props.maxSwipeDistance : this.props.maxSwipeDistance;
this._animateTo(-maxSwipeDistance, duration);
},
_animateToClosedPosition(duration: number = SWIPE_DURATION): void {
this._animateTo(CLOSED_LEFT_POSITION, duration);
},
_animateToClosedPositionDuringBounce(): void {
this._animateToClosedPosition(RIGHT_SWIPE_BOUNCE_BACK_DURATION);
},
_animateBounceBack(duration: number): void {
/**
* When swiping right, we want to bounce back past closed position on release
* so users know they should swipe right to get content.
*/
const swipeBounceBackDistance = IS_RTL ?
-RIGHT_SWIPE_BOUNCE_BACK_DISTANCE :
RIGHT_SWIPE_BOUNCE_BACK_DISTANCE;
this._animateTo(
-swipeBounceBackDistance,
duration,
this._animateToClosedPositionDuringBounce,
);
},
// Ignore swipes due to user's finger moving slightly when tapping
_isValidSwipe(gestureState: Object): boolean {
if (this.props.preventSwipeRight && this._previousLeft === CLOSED_LEFT_POSITION && gestureState.dx > 0) {
return false;
}
return Math.abs(gestureState.dx) > HORIZONTAL_SWIPE_DISTANCE_THRESHOLD;
},
_shouldAnimateRemainder(gestureState: Object): boolean {
/**
* If user has swiped past a certain distance, animate the rest of the way
* if they let go
*/
return (
Math.abs(gestureState.dx) > this.props.swipeThreshold ||
gestureState.vx > HORIZONTAL_FULL_SWIPE_SPEED_THRESHOLD
);
},
_handlePanResponderEnd(event: Object, gestureState: Object): void {
const horizontalDistance = IS_RTL ? -gestureState.dx : gestureState.dx;
if (this._isSwipingRightFromClosed(gestureState)) {
this.props.onOpen();
this._animateBounceBack(RIGHT_SWIPE_BOUNCE_BACK_DURATION);
} else if (this._shouldAnimateRemainder(gestureState)) {
if (horizontalDistance < 0) {
// Swiped left
this.props.onOpen();
this._animateToOpenPositionWith(gestureState.vx, horizontalDistance);
} else {
// Swiped right
this.props.onClose();
this._animateToClosedPosition();
}
} else {
if (this._previousLeft === CLOSED_LEFT_POSITION) {
this._animateToClosedPosition();
} else {
this._animateToOpenPosition();
}
}
this.props.onSwipeEnd();
},
});
const styles = StyleSheet.create({
slideOutContainer: {
bottom: 0,
left: 0,
position: 'absolute',
right: 0,
top: 0,
},
});
module.exports = SwipeableRow;

View File

@@ -0,0 +1,756 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @providesModule WindowedListView
* @flow
*/
'use strict';
const Batchinator = require('Batchinator');
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('fbjs/lib/invariant');
const nullthrows = require('fbjs/lib/nullthrows');
import type {NativeMethodsMixinType} from 'ReactNativeTypes';
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
) => ?React.Element<any>,
/**
* Rendered when the list is scrolled faster than rows can be rendered.
*/
renderWindowBoundaryIndicator?: (
showIndicator: boolean,
) => ?React.Element<any>,
/**
* Always rendered at the bottom of all the rows.
*/
renderFooter?: (
showFooter: boolean,
) => ?React.Element<any>,
/**
* 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) => React.Element<any>,
/**
* 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, State> {
/**
* Recomputing which rows to render is batched up and run asynchronously to avoid wastful updates,
* e.g. from multiple layout updates in rapid succession.
*/
_computeRowsToRenderBatcher: Batchinator;
_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;
_viewableRows: Array<number> = [];
_cellsInProgress: Set<string> = new Set();
_scrollRef: ?ScrollView;
_viewabilityHelper: ViewabilityHelper;
static defaultProps = {
initialNumToRender: 10,
maxNumToRender: 30,
numToRenderAhead: 10,
viewablePercentThreshold: 50,
/* $FlowFixMe(>=0.59.0 site=react_native_fb) This comment suppresses an
* error caught by Flow 0.59 which was not caught before. Most likely, this
* error is because an exported function parameter is missing an
* annotation. Without an annotation, these parameters are uncovered by
* Flow. */
renderScrollComponent: (props) => <ScrollView {...props} />,
disableIncrementalRendering: false,
recomputeRowsBatchingPeriod: 10, // This should capture most events that happen within a frame
};
constructor(props: Props) {
super(props);
invariant(
this.props.numToRenderAhead < this.props.maxNumToRender,
'WindowedListView: numToRenderAhead must be less than maxNumToRender'
);
this._computeRowsToRenderBatcher = new Batchinator(
() => this._computeRowsToRender(this.props),
this.props.recomputeRowsBatchingPeriod,
);
this._viewabilityHelper = new ViewabilityHelper({
viewAreaCoveragePercentThreshold: this.props.viewablePercentThreshold,
});
this.state = {
firstRow: 0,
lastRow: Math.min(this.props.data.length, this.props.initialNumToRender) - 1,
};
}
getScrollResponder(): ?ScrollView {
return this._scrollRef &&
this._scrollRef.getScrollResponder &&
this._scrollRef.getScrollResponder();
}
shouldComponentUpdate(newProps: Props, newState: State): boolean {
DEBUG && infoLog('WLV: shouldComponentUpdate...');
if (newState !== this.state) {
DEBUG && infoLog(' yes: ', {newState, oldState: this.state});
return true;
}
for (const key in newProps) {
if (key !== 'data' && newProps[key] !== this.props[key]) {
DEBUG && infoLog(' yes, non-data prop change: ', {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) {
DEBUG && infoLog(
' yes, subset length: ',
{newLen: newDataSubset.length, oldLen: prevDataSubset.length}
);
return true;
}
for (let idx = 0; idx < newDataSubset.length; idx++) {
if (newDataSubset[idx].rowData !== prevDataSubset[idx].rowData ||
newDataSubset[idx].rowKey !== prevDataSubset[idx].rowKey) {
DEBUG && infoLog(
' yes, data change: ',
{idx, new: newDataSubset[idx], old: prevDataSubset[idx]}
);
return true;
}
}
DEBUG && infoLog(' knope');
return false;
}
UNSAFE_componentWillReceiveProps() {
this._computeRowsToRenderBatcher.schedule();
}
_onMomentumScrollEnd = (e: Object) => {
this._onScroll(e);
};
_getFrameMetrics = (index: number): ?{length: number, offset: number} => {
const frame = this._rowFrames[this.props.data[index].rowKey];
return frame && {length: frame.height, offset: frame.y};
}
_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._computeRowsToRenderBatcher.schedule();
}
if (this.props.onViewableRowsChanged && Object.keys(this._rowFrames).length) {
const viewableRows = this._viewabilityHelper.computeViewableItems(
this.props.data.length,
e.nativeEvent.contentOffset.y,
e.nativeEvent.layoutMeasurement.height,
this._getFrameMetrics,
);
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 prev = this._rowFrames[rowKey] || {};
infoLog(
'record layout for row: ',
{k: rowKey, h: layout.height, y: layout.y, x: layout.x, hp: prev.height, yp: prev.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._computeRowsToRenderBatcher.schedule();
}
};
_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);
}
};
componentWillUnmount() {
this._computeRowsToRenderBatcher.dispose();
}
_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 = clamp(0, this.state.lastRow, totalRows - 1);
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 (targetLastRow > this.state.lastRow) {
lastRow++;
} else if (targetLastRow < this.state.lastRow) {
lastRow--;
}
// 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 scroll 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(): React.Node {
const {firstRow} = this.state;
const lastRow = clamp(0, this.state.lastRow, this.props.data.length - 1);
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(showIndicator)}
</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(showFooter)}
</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(!showFooter)}
</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
) => ?React.Element<any>,
/**
* 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<CellProps> {
_containerRef: NativeMethodsMixinType;
_offscreenRenderDone = false;
_timeout = 0;
_lastLayout: ?Object = null;
_perfUpdateID: number = 0;
_asyncCookie: any;
_includeInLayoutLatch: boolean = false;
UNSAFE_componentWillMount() {
if (this.props.asyncRowPerfEventName) {
this._perfUpdateID = g_perf_update_id++;
this._asyncCookie = Systrace.beginAsyncEvent(
this.props.asyncRowPerfEventName + this._perfUpdateID
);
// $FlowFixMe(>=0.28.0)
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
);
// $FlowFixMe(>=0.28.0)
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() {
/* $FlowFixMe(>=0.63.0 site=react_native_fb) This comment suppresses an
* error found when Flow v0.63 was deployed. To see the error delete this
* comment and run Flow. */
clearTimeout(this._timeout);
this.props.onProgressChange({rowKey: this.props.rowKey, inProgress: false});
this.props.onWillUnmount(this.props.rowKey);
}
UNSAFE_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) => {
/* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This comment
* suppresses an error when upgrading Flow's support for React. To see the
* error delete this comment and run Flow. */
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;