Skip to content

Commit ecaca80

Browse files
janicduplessisfacebook-github-bot
authored andcommitted
Support sticky headers for inverted Lists
Summary: Sticky headers for inverted lists should still stick at the top of the list instead of the bottom. Tested by adding the inverted prop to the SectionList example in RNTester. It does add a prop to ScrollView but it's very specific to the inverted list implementation, not sure if it should be documented. [GENERAL][ENHANCEMENT][LISTS] - Support sticky headers for inverted Lists Closes #17762 Differential Revision: D6830784 Pulled By: sahrens fbshipit-source-id: 6841fdd46e04b30547659d85ff54c3a21c61a8a2
1 parent 429fcc8 commit ecaca80

File tree

6 files changed

+124
-36
lines changed

6 files changed

+124
-36
lines changed

Libraries/Components/ScrollView/ScrollView.js

+22-2
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,11 @@ const ScrollView = createReactClass({
190190
'black',
191191
'white',
192192
]),
193+
/**
194+
* If sticky headers should stick at the bottom instead of the top of the
195+
* ScrollView. This is usually used with inverted ScrollViews.
196+
*/
197+
invertStickyHeaders: PropTypes.bool,
193198
/**
194199
* When true, the ScrollView will try to lock to only vertical or horizontal
195200
* scrolling while dragging. The default value is false.
@@ -499,7 +504,10 @@ const ScrollView = createReactClass({
499504
_stickyHeaderRefs: (new Map(): Map<number, ScrollViewStickyHeader>),
500505
_headerLayoutYs: (new Map(): Map<string, number>),
501506
getInitialState: function() {
502-
return this.scrollResponderMixinGetInitialState();
507+
return {
508+
...this.scrollResponderMixinGetInitialState(),
509+
layoutHeight: null,
510+
};
503511
},
504512

505513
componentWillMount: function() {
@@ -676,6 +684,15 @@ const ScrollView = createReactClass({
676684
this.scrollResponderHandleScroll(e);
677685
},
678686

687+
_handleLayout: function(e: Object) {
688+
if (this.props.invertStickyHeaders) {
689+
this.setState({ layoutHeight: e.nativeEvent.layout.height });
690+
}
691+
if (this.props.onLayout) {
692+
this.props.onLayout(e);
693+
}
694+
},
695+
679696
_handleContentOnLayout: function(e: Object) {
680697
const {width, height} = e.nativeEvent.layout;
681698
this.props.onContentSizeChange && this.props.onContentSizeChange(width, height);
@@ -761,7 +778,9 @@ const ScrollView = createReactClass({
761778
this._headerLayoutYs.get(this._getKeyForIndex(nextIndex, childArray))
762779
}
763780
onLayout={(event) => this._onStickyHeaderLayout(index, event, key)}
764-
scrollAnimatedValue={this._scrollAnimatedValue}>
781+
scrollAnimatedValue={this._scrollAnimatedValue}
782+
inverted={this.props.invertStickyHeaders}
783+
scrollViewHeight={this.state.layoutHeight}>
765784
{child}
766785
</ScrollViewStickyHeader>
767786
);
@@ -808,6 +827,7 @@ const ScrollView = createReactClass({
808827
// Override the onContentSizeChange from props, since this event can
809828
// bubble up from TextInputs
810829
onContentSizeChange: null,
830+
onLayout: this._handleLayout,
811831
onMomentumScrollBegin: this.scrollResponderHandleMomentumScrollBegin,
812832
onMomentumScrollEnd: this.scrollResponderHandleMomentumScrollEnd,
813833
onResponderGrant: this.scrollResponderHandleResponderGrant,

Libraries/Components/ScrollView/ScrollViewStickyHeader.js

+79-34
Original file line numberDiff line numberDiff line change
@@ -8,41 +8,48 @@
88
*
99
* @providesModule ScrollViewStickyHeader
1010
* @flow
11+
* @format
1112
*/
1213
'use strict';
1314

1415
const Animated = require('Animated');
1516
const React = require('React');
1617
const StyleSheet = require('StyleSheet');
1718

19+
import type {LayoutEvent} from 'CoreEventTypes';
20+
1821
type Props = {
1922
children?: React.Element<any>,
2023
nextHeaderLayoutY: ?number,
21-
onLayout: (event: Object) => void,
24+
onLayout: (event: LayoutEvent) => void,
2225
scrollAnimatedValue: Animated.Value,
26+
// Will cause sticky headers to stick at the bottom of the ScrollView instead
27+
// of the top.
28+
inverted: ?boolean,
29+
// The height of the parent ScrollView. Currently only set when inverted.
30+
scrollViewHeight: ?number,
2331
};
2432

25-
class ScrollViewStickyHeader extends React.Component<Props, {
33+
type State = {
2634
measured: boolean,
2735
layoutY: number,
2836
layoutHeight: number,
2937
nextHeaderLayoutY: ?number,
30-
}> {
31-
constructor(props: Props, context: Object) {
32-
super(props, context);
33-
this.state = {
34-
measured: false,
35-
layoutY: 0,
36-
layoutHeight: 0,
37-
nextHeaderLayoutY: props.nextHeaderLayoutY,
38-
};
39-
}
38+
};
39+
40+
class ScrollViewStickyHeader extends React.Component<Props, State> {
41+
state = {
42+
measured: false,
43+
layoutY: 0,
44+
layoutHeight: 0,
45+
nextHeaderLayoutY: this.props.nextHeaderLayoutY,
46+
};
4047

4148
setNextHeaderY(y: number) {
42-
this.setState({ nextHeaderLayoutY: y });
49+
this.setState({nextHeaderLayoutY: y});
4350
}
4451

45-
_onLayout = (event) => {
52+
_onLayout = event => {
4653
this.setState({
4754
measured: true,
4855
layoutY: event.nativeEvent.layout.y,
@@ -57,32 +64,70 @@ class ScrollViewStickyHeader extends React.Component<Props, {
5764
};
5865

5966
render() {
67+
const {inverted, scrollViewHeight} = this.props;
6068
const {measured, layoutHeight, layoutY, nextHeaderLayoutY} = this.state;
6169
const inputRange: Array<number> = [-1, 0];
6270
const outputRange: Array<number> = [0, 0];
6371

6472
if (measured) {
65-
// The interpolation looks like:
66-
// - Negative scroll: no translation
67-
// - From 0 to the y of the header: no translation. This will cause the header
68-
// to scroll normally until it reaches the top of the scroll view.
69-
// - From header y to when the next header y hits the bottom edge of the header: translate
70-
// equally to scroll. This will cause the header to stay at the top of the scroll view.
71-
// - Past the collision with the next header y: no more translation. This will cause the
72-
// header to continue scrolling up and make room for the next sticky header.
73-
// In the case that there is no next header just translate equally to
74-
// scroll indefinitely.
75-
inputRange.push(layoutY);
76-
outputRange.push(0);
77-
// Sometimes headers jump around so we make sure we don't violate the monotonic inputRange
78-
// condition.
79-
const collisionPoint = (nextHeaderLayoutY || 0) - layoutHeight;
80-
if (collisionPoint >= layoutY) {
81-
inputRange.push(collisionPoint, collisionPoint + 1);
82-
outputRange.push(collisionPoint - layoutY, collisionPoint - layoutY);
73+
if (inverted) {
74+
// The interpolation looks like:
75+
// - Negative scroll: no translation
76+
// - `stickStartPoint` is the point at which the header will start sticking.
77+
// It is calculated using the ScrollView viewport height so it is a the bottom.
78+
// - Headers that are in the initial viewport will never stick, `stickStartPoint`
79+
// will be negative.
80+
// - From 0 to `stickStartPoint` no translation. This will cause the header
81+
// to scroll normally until it reaches the top of the scroll view.
82+
// - From `stickStartPoint` to when the next header y hits the bottom edge of the header: translate
83+
// equally to scroll. This will cause the header to stay at the top of the scroll view.
84+
// - Past the collision with the next header y: no more translation. This will cause the
85+
// header to continue scrolling up and make room for the next sticky header.
86+
// In the case that there is no next header just translate equally to
87+
// scroll indefinitely.
88+
if (scrollViewHeight != null) {
89+
const stickStartPoint = layoutY + layoutHeight - scrollViewHeight;
90+
if (stickStartPoint > 0) {
91+
inputRange.push(stickStartPoint);
92+
outputRange.push(0);
93+
inputRange.push(stickStartPoint + 1);
94+
outputRange.push(1);
95+
// If the next sticky header has not loaded yet (probably windowing) or is the last
96+
// we can just keep it sticked forever.
97+
const collisionPoint =
98+
(nextHeaderLayoutY || 0) - layoutHeight - scrollViewHeight;
99+
if (collisionPoint > stickStartPoint) {
100+
inputRange.push(collisionPoint, collisionPoint + 1);
101+
outputRange.push(
102+
collisionPoint - stickStartPoint,
103+
collisionPoint - stickStartPoint,
104+
);
105+
}
106+
}
107+
}
83108
} else {
84-
inputRange.push(layoutY + 1);
85-
outputRange.push(1);
109+
// The interpolation looks like:
110+
// - Negative scroll: no translation
111+
// - From 0 to the y of the header: no translation. This will cause the header
112+
// to scroll normally until it reaches the top of the scroll view.
113+
// - From header y to when the next header y hits the bottom edge of the header: translate
114+
// equally to scroll. This will cause the header to stay at the top of the scroll view.
115+
// - Past the collision with the next header y: no more translation. This will cause the
116+
// header to continue scrolling up and make room for the next sticky header.
117+
// In the case that there is no next header just translate equally to
118+
// scroll indefinitely.
119+
inputRange.push(layoutY);
120+
outputRange.push(0);
121+
// If the next sticky header has not loaded yet (probably windowing) or is the last
122+
// we can just keep it sticked forever.
123+
const collisionPoint = (nextHeaderLayoutY || 0) - layoutHeight;
124+
if (collisionPoint >= layoutY) {
125+
inputRange.push(collisionPoint, collisionPoint + 1);
126+
outputRange.push(collisionPoint - layoutY, collisionPoint - layoutY);
127+
} else {
128+
inputRange.push(layoutY + 1);
129+
outputRange.push(1);
130+
}
86131
}
87132
}
88133

Libraries/Lists/VirtualizedList.js

+1
Original file line numberDiff line numberDiff line change
@@ -898,6 +898,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
898898
onScrollEndDrag: this._onScrollEndDrag,
899899
onMomentumScrollEnd: this._onMomentumScrollEnd,
900900
scrollEventThrottle: this.props.scrollEventThrottle, // TODO: Android support
901+
invertStickyHeaders: this.props.inverted,
901902
stickyHeaderIndices,
902903
};
903904
if (inversionStyle) {

Libraries/Lists/__tests__/__snapshots__/FlatList-test.js.snap

+4
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ exports[`FlatList renders all the bells and whistles 1`] = `
3131
getItemLayout={[Function]}
3232
horizontal={false}
3333
initialNumToRender={10}
34+
invertStickyHeaders={undefined}
3435
keyExtractor={[Function]}
3536
maxToRenderPerBatch={10}
3637
numColumns={2}
@@ -148,6 +149,7 @@ exports[`FlatList renders empty list 1`] = `
148149
getItemCount={[Function]}
149150
horizontal={false}
150151
initialNumToRender={10}
152+
invertStickyHeaders={undefined}
151153
keyExtractor={[Function]}
152154
maxToRenderPerBatch={10}
153155
numColumns={1}
@@ -177,6 +179,7 @@ exports[`FlatList renders null list 1`] = `
177179
getItemCount={[Function]}
178180
horizontal={false}
179181
initialNumToRender={10}
182+
invertStickyHeaders={undefined}
180183
keyExtractor={[Function]}
181184
maxToRenderPerBatch={10}
182185
numColumns={1}
@@ -218,6 +221,7 @@ exports[`FlatList renders simple list 1`] = `
218221
getItemCount={[Function]}
219222
horizontal={false}
220223
initialNumToRender={10}
224+
invertStickyHeaders={undefined}
221225
keyExtractor={[Function]}
222226
maxToRenderPerBatch={10}
223227
numColumns={1}

Libraries/Lists/__tests__/__snapshots__/SectionList-test.js.snap

+5
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ exports[`SectionList rendering empty section headers is fine 1`] = `
2323
getItemCount={[Function]}
2424
horizontal={false}
2525
initialNumToRender={10}
26+
invertStickyHeaders={undefined}
2627
keyExtractor={[Function]}
2728
maxToRenderPerBatch={10}
2829
onContentSizeChange={[Function]}
@@ -105,6 +106,7 @@ exports[`SectionList renders a footer when there is no data 1`] = `
105106
getItemCount={[Function]}
106107
horizontal={false}
107108
initialNumToRender={10}
109+
invertStickyHeaders={undefined}
108110
keyExtractor={[Function]}
109111
maxToRenderPerBatch={10}
110112
onContentSizeChange={[Function]}
@@ -173,6 +175,7 @@ exports[`SectionList renders a footer when there is no data and no header 1`] =
173175
getItemCount={[Function]}
174176
horizontal={false}
175177
initialNumToRender={10}
178+
invertStickyHeaders={undefined}
176179
keyExtractor={[Function]}
177180
maxToRenderPerBatch={10}
178181
onContentSizeChange={[Function]}
@@ -272,6 +275,7 @@ exports[`SectionList renders all the bells and whistles 1`] = `
272275
getItemCount={[Function]}
273276
horizontal={false}
274277
initialNumToRender={Infinity}
278+
invertStickyHeaders={undefined}
275279
keyExtractor={[Function]}
276280
maxToRenderPerBatch={10}
277281
onContentSizeChange={[Function]}
@@ -512,6 +516,7 @@ exports[`SectionList renders empty list 1`] = `
512516
getItemCount={[Function]}
513517
horizontal={false}
514518
initialNumToRender={10}
519+
invertStickyHeaders={undefined}
515520
keyExtractor={[Function]}
516521
maxToRenderPerBatch={10}
517522
onContentSizeChange={[Function]}

Libraries/Lists/__tests__/__snapshots__/VirtualizedList-test.js.snap

+13
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ exports[`VirtualizedList handles nested lists 1`] = `
1717
getItemCount={[Function]}
1818
horizontal={false}
1919
initialNumToRender={10}
20+
invertStickyHeaders={undefined}
2021
keyExtractor={[Function]}
2122
maxToRenderPerBatch={10}
2223
onContentSizeChange={[Function]}
@@ -53,6 +54,7 @@ exports[`VirtualizedList handles nested lists 1`] = `
5354
getItemCount={[Function]}
5455
horizontal={false}
5556
initialNumToRender={10}
57+
invertStickyHeaders={undefined}
5658
keyExtractor={[Function]}
5759
maxToRenderPerBatch={10}
5860
onContentSizeChange={[Function]}
@@ -106,6 +108,7 @@ exports[`VirtualizedList handles nested lists 1`] = `
106108
getItemCount={[Function]}
107109
horizontal={true}
108110
initialNumToRender={10}
111+
invertStickyHeaders={undefined}
109112
keyExtractor={[Function]}
110113
maxToRenderPerBatch={10}
111114
onContentSizeChange={[Function]}
@@ -180,6 +183,7 @@ exports[`VirtualizedList handles separators correctly 1`] = `
180183
getItemCount={[Function]}
181184
horizontal={false}
182185
initialNumToRender={10}
186+
invertStickyHeaders={undefined}
183187
keyExtractor={[Function]}
184188
maxToRenderPerBatch={10}
185189
onContentSizeChange={[Function]}
@@ -261,6 +265,7 @@ exports[`VirtualizedList handles separators correctly 2`] = `
261265
getItemCount={[Function]}
262266
horizontal={false}
263267
initialNumToRender={10}
268+
invertStickyHeaders={undefined}
264269
keyExtractor={[Function]}
265270
maxToRenderPerBatch={10}
266271
onContentSizeChange={[Function]}
@@ -342,6 +347,7 @@ exports[`VirtualizedList handles separators correctly 3`] = `
342347
getItemCount={[Function]}
343348
horizontal={false}
344349
initialNumToRender={10}
350+
invertStickyHeaders={undefined}
345351
keyExtractor={[Function]}
346352
maxToRenderPerBatch={10}
347353
onContentSizeChange={[Function]}
@@ -434,6 +440,7 @@ exports[`VirtualizedList renders all the bells and whistles 1`] = `
434440
getItemLayout={[Function]}
435441
horizontal={false}
436442
initialNumToRender={10}
443+
invertStickyHeaders={true}
437444
inverted={true}
438445
keyExtractor={[Function]}
439446
maxToRenderPerBatch={10}
@@ -622,6 +629,7 @@ exports[`VirtualizedList renders empty list 1`] = `
622629
getItemCount={[Function]}
623630
horizontal={false}
624631
initialNumToRender={10}
632+
invertStickyHeaders={undefined}
625633
keyExtractor={[Function]}
626634
maxToRenderPerBatch={10}
627635
onContentSizeChange={[Function]}
@@ -652,6 +660,7 @@ exports[`VirtualizedList renders empty list with empty component 1`] = `
652660
getItemCount={[Function]}
653661
horizontal={false}
654662
initialNumToRender={10}
663+
invertStickyHeaders={undefined}
655664
keyExtractor={[Function]}
656665
maxToRenderPerBatch={10}
657666
onContentSizeChange={[Function]}
@@ -705,6 +714,7 @@ exports[`VirtualizedList renders list with empty component 1`] = `
705714
getItemCount={[Function]}
706715
horizontal={false}
707716
initialNumToRender={10}
717+
invertStickyHeaders={undefined}
708718
keyExtractor={[Function]}
709719
maxToRenderPerBatch={10}
710720
onContentSizeChange={[Function]}
@@ -741,6 +751,7 @@ exports[`VirtualizedList renders null list 1`] = `
741751
getItemCount={[Function]}
742752
horizontal={false}
743753
initialNumToRender={10}
754+
invertStickyHeaders={undefined}
744755
keyExtractor={[Function]}
745756
maxToRenderPerBatch={10}
746757
onContentSizeChange={[Function]}
@@ -780,6 +791,7 @@ exports[`VirtualizedList renders simple list 1`] = `
780791
getItemCount={[Function]}
781792
horizontal={false}
782793
initialNumToRender={10}
794+
invertStickyHeaders={undefined}
783795
keyExtractor={[Function]}
784796
maxToRenderPerBatch={10}
785797
onContentSizeChange={[Function]}
@@ -838,6 +850,7 @@ exports[`VirtualizedList test getItem functionality where data is not an Array 1
838850
getItemCount={[Function]}
839851
horizontal={false}
840852
initialNumToRender={10}
853+
invertStickyHeaders={undefined}
841854
keyExtractor={[Function]}
842855
maxToRenderPerBatch={10}
843856
onContentSizeChange={[Function]}

0 commit comments

Comments
 (0)