8
8
*
9
9
* @providesModule ScrollViewStickyHeader
10
10
* @flow
11
+ * @format
11
12
*/
12
13
'use strict' ;
13
14
14
15
const Animated = require ( 'Animated' ) ;
15
16
const React = require ( 'React' ) ;
16
17
const StyleSheet = require ( 'StyleSheet' ) ;
17
18
19
+ import type { LayoutEvent } from 'CoreEventTypes' ;
20
+
18
21
type Props = {
19
22
children ?: React . Element < any > ,
20
23
nextHeaderLayoutY : ?number ,
21
- onLayout : ( event : Object ) => void ,
24
+ onLayout : ( event : LayoutEvent ) => void ,
22
25
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 ,
23
31
} ;
24
32
25
- class ScrollViewStickyHeader extends React . Component < Props , {
33
+ type State = {
26
34
measured : boolean ,
27
35
layoutY : number ,
28
36
layoutHeight : number ,
29
37
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
+ } ;
40
47
41
48
setNextHeaderY ( y : number ) {
42
- this . setState ( { nextHeaderLayoutY : y } ) ;
49
+ this . setState ( { nextHeaderLayoutY : y } ) ;
43
50
}
44
51
45
- _onLayout = ( event ) => {
52
+ _onLayout = event => {
46
53
this . setState ( {
47
54
measured : true ,
48
55
layoutY : event . nativeEvent . layout . y ,
@@ -57,32 +64,70 @@ class ScrollViewStickyHeader extends React.Component<Props, {
57
64
} ;
58
65
59
66
render ( ) {
67
+ const { inverted , scrollViewHeight } = this . props ;
60
68
const { measured, layoutHeight, layoutY, nextHeaderLayoutY} = this . state ;
61
69
const inputRange : Array < number > = [-1, 0];
62
70
const outputRange: Array< number > = [0, 0];
63
71
64
72
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
+ }
83
108
} 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
+ }
86
131
}
87
132
}
88
133
0 commit comments