Skip to content

Commit 72e2806

Browse files
committed
Allow to change the Scroll component
1 parent 0bb06fd commit 72e2806

File tree

2 files changed

+192
-72
lines changed

2 files changed

+192
-72
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ All of the properties of `ScrollView` are supported. Please refer to the
5050
The `HeaderImageScrollView` handle also the following props. None is required :
5151

5252
### Header
53+
5354
| Property | Type | Default | Description |
5455
| -------- | ---- | ------- | ----------- |
5556
| `renderHeader` | `function` | Empty view | Function which return the component to use as header. It can return background image for example. |
@@ -70,6 +71,12 @@ The `HeaderImageScrollView` handle also the following props. None is required :
7071
| `fadeOutForeground` | `bool` | `false` | If set, add a fade out effect on the foreground when scroll up |
7172
| `renderTouchableFixedForeground` | `function` | Empty view | Same as `renderFixedForeground` but allow to use touchable in it. [*Can cause performances issues on Android*](https://github.com/bamlab/react-native-image-header-scroll-view/issues/6)|
7273

74+
### Mixed
75+
76+
| Property | Type | Default | Description |
77+
| -------- | ---- | ------- | ----------- |
78+
| `ScrollViewComponent` | `Component` | `ScrollView` | The component to be used for scrolling. Can be any component with an `onScroll` props (ie. `ListView`, `FlatList`, `SectionList` or `ScrollView`) |
79+
7380

7481
### TriggeringView
7582

src/ImageHeaderScrollView.js

Lines changed: 185 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
// @flow
22
import React, { Component } from 'react';
33
import PropTypes from 'prop-types';
4-
import { Animated, ScrollView, StyleSheet, View } from 'react-native';
4+
import { Animated, ScrollView, StyleSheet, View, Platform } from 'react-native';
55
import type { ViewProps } from 'ViewPropTypes';
6+
import type { FlatList, SectionList, ListView } from 'react-native';
67

7-
export type Props = {
8+
type ScrollViewProps = {
9+
onScroll?: ?Function,
10+
style?: $PropertyType<ViewProps, 'style'>,
11+
contentContainerStyle?: $PropertyType<ViewProps, 'style'>,
12+
scrollEventThrottle: number,
13+
};
14+
15+
export type Props = ScrollViewProps & {
816
children?: ?React$Element<any>,
917
childrenStyle?: ?any,
1018
overlayColor: string,
@@ -15,11 +23,10 @@ export type Props = {
1523
minHeight: number,
1624
minOverlayOpacity: number,
1725
renderFixedForeground: () => React$Element<any>,
18-
renderForeground: () => React$Element<any>,
26+
renderForeground?: () => React$Element<any>,
1927
renderHeader: () => React$Element<any>,
2028
renderTouchableFixedForeground?: ?() => React$Element<any>,
21-
style?: $PropertyType<ViewProps, 'style'>,
22-
onScroll?: ?Function,
29+
ScrollViewComponent: React$ComponentType<ScrollViewProps>,
2330
};
2431

2532
export type DefaultProps = {
@@ -31,18 +38,20 @@ export type DefaultProps = {
3138
minHeight: number,
3239
minOverlayOpacity: number,
3340
renderFixedForeground: () => React$Element<any>,
34-
renderForeground: () => React$Element<any>,
3541
renderHeader: () => React$Element<any>,
42+
ScrollViewComponent: React$ComponentType<ScrollViewProps>,
3643
};
3744

3845
export type State = {
3946
scrollY: Animated.Value,
4047
pageY: number,
4148
};
4249

50+
type ScrollComponent<ItemT> = FlatList<ItemT> | SectionList<ItemT> | ListView | ScrollView;
51+
4352
class ImageHeaderScrollView extends Component<Props, State> {
4453
container: ?View; // @see https://github.com/facebook/react-native/issues/15955
45-
scrollViewRef: ?ScrollView; // @see https://github.com/facebook/react-native/issues/15955
54+
scrollViewRef: ?ScrollComponent<any>; // @see https://github.com/facebook/react-native/issues/15955
4655
state: State;
4756

4857
static defaultProps: DefaultProps = {
@@ -54,8 +63,8 @@ class ImageHeaderScrollView extends Component<Props, State> {
5463
minHeight: 80,
5564
minOverlayOpacity: 0,
5665
renderFixedForeground: () => <View />,
57-
renderForeground: () => <View />,
5866
renderHeader: () => <View />,
67+
ScrollViewComponent: ScrollView,
5968
};
6069

6170
constructor(props: Props) {
@@ -73,44 +82,6 @@ class ImageHeaderScrollView extends Component<Props, State> {
7382
};
7483
}
7584

76-
/*
77-
* Expose `ScrollView` API so this component is composable
78-
* with any component that expects a `ScrollView`.
79-
*/
80-
getScrollResponder() {
81-
if (!this.scrollViewRef) {
82-
return;
83-
}
84-
return this.scrollViewRef.getScrollResponder();
85-
}
86-
getScrollableNode() {
87-
const responder = this.getScrollResponder();
88-
if (!responder) {
89-
return;
90-
}
91-
return responder.getScrollableNode();
92-
}
93-
getInnerViewNode() {
94-
const responder = this.getScrollResponder();
95-
if (!responder) {
96-
return;
97-
}
98-
return responder.getInnerViewNode();
99-
}
100-
setNativeProps(props: Props) {
101-
if (!this.scrollViewRef) {
102-
return;
103-
}
104-
this.scrollViewRef.setNativeProps(props);
105-
}
106-
scrollTo(...args: *) {
107-
const responder = this.getScrollResponder();
108-
if (!responder) {
109-
return;
110-
}
111-
responder.scrollTo(...args);
112-
}
113-
11485
interpolateOnImageHeight(outputRange: Array<number>) {
11586
const headerScrollDistance = this.props.maxHeight - this.props.minHeight;
11687
return this.state.scrollY.interpolate({
@@ -164,6 +135,11 @@ class ImageHeaderScrollView extends Component<Props, State> {
164135
transform: [{ translateY: headerTranslate }],
165136
opacity: this.props.fadeOutForeground ? opacity : 1,
166137
};
138+
139+
if (!this.props.renderForeground) {
140+
return <View />;
141+
}
142+
167143
return (
168144
<Animated.View style={[styles.header, headerTransformStyle]}>
169145
{this.props.renderForeground()}
@@ -203,10 +179,17 @@ class ImageHeaderScrollView extends Component<Props, State> {
203179
this.container.measureInWindow((x, y) => this.setState(() => ({ pageY: y })));
204180
};
205181

182+
onScroll = (e: *) => {
183+
if (this.props.onScroll) {
184+
this.props.onScroll(e);
185+
}
186+
const scrollY = e.nativeEvent.contentOffset.y;
187+
this.state.scrollY.setValue(scrollY);
188+
};
189+
206190
render() {
207191
/* eslint-disable no-unused-vars */
208192
const {
209-
children,
210193
childrenStyle,
211194
overlayColor,
212195
fadeOutForeground,
@@ -220,48 +203,178 @@ class ImageHeaderScrollView extends Component<Props, State> {
220203
renderHeader,
221204
renderTouchableFixedForeground,
222205
style,
206+
contentContainerStyle,
223207
onScroll,
208+
ScrollViewComponent,
224209
...scrollViewProps
225210
} = this.props;
226211
/* eslint-enable no-unused-vars */
227212

228-
const headerScrollDistance = this.interpolateOnImageHeight([maxHeight, maxHeight - minHeight]);
229-
const topMargin = this.interpolateOnImageHeight([0, minHeight]);
230-
231-
const childrenContainerStyle = StyleSheet.flatten([
232-
{ transform: [{ translateY: headerScrollDistance }] },
233-
{ backgroundColor: 'white', paddingBottom: maxHeight },
234-
childrenStyle,
235-
]);
213+
const inset = maxHeight - minHeight;
236214

237215
return (
238216
<View
239-
style={styles.container}
217+
style={[styles.container, { paddingTop: minHeight }]}
240218
ref={ref => (this.container = ref)}
241219
onLayout={this.onContainerLayout}
242220
>
243221
{this.renderHeader()}
244-
<Animated.View style={[styles.container, { transform: [{ translateY: topMargin }] }]}>
245-
<ScrollView
246-
ref={ref => (this.scrollViewRef = ref)}
247-
scrollEventThrottle={16}
248-
{...scrollViewProps}
249-
style={[styles.container, style]}
250-
onScroll={Animated.event(
251-
[{ nativeEvent: { contentOffset: { y: this.state.scrollY } } }],
252-
{
253-
listener: onScroll,
254-
}
255-
)}
256-
>
257-
<Animated.View style={childrenContainerStyle}>{children}</Animated.View>
258-
</ScrollView>
259-
</Animated.View>
222+
<ScrollViewComponent
223+
ref={ref => (this.scrollViewRef = ref)}
224+
scrollEventThrottle={16}
225+
overScrollMode="never"
226+
{...scrollViewProps}
227+
contentContainerStyle={[
228+
{
229+
backgroundColor: 'white',
230+
marginTop: inset,
231+
paddingBottom: inset,
232+
},
233+
contentContainerStyle,
234+
childrenStyle,
235+
]}
236+
style={[styles.container, style]}
237+
onScroll={this.onScroll}
238+
/>
260239
{this.renderTouchableFixedForeground()}
261240
{this.renderForeground()}
262241
</View>
263242
);
264243
}
244+
245+
/*
246+
* Expose `ScrollView` API so this component is composable
247+
* with any component that expects a `ScrollView`.
248+
*/
249+
getScrollableNode(): any {
250+
const responder = this.getScrollResponder();
251+
if (!responder) {
252+
return;
253+
}
254+
return responder.getScrollableNode();
255+
}
256+
getInnerViewNode(): any {
257+
const responder = this.getScrollResponder();
258+
if (!responder) {
259+
return;
260+
}
261+
return responder.getInnerViewNode();
262+
}
263+
264+
scrollTo(
265+
y?: number | { x?: number, y?: number, animated?: boolean },
266+
x?: number,
267+
animated?: boolean
268+
) {
269+
const responder = this.getScrollResponder();
270+
if (!responder) {
271+
return;
272+
}
273+
responder.scrollTo(y, x, animated);
274+
}
275+
276+
scrollToEnd(params?: ?{ animated?: ?boolean }) {
277+
if (
278+
this.scrollViewRef &&
279+
this.scrollViewRef.scrollToEnd &&
280+
typeof this.scrollViewRef.scrollToEnd === 'function'
281+
) {
282+
this.scrollViewRef.scrollToEnd(params);
283+
}
284+
}
285+
286+
getScrollResponder(): ?ScrollView {
287+
if (this.scrollViewRef && this.scrollViewRef.getScrollResponder) {
288+
return this.scrollViewRef.getScrollResponder();
289+
}
290+
}
291+
292+
setNativeProps(props: Object) {
293+
if (this.scrollViewRef && this.scrollViewRef.setNativeProps) {
294+
this.scrollViewRef.setNativeProps(props);
295+
}
296+
}
297+
298+
recordInteraction() {
299+
if (this.scrollViewRef && this.scrollViewRef.recordInteraction) {
300+
this.scrollViewRef.recordInteraction();
301+
}
302+
}
303+
304+
flashScrollIndicators() {
305+
if (this.scrollViewRef && this.scrollViewRef.flashScrollIndicators) {
306+
this.scrollViewRef.flashScrollIndicators();
307+
}
308+
}
309+
310+
getMetrics(): ?Object {
311+
if (
312+
this.scrollViewRef &&
313+
this.scrollViewRef.getMetrics &&
314+
typeof this.scrollViewRef.getMetrics === 'function'
315+
) {
316+
return this.scrollViewRef.getMetrics();
317+
}
318+
}
319+
320+
/**
321+
* Expose `FlatList` API so this component is composable
322+
* with any component that expects a `FlatList`.
323+
*/
324+
scrollToIndex(params: {
325+
animated?: ?boolean,
326+
index: number,
327+
viewOffset?: number,
328+
viewPosition?: number,
329+
}) {
330+
if (
331+
this.scrollViewRef &&
332+
this.scrollViewRef.scrollToIndex &&
333+
typeof this.scrollViewRef.scrollToIndex === 'function'
334+
) {
335+
this.scrollViewRef.scrollToIndex(params);
336+
}
337+
}
338+
339+
scrollToItem(params: { animated?: ?boolean, item: any, viewPosition?: number }) {
340+
if (
341+
this.scrollViewRef &&
342+
this.scrollViewRef.scrollToItem &&
343+
typeof this.scrollViewRef.scrollToItem === 'function'
344+
) {
345+
this.scrollViewRef.scrollToItem(params);
346+
}
347+
}
348+
349+
scrollToOffset(params: { animated?: ?boolean, offset: number }) {
350+
if (
351+
this.scrollViewRef &&
352+
this.scrollViewRef.scrollToOffset &&
353+
typeof this.scrollViewRef.scrollToOffset === 'function'
354+
) {
355+
this.scrollViewRef.scrollToOffset(params);
356+
}
357+
}
358+
359+
/**
360+
* Expose `SectionList` API so this component is composable
361+
* with any component that expects a `SectionList`.
362+
*/
363+
scrollToLocation(params: {
364+
animated?: ?boolean,
365+
itemIndex: number,
366+
sectionIndex: number,
367+
viewOffset?: number,
368+
viewPosition?: number,
369+
}) {
370+
if (
371+
this.scrollViewRef &&
372+
this.scrollViewRef.scrollToLocation &&
373+
typeof this.scrollViewRef.scrollToLocation === 'function'
374+
) {
375+
this.scrollViewRef.scrollToLocation(params);
376+
}
377+
}
265378
}
266379

267380
ImageHeaderScrollView.childContextTypes = {

0 commit comments

Comments
 (0)