mirror of
https://github.com/status-im/react-native.git
synced 2025-02-26 16:10:58 +00:00
support sticky headers
Summary: This adds support for both automagical sticky section headers in `SectionList` as well as the more free-form `stickyHeaderIndices` on `FlatList` or `VirtualizedList`. The basic concept is to take the initial `stickySectionHeaders` and remap them to the indices corresponding to the mounted subset in the render window. The main trick here is that the currently stuck header might itself be outside of the render window, so we need to search the gap to see if that's the case and render it (with spacers above and below it instead of one big spacer). In the `SectionList` we simply pre-compute the sticky headers at the same time as when we scan the sections to determine the flattened length and pass those to `VirtualizedList`. This also requires some updates to `ScrollView` to work in the churny environment of `VirtualizedList`. We propogate the keys on the children to the animated wrappers so that as items are removed and the indices of the remaining items change, react can keep proper track of them. We also fix the scroll back case where new headers are rendered from the top down and aren't updated with the `setNextLayoutY` callback because the `onLayout` call for the next header happened before it was mounted. This is done by just tracking all the layout values in a map and providing them to the sticky components at render time. This might also improve perf a little by property configuring the animations syncronously instead of waiting for the `onLayout` callback. We also need to protect against stale onLayout callbacks and other fun stuff. == Test Plan == https://www.facebook.com/groups/react.native.community/permalink/940332509435661/ Scroll a lot with and without debug mode on. Make sure spinner still spins and there are no crashes (lots of crashes during development due to the animated configuration being non-monotonic if anything stale values get through). Also made sure that tapping a row to change it's height would properly update the animation configurations so the collision point would still be correct. Reviewed By: yungsters Differential Revision: D4695065 fbshipit-source-id: 855c4e31c8f8b450d32150dbdb2e07f1a9f9f98e
This commit is contained in:
parent
7861fdd974
commit
72670bf8d2
@ -79,6 +79,7 @@ class SectionListExample extends React.PureComponent {
|
|||||||
|
|
||||||
state = {
|
state = {
|
||||||
data: genItemData(1000),
|
data: genItemData(1000),
|
||||||
|
debug: false,
|
||||||
filterText: '',
|
filterText: '',
|
||||||
logViewable: false,
|
logViewable: false,
|
||||||
virtualized: true,
|
virtualized: true,
|
||||||
@ -96,6 +97,16 @@ class SectionListExample extends React.PureComponent {
|
|||||||
filterRegex.test(item.text) || filterRegex.test(item.title)
|
filterRegex.test(item.text) || filterRegex.test(item.title)
|
||||||
);
|
);
|
||||||
const filteredData = this.state.data.filter(filter);
|
const filteredData = this.state.data.filter(filter);
|
||||||
|
const filteredSectionData = [];
|
||||||
|
let startIndex = 0;
|
||||||
|
const endIndex = filteredData.length - 1;
|
||||||
|
for (let ii = 10; ii <= endIndex + 10; ii += 10) {
|
||||||
|
filteredSectionData.push({
|
||||||
|
key: `${filteredData[startIndex].key} - ${filteredData[Math.min(ii - 1, endIndex)].key}`,
|
||||||
|
data: filteredData.slice(startIndex, ii),
|
||||||
|
});
|
||||||
|
startIndex = ii;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<UIExplorerPage
|
<UIExplorerPage
|
||||||
noSpacer={true}
|
noSpacer={true}
|
||||||
@ -111,6 +122,7 @@ class SectionListExample extends React.PureComponent {
|
|||||||
<View style={styles.optionSection}>
|
<View style={styles.optionSection}>
|
||||||
{renderSmallSwitchOption(this, 'virtualized')}
|
{renderSmallSwitchOption(this, 'virtualized')}
|
||||||
{renderSmallSwitchOption(this, 'logViewable')}
|
{renderSmallSwitchOption(this, 'logViewable')}
|
||||||
|
{renderSmallSwitchOption(this, 'debug')}
|
||||||
<Spindicator value={this._scrollPos} />
|
<Spindicator value={this._scrollPos} />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@ -124,6 +136,7 @@ class SectionListExample extends React.PureComponent {
|
|||||||
ItemSeparatorComponent={() =>
|
ItemSeparatorComponent={() =>
|
||||||
<CustomSeparatorComponent text="ITEM SEPARATOR" />
|
<CustomSeparatorComponent text="ITEM SEPARATOR" />
|
||||||
}
|
}
|
||||||
|
debug={this.state.debug}
|
||||||
enableVirtualization={this.state.virtualized}
|
enableVirtualization={this.state.virtualized}
|
||||||
onRefresh={() => alert('onRefresh: nothing to refresh :P')}
|
onRefresh={() => alert('onRefresh: nothing to refresh :P')}
|
||||||
onScroll={this._scrollSinkY}
|
onScroll={this._scrollSinkY}
|
||||||
@ -139,7 +152,7 @@ class SectionListExample extends React.PureComponent {
|
|||||||
{noImage: true, title: '1st item', text: 'Section s2', key: '0'},
|
{noImage: true, title: '1st item', text: 'Section s2', key: '0'},
|
||||||
{noImage: true, title: '2nd item', text: 'Section s2', key: '1'},
|
{noImage: true, title: '2nd item', text: 'Section s2', key: '1'},
|
||||||
]},
|
]},
|
||||||
{key: 'Filtered Items', data: filteredData},
|
...filteredSectionData,
|
||||||
]}
|
]}
|
||||||
viewabilityConfig={VIEWABILITY_CONFIG}
|
viewabilityConfig={VIEWABILITY_CONFIG}
|
||||||
/>
|
/>
|
||||||
|
@ -383,7 +383,7 @@ const ScrollView = React.createClass({
|
|||||||
_scrollAnimatedValue: (new Animated.Value(0): Animated.Value),
|
_scrollAnimatedValue: (new Animated.Value(0): Animated.Value),
|
||||||
_scrollAnimatedValueAttachment: (null: ?{detach: () => void}),
|
_scrollAnimatedValueAttachment: (null: ?{detach: () => void}),
|
||||||
_stickyHeaderRefs: (new Map(): Map<number, ScrollViewStickyHeader>),
|
_stickyHeaderRefs: (new Map(): Map<number, ScrollViewStickyHeader>),
|
||||||
|
_headerLayoutYs: (new Map(): Map<string, number>),
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return this.scrollResponderMixinGetInitialState();
|
return this.scrollResponderMixinGetInitialState();
|
||||||
},
|
},
|
||||||
@ -391,6 +391,7 @@ const ScrollView = React.createClass({
|
|||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
this._scrollAnimatedValue = new Animated.Value(0);
|
this._scrollAnimatedValue = new Animated.Value(0);
|
||||||
this._stickyHeaderRefs = new Map();
|
this._stickyHeaderRefs = new Map();
|
||||||
|
this._headerLayoutYs = new Map();
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
@ -482,6 +483,11 @@ const ScrollView = React.createClass({
|
|||||||
this.scrollTo({x, y, animated: false});
|
this.scrollTo({x, y, animated: false});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_getKeyForIndex: function(index, childArray) {
|
||||||
|
const child = childArray[index];
|
||||||
|
return child && child.key;
|
||||||
|
},
|
||||||
|
|
||||||
_updateAnimatedNodeAttachment: function() {
|
_updateAnimatedNodeAttachment: function() {
|
||||||
if (this.props.stickyHeaderIndices && this.props.stickyHeaderIndices.length > 0) {
|
if (this.props.stickyHeaderIndices && this.props.stickyHeaderIndices.length > 0) {
|
||||||
if (!this._scrollAnimatedValueAttachment) {
|
if (!this._scrollAnimatedValueAttachment) {
|
||||||
@ -498,21 +504,34 @@ const ScrollView = React.createClass({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_setStickyHeaderRef: function(index, ref) {
|
_setStickyHeaderRef: function(key, ref) {
|
||||||
this._stickyHeaderRefs.set(index, ref);
|
if (ref) {
|
||||||
|
this._stickyHeaderRefs.set(key, ref);
|
||||||
|
} else {
|
||||||
|
this._stickyHeaderRefs.delete(key);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_onStickyHeaderLayout: function(index, event) {
|
_onStickyHeaderLayout: function(index, event, key) {
|
||||||
if (!this.props.stickyHeaderIndices) {
|
if (!this.props.stickyHeaderIndices) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const childArray = React.Children.toArray(this.props.children);
|
||||||
|
if (key !== this._getKeyForIndex(index, childArray)) {
|
||||||
|
// ignore stale layout update
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const previousHeaderIndex = this.props.stickyHeaderIndices[
|
const layoutY = event.nativeEvent.layout.y;
|
||||||
this.props.stickyHeaderIndices.indexOf(index) - 1
|
this._headerLayoutYs.set(key, layoutY);
|
||||||
];
|
|
||||||
|
const indexOfIndex = this.props.stickyHeaderIndices.indexOf(index);
|
||||||
|
const previousHeaderIndex = this.props.stickyHeaderIndices[indexOfIndex - 1];
|
||||||
if (previousHeaderIndex != null) {
|
if (previousHeaderIndex != null) {
|
||||||
const previousHeader = this._stickyHeaderRefs.get(previousHeaderIndex);
|
const previousHeader = this._stickyHeaderRefs.get(
|
||||||
previousHeader && previousHeader.setNextHeaderY(event.nativeEvent.layout.y);
|
this._getKeyForIndex(previousHeaderIndex, childArray)
|
||||||
|
);
|
||||||
|
previousHeader && previousHeader.setNextHeaderY(layoutY);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -601,15 +620,21 @@ const ScrollView = React.createClass({
|
|||||||
|
|
||||||
const {stickyHeaderIndices} = this.props;
|
const {stickyHeaderIndices} = this.props;
|
||||||
const hasStickyHeaders = stickyHeaderIndices && stickyHeaderIndices.length > 0;
|
const hasStickyHeaders = stickyHeaderIndices && stickyHeaderIndices.length > 0;
|
||||||
const children = stickyHeaderIndices && hasStickyHeaders ?
|
const childArray = hasStickyHeaders && React.Children.toArray(this.props.children);
|
||||||
React.Children.toArray(this.props.children).map((child, index) => {
|
const children = hasStickyHeaders ?
|
||||||
const stickyHeaderIndex = stickyHeaderIndices.indexOf(index);
|
childArray.map((child, index) => {
|
||||||
if (child && stickyHeaderIndex >= 0) {
|
const indexOfIndex = child ? stickyHeaderIndices.indexOf(index) : -1;
|
||||||
|
if (indexOfIndex > -1) {
|
||||||
|
const key = child.key;
|
||||||
|
const nextIndex = stickyHeaderIndices[indexOfIndex + 1];
|
||||||
return (
|
return (
|
||||||
<ScrollViewStickyHeader
|
<ScrollViewStickyHeader
|
||||||
key={index}
|
key={key}
|
||||||
ref={(ref) => this._setStickyHeaderRef(index, ref)}
|
ref={(ref) => this._setStickyHeaderRef(key, ref)}
|
||||||
onLayout={(event) => this._onStickyHeaderLayout(index, event)}
|
nextHeaderLayoutY={
|
||||||
|
this._headerLayoutYs.get(this._getKeyForIndex(nextIndex, childArray))
|
||||||
|
}
|
||||||
|
onLayout={(event) => this._onStickyHeaderLayout(index, event, key)}
|
||||||
scrollAnimatedValue={this._scrollAnimatedValue}>
|
scrollAnimatedValue={this._scrollAnimatedValue}>
|
||||||
{child}
|
{child}
|
||||||
</ScrollViewStickyHeader>
|
</ScrollViewStickyHeader>
|
||||||
|
@ -17,18 +17,29 @@ const StyleSheet = require('StyleSheet');
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children?: React.Element<*>,
|
children?: React.Element<*>,
|
||||||
scrollAnimatedValue: Animated.Value,
|
nextHeaderLayoutY: ?number,
|
||||||
onLayout: (event: Object) => void,
|
onLayout: (event: Object) => void,
|
||||||
|
scrollAnimatedValue: Animated.Value,
|
||||||
};
|
};
|
||||||
|
|
||||||
class ScrollViewStickyHeader extends React.Component {
|
class ScrollViewStickyHeader extends React.Component {
|
||||||
props: Props;
|
props: Props;
|
||||||
state = {
|
state: {
|
||||||
|
measured: boolean,
|
||||||
|
layoutY: number,
|
||||||
|
layoutHeight: number,
|
||||||
|
nextHeaderLayoutY: ?number,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props: Props, context: Object) {
|
||||||
|
super(props, context);
|
||||||
|
this.state = {
|
||||||
measured: false,
|
measured: false,
|
||||||
layoutY: 0,
|
layoutY: 0,
|
||||||
layoutHeight: 0,
|
layoutHeight: 0,
|
||||||
nextHeaderLayoutY: (null: ?number),
|
nextHeaderLayoutY: props.nextHeaderLayoutY,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
setNextHeaderY(y: number) {
|
setNextHeaderY(y: number) {
|
||||||
this.setState({ nextHeaderLayoutY: y });
|
this.setState({ nextHeaderLayoutY: y });
|
||||||
@ -65,8 +76,10 @@ class ScrollViewStickyHeader extends React.Component {
|
|||||||
// scroll indefinetly.
|
// scroll indefinetly.
|
||||||
const inputRange = [-1, 0, layoutY];
|
const inputRange = [-1, 0, layoutY];
|
||||||
const outputRange: Array<number> = [0, 0, 0];
|
const outputRange: Array<number> = [0, 0, 0];
|
||||||
if (nextHeaderLayoutY != null) {
|
// Sometimes headers jump around so we make sure we don't violate the monotonic inputRange
|
||||||
const collisionPoint = nextHeaderLayoutY - layoutHeight;
|
// condition.
|
||||||
|
const collisionPoint = (nextHeaderLayoutY || 0) - layoutHeight;
|
||||||
|
if (collisionPoint >= layoutY) {
|
||||||
inputRange.push(collisionPoint, collisionPoint + 1);
|
inputRange.push(collisionPoint, collisionPoint + 1);
|
||||||
outputRange.push(collisionPoint - layoutY, collisionPoint - layoutY);
|
outputRange.push(collisionPoint - layoutY, collisionPoint - layoutY);
|
||||||
} else {
|
} else {
|
||||||
|
@ -33,6 +33,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const MetroListView = require('MetroListView');
|
const MetroListView = require('MetroListView');
|
||||||
|
const Platform = require('Platform');
|
||||||
const React = require('React');
|
const React = require('React');
|
||||||
const VirtualizedSectionList = require('VirtualizedSectionList');
|
const VirtualizedSectionList = require('VirtualizedSectionList');
|
||||||
|
|
||||||
@ -52,9 +53,7 @@ type SectionBase<SectionItemT> = {
|
|||||||
keyExtractor?: (item: SectionItemT) => string,
|
keyExtractor?: (item: SectionItemT) => string,
|
||||||
|
|
||||||
// TODO: support more optional/override props
|
// TODO: support more optional/override props
|
||||||
// FooterComponent?: ?ReactClass<*>,
|
// onViewableItemsChanged?: ...
|
||||||
// HeaderComponent?: ?ReactClass<*>,
|
|
||||||
// onViewableItemsChanged?: ({viewableItems: Array<ViewToken>, changed: Array<ViewToken>}) => void,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type RequiredProps<SectionT: SectionBase<any>> = {
|
type RequiredProps<SectionT: SectionBase<any>> = {
|
||||||
@ -102,7 +101,10 @@ type OptionalProps<SectionT: SectionBase<any>> = {
|
|||||||
* Called when the viewability of rows changes, as defined by the
|
* Called when the viewability of rows changes, as defined by the
|
||||||
* `viewabilityConfig` prop.
|
* `viewabilityConfig` prop.
|
||||||
*/
|
*/
|
||||||
onViewableItemsChanged?: ?(info: {viewableItems: Array<ViewToken>, changed: Array<ViewToken>}) => void,
|
onViewableItemsChanged?: ?(info: {
|
||||||
|
viewableItems: Array<ViewToken>,
|
||||||
|
changed: Array<ViewToken>,
|
||||||
|
}) => void,
|
||||||
/**
|
/**
|
||||||
* Set this true while waiting for new data from a refresh.
|
* Set this true while waiting for new data from a refresh.
|
||||||
*/
|
*/
|
||||||
@ -114,13 +116,23 @@ type OptionalProps<SectionT: SectionBase<any>> = {
|
|||||||
prevProps: {item: Item, index: number},
|
prevProps: {item: Item, index: number},
|
||||||
nextProps: {item: Item, index: number}
|
nextProps: {item: Item, index: number}
|
||||||
) => boolean,
|
) => boolean,
|
||||||
|
/**
|
||||||
|
* Makes section headers stick to the top of the screen until the next one pushes it off. Only
|
||||||
|
* enabled by default on iOS because that is the platform standard there.
|
||||||
|
*/
|
||||||
|
stickySectionHeadersEnabled?: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props<SectionT> = RequiredProps<SectionT>
|
type Props<SectionT> = RequiredProps<SectionT>
|
||||||
& OptionalProps<SectionT>
|
& OptionalProps<SectionT>
|
||||||
& VirtualizedSectionListProps<SectionT>;
|
& VirtualizedSectionListProps<SectionT>;
|
||||||
|
|
||||||
type DefaultProps = typeof VirtualizedSectionList.defaultProps;
|
const defaultProps = {
|
||||||
|
...VirtualizedSectionList.defaultProps,
|
||||||
|
stickySectionHeadersEnabled: Platform.OS === 'ios',
|
||||||
|
};
|
||||||
|
|
||||||
|
type DefaultProps = typeof defaultProps;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A performant interface for rendering sectioned lists, supporting the most handy features:
|
* A performant interface for rendering sectioned lists, supporting the most handy features:
|
||||||
@ -136,7 +148,8 @@ type DefaultProps = typeof VirtualizedSectionList.defaultProps;
|
|||||||
* - Pull to Refresh.
|
* - Pull to Refresh.
|
||||||
* - Scroll loading.
|
* - Scroll loading.
|
||||||
*
|
*
|
||||||
* If you don't need section support and want a simpler interface, use [`<FlatList>`](/react-native/docs/flatlist.html).
|
* If you don't need section support and want a simpler interface, use
|
||||||
|
* [`<FlatList>`](/react-native/docs/flatlist.html).
|
||||||
*
|
*
|
||||||
* If you need _sticky_ section header support, use `ListView` for now.
|
* If you need _sticky_ section header support, use `ListView` for now.
|
||||||
*
|
*
|
||||||
@ -180,7 +193,7 @@ class SectionList<SectionT: SectionBase<any>>
|
|||||||
extends React.PureComponent<DefaultProps, Props<SectionT>, void>
|
extends React.PureComponent<DefaultProps, Props<SectionT>, void>
|
||||||
{
|
{
|
||||||
props: Props<SectionT>;
|
props: Props<SectionT>;
|
||||||
static defaultProps: DefaultProps = VirtualizedSectionList.defaultProps;
|
static defaultProps: DefaultProps = defaultProps;
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const List = this.props.legacyImplementation ? MetroListView : VirtualizedSectionList;
|
const List = this.props.legacyImplementation ? MetroListView : VirtualizedSectionList;
|
||||||
|
@ -105,7 +105,10 @@ type OptionalProps = {
|
|||||||
* Called when the viewability of rows changes, as defined by the
|
* Called when the viewability of rows changes, as defined by the
|
||||||
* `viewabilityConfig` prop.
|
* `viewabilityConfig` prop.
|
||||||
*/
|
*/
|
||||||
onViewableItemsChanged?: ?(info: {viewableItems: Array<ViewToken>, changed: Array<ViewToken>}) => void,
|
onViewableItemsChanged?: ?(info: {
|
||||||
|
viewableItems: Array<ViewToken>,
|
||||||
|
changed: Array<ViewToken>,
|
||||||
|
}) => void,
|
||||||
/**
|
/**
|
||||||
* Set this true while waiting for new data from a refresh.
|
* Set this true while waiting for new data from a refresh.
|
||||||
*/
|
*/
|
||||||
@ -336,21 +339,31 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
|
|||||||
this._updateCellsToRenderBatcher.schedule();
|
this._updateCellsToRenderBatcher.schedule();
|
||||||
}
|
}
|
||||||
|
|
||||||
_pushCells(cells, first, last) {
|
_pushCells(
|
||||||
|
cells: Array<Object>,
|
||||||
|
stickyHeaderIndices: Array<number>,
|
||||||
|
stickyIndicesFromProps: Set<number>,
|
||||||
|
first: number,
|
||||||
|
last: number,
|
||||||
|
) {
|
||||||
const {ItemSeparatorComponent, data, getItem, getItemCount, keyExtractor} = this.props;
|
const {ItemSeparatorComponent, data, getItem, getItemCount, keyExtractor} = this.props;
|
||||||
|
const stickyOffset = this.props.ListHeaderComponent ? 1 : 0;
|
||||||
const end = getItemCount(data) - 1;
|
const end = getItemCount(data) - 1;
|
||||||
last = Math.min(end, last);
|
last = Math.min(end, last);
|
||||||
for (let ii = first; ii <= last; ii++) {
|
for (let ii = first; ii <= last; ii++) {
|
||||||
const item = getItem(data, ii);
|
const item = getItem(data, ii);
|
||||||
invariant(item, 'No item for index ' + ii);
|
invariant(item, 'No item for index ' + ii);
|
||||||
const key = keyExtractor(item, ii);
|
const key = keyExtractor(item, ii);
|
||||||
|
if (stickyIndicesFromProps.has(ii + stickyOffset)) {
|
||||||
|
stickyHeaderIndices.push(cells.length);
|
||||||
|
}
|
||||||
cells.push(
|
cells.push(
|
||||||
<CellRenderer
|
<CellRenderer
|
||||||
cellKey={key}
|
cellKey={key}
|
||||||
index={ii}
|
index={ii}
|
||||||
item={item}
|
item={item}
|
||||||
key={key}
|
key={key}
|
||||||
onCellLayout={this._onCellLayout}
|
onLayout={(e) => this._onCellLayout(e, key, ii)}
|
||||||
onUnmount={this._onCellUnmount}
|
onUnmount={this._onCellUnmount}
|
||||||
parentProps={this.props}
|
parentProps={this.props}
|
||||||
/>
|
/>
|
||||||
@ -364,6 +377,8 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
|
|||||||
const {ListFooterComponent, ListHeaderComponent} = this.props;
|
const {ListFooterComponent, ListHeaderComponent} = this.props;
|
||||||
const {data, disableVirtualization, horizontal} = this.props;
|
const {data, disableVirtualization, horizontal} = this.props;
|
||||||
const cells = [];
|
const cells = [];
|
||||||
|
const stickyIndicesFromProps = new Set(this.props.stickyHeaderIndices);
|
||||||
|
const stickyHeaderIndices = [];
|
||||||
if (ListHeaderComponent) {
|
if (ListHeaderComponent) {
|
||||||
cells.push(
|
cells.push(
|
||||||
<View key="$header" onLayout={this._onLayoutHeader}>
|
<View key="$header" onLayout={this._onLayoutHeader}>
|
||||||
@ -374,18 +389,45 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
|
|||||||
const itemCount = this.props.getItemCount(data);
|
const itemCount = this.props.getItemCount(data);
|
||||||
if (itemCount > 0) {
|
if (itemCount > 0) {
|
||||||
_usedIndexForKey = false;
|
_usedIndexForKey = false;
|
||||||
|
const spacerKey = !horizontal ? 'height' : 'width';
|
||||||
const lastInitialIndex = this.props.initialNumToRender - 1;
|
const lastInitialIndex = this.props.initialNumToRender - 1;
|
||||||
const {first, last} = this.state;
|
const {first, last} = this.state;
|
||||||
this._pushCells(cells, 0, lastInitialIndex);
|
this._pushCells(cells, stickyHeaderIndices, stickyIndicesFromProps, 0, lastInitialIndex);
|
||||||
if (!disableVirtualization && first > lastInitialIndex) {
|
const firstAfterInitial = Math.max(lastInitialIndex + 1, first);
|
||||||
|
if (!disableVirtualization && first > lastInitialIndex + 1) {
|
||||||
|
let insertedStickySpacer = false;
|
||||||
|
if (stickyIndicesFromProps.size > 0) {
|
||||||
|
const stickyOffset = ListHeaderComponent ? 1 : 0;
|
||||||
|
// See if there are any sticky headers in the virtualized space that we need to render.
|
||||||
|
for (let ii = firstAfterInitial - 1; ii > lastInitialIndex; ii--) {
|
||||||
|
if (stickyIndicesFromProps.has(ii + stickyOffset)) {
|
||||||
|
const initBlock = this._getFrameMetricsApprox(lastInitialIndex);
|
||||||
|
const stickyBlock = this._getFrameMetricsApprox(ii);
|
||||||
|
const leadSpace = stickyBlock.offset - (initBlock.offset + initBlock.length);
|
||||||
|
cells.push(
|
||||||
|
<View key="$sticky_lead" style={{[spacerKey]: leadSpace}} />
|
||||||
|
);
|
||||||
|
this._pushCells(cells, stickyHeaderIndices, stickyIndicesFromProps, ii, ii);
|
||||||
|
const trailSpace = this._getFrameMetricsApprox(first).offset -
|
||||||
|
(stickyBlock.offset + stickyBlock.length);
|
||||||
|
cells.push(
|
||||||
|
<View key="$sticky_trail" style={{[spacerKey]: trailSpace}} />
|
||||||
|
);
|
||||||
|
insertedStickySpacer = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!insertedStickySpacer) {
|
||||||
const initBlock = this._getFrameMetricsApprox(lastInitialIndex);
|
const initBlock = this._getFrameMetricsApprox(lastInitialIndex);
|
||||||
const firstSpace = this._getFrameMetricsApprox(first).offset -
|
const firstSpace = this._getFrameMetricsApprox(first).offset -
|
||||||
(initBlock.offset + initBlock.length);
|
(initBlock.offset + initBlock.length);
|
||||||
cells.push(
|
cells.push(
|
||||||
<View key="$lead_spacer" style={{[!horizontal ? 'height' : 'width']: firstSpace}} />
|
<View key="$lead_spacer" style={{[spacerKey]: firstSpace}} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this._pushCells(cells, Math.max(lastInitialIndex + 1, first), last);
|
}
|
||||||
|
this._pushCells(cells, stickyHeaderIndices, stickyIndicesFromProps, firstAfterInitial, last);
|
||||||
if (!this._hasWarned.keys && _usedIndexForKey) {
|
if (!this._hasWarned.keys && _usedIndexForKey) {
|
||||||
console.warn(
|
console.warn(
|
||||||
'VirtualizedList: missing keys for items, make sure to specify a key property on each ' +
|
'VirtualizedList: missing keys for items, make sure to specify a key property on each ' +
|
||||||
@ -406,7 +448,7 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
|
|||||||
(endFrame.offset + endFrame.length) -
|
(endFrame.offset + endFrame.length) -
|
||||||
(lastFrame.offset + lastFrame.length);
|
(lastFrame.offset + lastFrame.length);
|
||||||
cells.push(
|
cells.push(
|
||||||
<View key="$tail_spacer" style={{[!horizontal ? 'height' : 'width']: tailSpacerLength}} />
|
<View key="$tail_spacer" style={{[spacerKey]: tailSpacerLength}} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -426,6 +468,7 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
|
|||||||
onScrollBeginDrag: this._onScrollBeginDrag,
|
onScrollBeginDrag: this._onScrollBeginDrag,
|
||||||
ref: this._captureScrollRef,
|
ref: this._captureScrollRef,
|
||||||
scrollEventThrottle: 50, // TODO: Android support
|
scrollEventThrottle: 50, // TODO: Android support
|
||||||
|
stickyHeaderIndices,
|
||||||
},
|
},
|
||||||
cells,
|
cells,
|
||||||
);
|
);
|
||||||
@ -460,7 +503,7 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
|
|||||||
this._scrollRef = ref;
|
this._scrollRef = ref;
|
||||||
};
|
};
|
||||||
|
|
||||||
_onCellLayout = (e, cellKey, index) => {
|
_onCellLayout(e, cellKey, index) {
|
||||||
const layout = e.nativeEvent.layout;
|
const layout = e.nativeEvent.layout;
|
||||||
const next = {
|
const next = {
|
||||||
offset: this._selectOffset(layout),
|
offset: this._selectOffset(layout),
|
||||||
@ -480,8 +523,10 @@ class VirtualizedList extends React.PureComponent<OptionalProps, Props, State> {
|
|||||||
this._frames[cellKey] = next;
|
this._frames[cellKey] = next;
|
||||||
this._highestMeasuredFrameIndex = Math.max(this._highestMeasuredFrameIndex, index);
|
this._highestMeasuredFrameIndex = Math.max(this._highestMeasuredFrameIndex, index);
|
||||||
this._updateCellsToRenderBatcher.schedule();
|
this._updateCellsToRenderBatcher.schedule();
|
||||||
|
} else {
|
||||||
|
this._frames[cellKey].inLayout = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
_onCellUnmount = (cellKey: string) => {
|
_onCellUnmount = (cellKey: string) => {
|
||||||
const curr = this._frames[cellKey];
|
const curr = this._frames[cellKey];
|
||||||
@ -710,7 +755,7 @@ class CellRenderer extends React.Component {
|
|||||||
cellKey: string,
|
cellKey: string,
|
||||||
index: number,
|
index: number,
|
||||||
item: Item,
|
item: Item,
|
||||||
onCellLayout: (event: Object, cellKey: string, index: number) => void,
|
onLayout: (event: Object) => void, // This is extracted by ScrollViewStickyHeader
|
||||||
onUnmount: (cellKey: string) => void,
|
onUnmount: (cellKey: string) => void,
|
||||||
parentProps: {
|
parentProps: {
|
||||||
renderItem: renderItemType,
|
renderItem: renderItemType,
|
||||||
@ -721,9 +766,6 @@ class CellRenderer extends React.Component {
|
|||||||
) => boolean,
|
) => boolean,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
_onLayout = (e) => {
|
|
||||||
this.props.onCellLayout(e, this.props.cellKey, this.props.index);
|
|
||||||
}
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.props.onUnmount(this.props.cellKey);
|
this.props.onUnmount(this.props.cellKey);
|
||||||
}
|
}
|
||||||
@ -740,8 +782,10 @@ class CellRenderer extends React.Component {
|
|||||||
if (getItemLayout && !parentProps.debug) {
|
if (getItemLayout && !parentProps.debug) {
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
// NOTE: that when this is a sticky header, `onLayout` will get automatically extracted and
|
||||||
|
// called explicitly by `ScrollViewStickyHeader`.
|
||||||
return (
|
return (
|
||||||
<View onLayout={this._onLayout}>
|
<View onLayout={this.props.onLayout}>
|
||||||
{element}
|
{element}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
@ -79,7 +79,7 @@ type OptionalProps<SectionT: SectionBase> = {
|
|||||||
*/
|
*/
|
||||||
renderItem: ({item: Item, index: number}) => ?React.Element<*>,
|
renderItem: ({item: Item, index: number}) => ?React.Element<*>,
|
||||||
/**
|
/**
|
||||||
* Rendered at the top of each section. In the future, a sticky option will be added.
|
* Rendered at the top of each section.
|
||||||
*/
|
*/
|
||||||
renderSectionHeader?: ?({section: SectionT}) => ?React.Element<*>,
|
renderSectionHeader?: ?({section: SectionT}) => ?React.Element<*>,
|
||||||
/**
|
/**
|
||||||
@ -210,11 +210,6 @@ class VirtualizedSectionList<SectionT: SectionBase>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_isItemSticky = (item, index) => {
|
|
||||||
const info = this._subExtractor(index);
|
|
||||||
return info && info.index == null;
|
|
||||||
};
|
|
||||||
|
|
||||||
_renderItem = ({item, index}: {item: Item, index: number}) => {
|
_renderItem = ({item, index}: {item: Item, index: number}) => {
|
||||||
const info = this._subExtractor(index);
|
const info = this._subExtractor(index);
|
||||||
if (!info) {
|
if (!info) {
|
||||||
@ -263,7 +258,15 @@ class VirtualizedSectionList<SectionT: SectionBase>
|
|||||||
}
|
}
|
||||||
|
|
||||||
_computeState(props: Props<SectionT>): State {
|
_computeState(props: Props<SectionT>): State {
|
||||||
const itemCount = props.sections.reduce((v, section) => v + section.data.length + 1, 0);
|
const offset = props.ListHeaderComponent ? 1 : 0;
|
||||||
|
const stickyHeaderIndices = [];
|
||||||
|
const itemCount = props.sections.reduce(
|
||||||
|
(v, section) => {
|
||||||
|
stickyHeaderIndices.push(v + offset);
|
||||||
|
return v + section.data.length + 1;
|
||||||
|
},
|
||||||
|
0
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
childProps: {
|
childProps: {
|
||||||
...props,
|
...props,
|
||||||
@ -272,21 +275,17 @@ class VirtualizedSectionList<SectionT: SectionBase>
|
|||||||
data: props.sections,
|
data: props.sections,
|
||||||
getItemCount: () => itemCount,
|
getItemCount: () => itemCount,
|
||||||
getItem,
|
getItem,
|
||||||
isItemSticky: this._isItemSticky,
|
|
||||||
keyExtractor: this._keyExtractor,
|
keyExtractor: this._keyExtractor,
|
||||||
onViewableItemsChanged:
|
onViewableItemsChanged:
|
||||||
props.onViewableItemsChanged ? this._onViewableItemsChanged : undefined,
|
props.onViewableItemsChanged ? this._onViewableItemsChanged : undefined,
|
||||||
shouldItemUpdate: this._shouldItemUpdate,
|
shouldItemUpdate: this._shouldItemUpdate,
|
||||||
|
stickyHeaderIndices: props.stickySectionHeadersEnabled ? stickyHeaderIndices : undefined,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props: Props<SectionT>, context: Object) {
|
constructor(props: Props<SectionT>, context: Object) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
warning(
|
|
||||||
!props.stickySectionHeadersEnabled,
|
|
||||||
'VirtualizedSectionList: Sticky headers only supported with legacyImplementation for now.'
|
|
||||||
);
|
|
||||||
this.state = this._computeState(props);
|
this.state = this._computeState(props);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,6 +53,7 @@ exports[`FlatList renders all the bells and whistles 1`] = `
|
|||||||
renderScrollComponent={[Function]}
|
renderScrollComponent={[Function]}
|
||||||
scrollEventThrottle={50}
|
scrollEventThrottle={50}
|
||||||
shouldItemUpdate={[Function]}
|
shouldItemUpdate={[Function]}
|
||||||
|
stickyHeaderIndices={Array []}
|
||||||
updateCellsBatchingPeriod={50}
|
updateCellsBatchingPeriod={50}
|
||||||
windowSize={21}
|
windowSize={21}
|
||||||
>
|
>
|
||||||
@ -145,6 +146,7 @@ exports[`FlatList renders empty list 1`] = `
|
|||||||
renderScrollComponent={[Function]}
|
renderScrollComponent={[Function]}
|
||||||
scrollEventThrottle={50}
|
scrollEventThrottle={50}
|
||||||
shouldItemUpdate={[Function]}
|
shouldItemUpdate={[Function]}
|
||||||
|
stickyHeaderIndices={Array []}
|
||||||
updateCellsBatchingPeriod={50}
|
updateCellsBatchingPeriod={50}
|
||||||
windowSize={21}
|
windowSize={21}
|
||||||
>
|
>
|
||||||
@ -175,6 +177,7 @@ exports[`FlatList renders null list 1`] = `
|
|||||||
renderScrollComponent={[Function]}
|
renderScrollComponent={[Function]}
|
||||||
scrollEventThrottle={50}
|
scrollEventThrottle={50}
|
||||||
shouldItemUpdate={[Function]}
|
shouldItemUpdate={[Function]}
|
||||||
|
stickyHeaderIndices={Array []}
|
||||||
updateCellsBatchingPeriod={50}
|
updateCellsBatchingPeriod={50}
|
||||||
windowSize={21}
|
windowSize={21}
|
||||||
>
|
>
|
||||||
@ -217,6 +220,7 @@ exports[`FlatList renders simple list 1`] = `
|
|||||||
renderScrollComponent={[Function]}
|
renderScrollComponent={[Function]}
|
||||||
scrollEventThrottle={50}
|
scrollEventThrottle={50}
|
||||||
shouldItemUpdate={[Function]}
|
shouldItemUpdate={[Function]}
|
||||||
|
stickyHeaderIndices={Array []}
|
||||||
updateCellsBatchingPeriod={50}
|
updateCellsBatchingPeriod={50}
|
||||||
windowSize={21}
|
windowSize={21}
|
||||||
>
|
>
|
||||||
|
@ -23,7 +23,6 @@ exports[`SectionList rendering empty section headers is fine 1`] = `
|
|||||||
getItemCount={[Function]}
|
getItemCount={[Function]}
|
||||||
horizontal={false}
|
horizontal={false}
|
||||||
initialNumToRender={10}
|
initialNumToRender={10}
|
||||||
isItemSticky={[Function]}
|
|
||||||
keyExtractor={[Function]}
|
keyExtractor={[Function]}
|
||||||
maxToRenderPerBatch={10}
|
maxToRenderPerBatch={10}
|
||||||
onContentSizeChange={[Function]}
|
onContentSizeChange={[Function]}
|
||||||
@ -54,6 +53,12 @@ exports[`SectionList rendering empty section headers is fine 1`] = `
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
shouldItemUpdate={[Function]}
|
shouldItemUpdate={[Function]}
|
||||||
|
stickyHeaderIndices={
|
||||||
|
Array [
|
||||||
|
0,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
stickySectionHeadersEnabled={true}
|
||||||
updateCellsBatchingPeriod={50}
|
updateCellsBatchingPeriod={50}
|
||||||
windowSize={21}
|
windowSize={21}
|
||||||
>
|
>
|
||||||
@ -123,7 +128,6 @@ exports[`SectionList renders all the bells and whistles 1`] = `
|
|||||||
getItemCount={[Function]}
|
getItemCount={[Function]}
|
||||||
horizontal={false}
|
horizontal={false}
|
||||||
initialNumToRender={10}
|
initialNumToRender={10}
|
||||||
isItemSticky={[Function]}
|
|
||||||
keyExtractor={[Function]}
|
keyExtractor={[Function]}
|
||||||
maxToRenderPerBatch={10}
|
maxToRenderPerBatch={10}
|
||||||
onContentSizeChange={[Function]}
|
onContentSizeChange={[Function]}
|
||||||
@ -176,6 +180,13 @@ exports[`SectionList renders all the bells and whistles 1`] = `
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
shouldItemUpdate={[Function]}
|
shouldItemUpdate={[Function]}
|
||||||
|
stickyHeaderIndices={
|
||||||
|
Array [
|
||||||
|
1,
|
||||||
|
4,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
stickySectionHeadersEnabled={true}
|
||||||
updateCellsBatchingPeriod={50}
|
updateCellsBatchingPeriod={50}
|
||||||
windowSize={21}
|
windowSize={21}
|
||||||
>
|
>
|
||||||
@ -257,7 +268,6 @@ exports[`SectionList renders empty list 1`] = `
|
|||||||
getItemCount={[Function]}
|
getItemCount={[Function]}
|
||||||
horizontal={false}
|
horizontal={false}
|
||||||
initialNumToRender={10}
|
initialNumToRender={10}
|
||||||
isItemSticky={[Function]}
|
|
||||||
keyExtractor={[Function]}
|
keyExtractor={[Function]}
|
||||||
maxToRenderPerBatch={10}
|
maxToRenderPerBatch={10}
|
||||||
onContentSizeChange={[Function]}
|
onContentSizeChange={[Function]}
|
||||||
@ -273,6 +283,8 @@ exports[`SectionList renders empty list 1`] = `
|
|||||||
scrollEventThrottle={50}
|
scrollEventThrottle={50}
|
||||||
sections={Array []}
|
sections={Array []}
|
||||||
shouldItemUpdate={[Function]}
|
shouldItemUpdate={[Function]}
|
||||||
|
stickyHeaderIndices={Array []}
|
||||||
|
stickySectionHeadersEnabled={true}
|
||||||
updateCellsBatchingPeriod={50}
|
updateCellsBatchingPeriod={50}
|
||||||
windowSize={21}
|
windowSize={21}
|
||||||
>
|
>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user