react-native-recyclerview-list
Version:
A RecyclerView implementation for React Native
411 lines (356 loc) • 11.9 kB
JavaScript
import React, { Component } from 'react';
import ReactNative, { View, requireNativeComponent, DeviceEventEmitter, StyleSheet, UIManager, ViewPropTypes } from 'react-native';
import PropTypes from 'prop-types';
import DataSource from './DataSource';
class RecyclerViewItem extends Component {
static propTypes = {
style: ViewPropTypes.style,
itemIndex: PropTypes.number,
shouldUpdate: PropTypes.bool,
dataSource: PropTypes.object,
renderItem: PropTypes.func,
header: PropTypes.any,
separator: PropTypes.any,
footer: PropTypes.any
}
shouldComponentUpdate(nextProps) {
if (
(nextProps.itemIndex !== this.props.itemIndex) ||
(nextProps.header !== this.props.header) ||
(nextProps.footer !== this.props.footer) ||
(nextProps.separator !== this.props.separator) ||
(nextProps.shouldUpdate)
) {
return true;
}
return false;
}
render() {
const { style, itemIndex, dataSource, renderItem, header, separator, footer } = this.props;
const element = renderItem({
item: dataSource.get(itemIndex),
index: itemIndex
});
return (
<NativeRecyclerViewItem
style={style}
itemIndex={itemIndex}>
{header}
{element}
{separator}
{footer}
</NativeRecyclerViewItem>
);
}
}
const NativeRecyclerViewItem = requireNativeComponent('RecyclerViewItemView');
class RecyclerView extends React.PureComponent {
static propTypes = {
...View.propTypes,
renderItem: PropTypes.func,
dataSource: PropTypes.instanceOf(DataSource),
windowSize: PropTypes.number,
initialListSize: PropTypes.number,
initialScrollIndex: PropTypes.number,
initialScrollOffset: PropTypes.number,
inverted: PropTypes.bool,
itemAnimatorEnabled: PropTypes.bool,
ListHeaderComponent: PropTypes.element,
ListFooterComponent: PropTypes.element,
ListEmptyComponent: PropTypes.element,
ItemSeparatorComponent: PropTypes.element,
onVisibleItemsChange: PropTypes.func
}
static defaultProps = {
dataSource: new DataSource([], (item, i) => i),
initialListSize: 10,
windowSize: 30,
inverted: false,
itemAnimatorEnabled: true,
}
_dataSourceListener = {
onUnshift: () => {
this._notifyItemRangeInserted(0, 1);
this._shouldUpdateAll = true;
},
onPush: () => {
const { dataSource } = this.props;
this._notifyItemRangeInserted(dataSource.size(), 1);
this._shouldUpdateAll = true;
},
onMoveUp: (position) => {
this._notifyItemMoved(position, position - 1);
this._shouldUpdateAll = true;
},
onMoveDown: (position) => {
this._notifyItemMoved(position, position + 1);
this._shouldUpdateAll = true;
},
onSplice: (start, deleteCount, ...items) => {
if (deleteCount > 0) {
this._notifyItemRangeRemoved(start, deleteCount);
}
if (items.length > 0) {
this._notifyItemRangeInserted(start, items.length);
}
this._shouldUpdateAll = true;
},
onSet: (index, item) => {
this._shouldUpdateKeys.push(this.props.dataSource.getKey(item, index));
this.forceUpdate();
},
onSetDirty: () => {
this._shouldUpdateAll = true;
this.forceUpdate();
}
}
constructor(props) {
super(props);
const {
dataSource,
initialListSize,
initialScrollIndex
} = this.props;
dataSource._addListener(this._dataSourceListener);
var visibleRange = initialScrollIndex >= 0 ?
[initialScrollIndex, initialScrollIndex + initialListSize]
: [0, initialListSize];
this.state = {
firstVisibleIndex: visibleRange[0],
lastVisibleIndex: visibleRange[1],
itemCount: dataSource.size()
};
this._shouldUpdateAll = true;
this._shouldUpdateKeys = [];
}
componentWillMount() {
}
componentWillUnmount() {
const { dataSource } = this.props;
if (dataSource) {
dataSource._removeListener(this._dataSourceListener);
}
}
componentDidMount() {
const { initialScrollIndex, initialScrollOffset } = this.props;
if (initialScrollIndex) {
this.scrollToIndex({
animated: false,
index: initialScrollIndex,
viewPosition: 0,
viewOffset: initialScrollOffset
});
}
this._shouldUpdateAll = false;
this._shouldUpdateKeys = [];
}
componentWillReceiveProps(nextProps) {
const { dataSource } = this.props;
if (nextProps.dataSource !== dataSource) {
dataSource._removeListener(this._dataSourceListener);
nextProps.dataSource._addListener(this._dataSourceListener);
this._notifyDataSetChanged(nextProps.dataSource.size());
}
}
componentDidUpdate(prevProps, prevState) {
this._shouldUpdateAll = false;
this._shouldUpdateKeys = [];
}
render() {
const {
dataSource,
renderItem,
ListHeaderComponent,
ListFooterComponent,
ListEmptyComponent,
ItemSeparatorComponent,
inverted,
...rest
} = this.props;
const itemCount = dataSource.size();
const end = itemCount-1;
var stateItemCount = this.state.itemCount;
var body = [];
var itemRangeToRender = this._calcItemRangeToRender(this.state.firstVisibleIndex, this.state.lastVisibleIndex);
if (ListHeaderComponent) {
var headerElement = React.isValidElement(ListHeaderComponent)
? ListHeaderComponent
: <ListHeaderComponent />;
}
if (ListFooterComponent) {
var footerElement = React.isValidElement(ListFooterComponent)
? ListFooterComponent
: <ListFooterComponent />;
}
if (ItemSeparatorComponent) {
var separatorElement = React.isValidElement(ItemSeparatorComponent)
? ItemSeparatorComponent
: <ItemSeparatorComponent />;
}
if (itemCount > 0) {
for (var i=itemRangeToRender[0]; i<itemRangeToRender[1]; i++) {
let item = dataSource.get(i);
let itemKey = dataSource.getKey(item, i);
let shouldUpdate = this._needsItemUpdate(itemKey);
let isFirst = i == 0;
let isLast = i == end;
let header = inverted ? (isLast && footerElement) : (isFirst && headerElement);
let footer = inverted ? (isFirst && headerElement) : (isLast && footerElement);
let separator = inverted ? (!isFirst && separatorElement) : (!isLast && separatorElement);
body.push(
<RecyclerViewItem
key={itemKey}
style={styles.absolute}
itemIndex={i}
shouldUpdate={shouldUpdate}
dataSource={dataSource}
renderItem={renderItem}
header={header}
separator={separator}
footer={footer} />
);
}
} else if (ListEmptyComponent) {
var emptyElement = React.isValidElement(ListEmptyComponent)
? ListEmptyComponent
: <ListEmptyComponent />;
body.push(
<RecyclerViewItem
style={styles.absolute}
key="$empty"
itemIndex={0}
shouldUpdate={true}
dataSource={dataSource}
renderItem={() => emptyElement}
header={headerElement}
footer={footerElement} />
);
stateItemCount = 1;
}
return (
<NativeRecyclerView
{...rest}
itemCount={stateItemCount}
onVisibleItemsChange={this._handleVisibleItemsChange}
inverted={inverted}>
{body}
</NativeRecyclerView>
);
}
scrollToEnd({ animated = true, velocity } = {}) {
this.scrollToIndex({
index: this.props.dataSource.size() - 1,
animated,
velocity
});
}
scrollToIndex = ({ animated = true, index, velocity, viewPosition, viewOffset }) => {
index = Math.max(0, Math.min(index, this.props.dataSource.size()-1));
if (animated) {
UIManager.dispatchViewManagerCommand(
ReactNative.findNodeHandle(this),
UIManager.AndroidRecyclerViewBackedScrollView.Commands.scrollToIndex,
[animated, index, velocity, viewPosition, viewOffset],
);
} else {
this.setState({
firstVisibleIndex: index,
lastVisibleIndex: index + (this.state.lastVisibleIndex - this.state.firstVisibleIndex)
}, () => {
UIManager.dispatchViewManagerCommand(
ReactNative.findNodeHandle(this),
UIManager.AndroidRecyclerViewBackedScrollView.Commands.scrollToIndex,
[animated, index, velocity, viewPosition, viewOffset],
);
});
}
}
_needsItemUpdate(itemKey) {
return this._shouldUpdateAll || this._shouldUpdateKeys.includes(itemKey);
}
_handleVisibleItemsChange = ({nativeEvent}) => {
var firstIndex = nativeEvent.firstIndex;
var lastIndex = nativeEvent.lastIndex;
this.setState({
firstVisibleIndex: firstIndex,
lastVisibleIndex: lastIndex,
});
const { onVisibleItemsChange } = this.props;
if (onVisibleItemsChange) {
onVisibleItemsChange(nativeEvent);
}
}
_calcItemRangeToRender(firstVisibleIndex, lastVisibleIndex) {
const { dataSource, windowSize } = this.props;
var count = dataSource.size();
var from = Math.min(count, Math.max(0, firstVisibleIndex - windowSize));
var to = Math.min(count, lastVisibleIndex + windowSize);
return [from, to];
}
_notifyItemMoved(currentPosition, nextPosition) {
UIManager.dispatchViewManagerCommand(
ReactNative.findNodeHandle(this),
UIManager.AndroidRecyclerViewBackedScrollView.Commands.notifyItemMoved,
[currentPosition, nextPosition],
);
this.forceUpdate();
}
_notifyItemRangeInserted(position, count) {
UIManager.dispatchViewManagerCommand(
ReactNative.findNodeHandle(this),
UIManager.AndroidRecyclerViewBackedScrollView.Commands.notifyItemRangeInserted,
[position, count],
);
const { firstVisibleIndex, lastVisibleIndex, itemCount } = this.state;
if (itemCount == 0) {
this.setState({
itemCount: this.props.dataSource.size(),
firstVisibleIndex: 0,
lastVisibleIndex: this.props.initialListSize
});
} else {
if (position <= firstVisibleIndex) {
this.setState({
firstVisibleIndex: this.state.firstVisibleIndex + count,
lastVisibleIndex: this.state.lastVisibleIndex + count,
});
} else {
this.forceUpdate();
}
}
}
_notifyItemRangeRemoved(position, count) {
UIManager.dispatchViewManagerCommand(
ReactNative.findNodeHandle(this),
UIManager.AndroidRecyclerViewBackedScrollView.Commands.notifyItemRangeRemoved,
[position, count],
);
this.forceUpdate();
}
_notifyDataSetChanged(itemCount) {
UIManager.dispatchViewManagerCommand(
ReactNative.findNodeHandle(this),
UIManager.AndroidRecyclerViewBackedScrollView.Commands.notifyDataSetChanged,
[itemCount],
);
this.setState({
itemCount
});
}
}
var nativeOnlyProps = {
nativeOnly: {
onVisibleItemsChange: true,
itemCount: true
}
};
var styles = StyleSheet.create({
absolute: {
position: 'absolute',
top: 0,
left: 0,
right: 0
},
});
const NativeRecyclerView = requireNativeComponent('AndroidRecyclerViewBackedScrollView', RecyclerView, nativeOnlyProps);
module.exports = RecyclerView;