├── .gitignore
├── package.json
├── README.md
└── index.js
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | npm-debug\.log
3 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-parallax-header",
3 | "version": "1.1.4",
4 | "description": "A react native scroll view component with Parallax header :p",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/kyaroru/RNParallax.git"
12 | },
13 | "keywords": [
14 | "parallax",
15 | "react-native-parallax",
16 | "header",
17 | "scrollview"
18 | ],
19 | "author": "Chiew Carol",
20 | "license": "ISC",
21 | "bugs": {
22 | "url": "https://github.com/kyaroru/RNParallax/issues"
23 | },
24 | "homepage": "https://github.com/kyaroru/RNParallax#readme"
25 | }
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # RNParallax (react-native-parallax-header)
3 | [](https://github.com/kyaroru/RNParallax/stargazers)
4 | [](https://github.com/kyaroru/RNParallax/network)
5 | [](https://github.com/kyaroru/RNParallax/issues)
6 |
7 | [](https://nodei.co/npm/react-native-parallax-header/)
8 |
9 | - A react native scroll view component with Parallax header :p
10 | - Inspired by [GitHub - jaysoo/react-native-parallax-scroll-view](https://github.com/jaysoo/react-native-parallax-scroll-view)
11 | - Code is based on [React Native ScrollView animated header – App & Flow – Medium](https://medium.com/appandflow/react-native-scrollview-animated-header-10a18cb9469e) and added little customisation :p
12 |
13 | ## Installation
14 | ```bash
15 | $ npm i react-native-parallax-header --save
16 | ```
17 | ## Demo
18 | ### iPhone X or XS (Using `alwaysShowTitle={false}` & `alwaysShowNavBar={false}`)
19 | 
20 |
21 | ### iPhone X or XS
22 | 
23 |
24 | ### iPhone 8
25 | 
26 |
27 | ## Example
28 | Refer to [TestParallax](https://github.com/kyaroru/TestParallax) for working example
29 | ```jsx
30 | import React from 'react';
31 | import {
32 | StyleSheet,
33 | View,
34 | Text,
35 | StatusBar,
36 | Dimensions,
37 | TouchableOpacity,
38 | } from 'react-native';
39 | import ReactNativeParallaxHeader from 'react-native-parallax-header';
40 |
41 | const {height: SCREEN_HEIGHT} = Dimensions.get('window');
42 |
43 | const IS_IPHONE_X = SCREEN_HEIGHT === 812 || SCREEN_HEIGHT === 896;
44 | const STATUS_BAR_HEIGHT = Platform.OS === 'ios' ? (IS_IPHONE_X ? 44 : 20) : 0;
45 | const HEADER_HEIGHT = Platform.OS === 'ios' ? (IS_IPHONE_X ? 88 : 64) : 64;
46 | const NAV_BAR_HEIGHT = HEADER_HEIGHT - STATUS_BAR_HEIGHT;
47 |
48 | const renderNavBar = () => (
49 |
50 |
51 |
52 | {}}>
53 | About
54 |
55 | {}}>
56 | Me
57 |
58 |
59 |
60 | );
61 |
62 | const renderContent = () => {
63 | return (
64 |
65 | {Array.from(Array(30).keys()).map((i) => (
66 |
69 | Item {i + 1}
70 |
71 | ))}
72 |
73 | );
74 | };
75 |
76 | const title = () => {
77 | return (
78 |
79 | Parallax Header
80 |
81 | );
82 | };
83 |
84 | const App = () => {
85 | return (
86 | <>
87 |
88 | console.log('onScrollBeginDrag'),
104 | onScrollEndDrag: () => console.log('onScrollEndDrag'),
105 | }}
106 | />
107 | >
108 | );
109 | };
110 |
111 | const styles = StyleSheet.create({
112 | container: {
113 | flex: 1,
114 | },
115 | contentContainer: {
116 | flexGrow: 1,
117 | },
118 | navContainer: {
119 | height: HEADER_HEIGHT,
120 | marginHorizontal: 10,
121 | },
122 | statusBar: {
123 | height: STATUS_BAR_HEIGHT,
124 | backgroundColor: 'transparent',
125 | },
126 | navBar: {
127 | height: NAV_BAR_HEIGHT,
128 | justifyContent: 'space-between',
129 | alignItems: 'center',
130 | flexDirection: 'row',
131 | backgroundColor: 'transparent',
132 | },
133 | titleStyle: {
134 | color: 'white',
135 | fontWeight: 'bold',
136 | fontSize: 18,
137 | },
138 | });
139 |
140 | export default App;
141 | ```
142 |
143 | ## API Usage
144 | | Property | Type | Required | Description | Default |
145 | | -------- | ---- | -------- | ----------- | ------- |
146 | | `renderNavBar` | `func` | No | This renders the nav bar component | Empty `` |
147 | | `renderContent` | `func` | **YES** | This renders the scroll view content | - |
148 | | `headerMaxHeight` | `number` | No | This is the header maximum height | Default to `170` |
149 | | `headerMinHeight` | `number` | No | This is the header minimum height | Default to common ios & android navbar height (have support for iPhone X too :p) |
150 | | `backgroundImage` | `image source` | No | This renders the background image of the header (**if specified, background color will not take effect**) | Default to `null` |
151 | | `backgroundImageScale` | `number` | No | This is the image scale - either enlarge or shrink (after scrolling to bottom & exceed the headerMaxHeight) | Default is `1.5` |
152 | | `backgroundColor` | `string` | No | This is the color of the parallax background (before scrolling up), **will not be used if `backgroundImage` is specified** | Default color is `#303F9F` |
153 | | `extraScrollHeight` | `number` | No | This is the extra scroll height (after scrolling to bottom & exceed the headerMaxHeight) | Default is `30` |
154 | | `navbarColor` | `string` | No | This is the background color of the navbar (after scroll up) | Default color is `#3498db` |
155 | | `statusBarColor` | `string` | No | This is the status bar color (for android) navBarColor will be used if no statusBarColor is passed in | Default to `null` |
156 | | `title` | `any` | No | This is the title to be display in the header, can be string or component | Default to `null` |
157 | | `titleStyle` | `style` | No | This is the title style to override default font size/color | Default to `color: ‘white’ `text and `fontSize: 16` |
158 | | `headerTitleStyle` | `style` | No | This is the header title animated view style to override default `` style | Default to `null` |
159 | | `scrollEventThrottle` | `number` | No | This is the scroll event throttle | Default is `16` |
160 | | `contentContainerStyle` | `style` | No | This is the contentContainerStyle style to override default `` contentContainerStyle style | Default to null |
161 | | `containerStyle` | `style` | No | This is the style to override default outermost `` style | Default to null |
162 | | `scrollViewStyle` | `style` | No | This is the scrollview style to override default `` style | Default to null |
163 | | `innerContainerStyle` | `style` | No | This is the inner content style to override default `` style inside `` component | Default to null |
164 | | `alwaysShowTitle` | `bool` | No | This is to determine whether show or hide the title after scroll | Default to `true` |
165 | | `alwaysShowNavBar` | `bool` | No | This is to determine whether show or hide the navBar before scroll | Default to `true` |
166 | | `scrollViewProps` | `object` | No | This is to override default scroll view properties | Default to `{}` |
167 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import PropTypes from 'prop-types';
3 | import {
4 | StyleSheet,
5 | Platform,
6 | Animated,
7 | Text,
8 | View,
9 | Dimensions,
10 | StatusBar,
11 | } from 'react-native';
12 |
13 | const {height: SCREEN_HEIGHT} = Dimensions.get('window');
14 |
15 | const IS_IPHONE_X = SCREEN_HEIGHT === 812 || SCREEN_HEIGHT === 896;
16 | const STATUS_BAR_HEIGHT = Platform.OS === 'ios' ? (IS_IPHONE_X ? 44 : 20) : 0;
17 | const NAV_BAR_HEIGHT = Platform.OS === 'ios' ? (IS_IPHONE_X ? 88 : 64) : 64;
18 |
19 | const SCROLL_EVENT_THROTTLE = 16;
20 | const DEFAULT_HEADER_MAX_HEIGHT = 170;
21 | const DEFAULT_HEADER_MIN_HEIGHT = NAV_BAR_HEIGHT;
22 | const DEFAULT_EXTRA_SCROLL_HEIGHT = 30;
23 | const DEFAULT_BACKGROUND_IMAGE_SCALE = 1.5;
24 |
25 | const DEFAULT_NAVBAR_COLOR = '#3498db';
26 | const DEFAULT_BACKGROUND_COLOR = '#303F9F';
27 | const DEFAULT_TITLE_COLOR = 'white';
28 |
29 | const styles = StyleSheet.create({
30 | container: {
31 | backgroundColor: 'white',
32 | flex: 1,
33 | },
34 | scrollView: {
35 | flex: 1,
36 | },
37 | header: {
38 | position: 'absolute',
39 | top: 0,
40 | left: 0,
41 | right: 0,
42 | backgroundColor: DEFAULT_NAVBAR_COLOR,
43 | overflow: 'hidden',
44 | },
45 | backgroundImage: {
46 | position: 'absolute',
47 | top: 0,
48 | left: 0,
49 | right: 0,
50 | width: null,
51 | height: DEFAULT_HEADER_MAX_HEIGHT,
52 | resizeMode: 'cover',
53 | },
54 | bar: {
55 | backgroundColor: 'transparent',
56 | height: DEFAULT_HEADER_MIN_HEIGHT,
57 | position: 'absolute',
58 | top: 0,
59 | left: 0,
60 | right: 0,
61 | },
62 | headerTitle: {
63 | backgroundColor: 'transparent',
64 | position: 'absolute',
65 | top: 0,
66 | left: 0,
67 | right: 0,
68 | paddingTop: STATUS_BAR_HEIGHT,
69 | alignItems: 'center',
70 | justifyContent: 'center',
71 | },
72 | headerText: {
73 | color: DEFAULT_TITLE_COLOR,
74 | textAlign: 'center',
75 | fontSize: 16,
76 | },
77 | });
78 |
79 | class RNParallax extends Component {
80 | constructor() {
81 | super();
82 | this.state = {
83 | scrollY: new Animated.Value(0),
84 | };
85 | }
86 |
87 | getHeaderMaxHeight() {
88 | const {headerMaxHeight} = this.props;
89 | return headerMaxHeight;
90 | }
91 |
92 | getHeaderMinHeight() {
93 | const {headerMinHeight} = this.props;
94 | return headerMinHeight;
95 | }
96 |
97 | getHeaderScrollDistance() {
98 | return this.getHeaderMaxHeight() - this.getHeaderMinHeight();
99 | }
100 |
101 | getExtraScrollHeight() {
102 | const {extraScrollHeight} = this.props;
103 | return extraScrollHeight;
104 | }
105 |
106 | getBackgroundImageScale() {
107 | const {backgroundImageScale} = this.props;
108 | return backgroundImageScale;
109 | }
110 |
111 | getInputRange() {
112 | return [-this.getExtraScrollHeight(), 0, this.getHeaderScrollDistance()];
113 | }
114 |
115 | getHeaderHeight() {
116 | const {scrollY} = this.state;
117 | return scrollY.interpolate({
118 | inputRange: this.getInputRange(),
119 | outputRange: [
120 | this.getHeaderMaxHeight() + this.getExtraScrollHeight(),
121 | this.getHeaderMaxHeight(),
122 | this.getHeaderMinHeight(),
123 | ],
124 | extrapolate: 'clamp',
125 | });
126 | }
127 |
128 | getNavBarOpacity() {
129 | const {scrollY} = this.state;
130 | return scrollY.interpolate({
131 | inputRange: this.getInputRange(),
132 | outputRange: [0, 1, 1],
133 | extrapolate: 'clamp',
134 | });
135 | }
136 |
137 | getNavBarForegroundOpacity() {
138 | const {scrollY} = this.state;
139 | const {alwaysShowNavBar} = this.props;
140 | return scrollY.interpolate({
141 | inputRange: this.getInputRange(),
142 | outputRange: [alwaysShowNavBar ? 1 : 0, alwaysShowNavBar ? 1 : 0, 1],
143 | extrapolate: 'clamp',
144 | });
145 | }
146 |
147 | getImageOpacity() {
148 | const {scrollY} = this.state;
149 | return scrollY.interpolate({
150 | inputRange: this.getInputRange(),
151 | outputRange: [1, 1, 0],
152 | extrapolate: 'clamp',
153 | });
154 | }
155 |
156 | getImageTranslate() {
157 | const {scrollY} = this.state;
158 | return scrollY.interpolate({
159 | inputRange: this.getInputRange(),
160 | outputRange: [0, 0, -50],
161 | extrapolate: 'clamp',
162 | });
163 | }
164 |
165 | getImageScale() {
166 | const {scrollY} = this.state;
167 | return scrollY.interpolate({
168 | inputRange: this.getInputRange(),
169 | outputRange: [this.getBackgroundImageScale(), 1, 1],
170 | extrapolate: 'clamp',
171 | });
172 | }
173 |
174 | getTitleTranslateY() {
175 | const {scrollY} = this.state;
176 | return scrollY.interpolate({
177 | inputRange: this.getInputRange(),
178 | outputRange: [5, 0, 0],
179 | extrapolate: 'clamp',
180 | });
181 | }
182 |
183 | getTitleOpacity() {
184 | const {scrollY} = this.state;
185 | const {alwaysShowTitle} = this.props;
186 | return scrollY.interpolate({
187 | inputRange: this.getInputRange(),
188 | outputRange: [1, 1, alwaysShowTitle ? 1 : 0],
189 | extrapolate: 'clamp',
190 | });
191 | }
192 |
193 | renderBackgroundImage() {
194 | const {backgroundImage} = this.props;
195 | const imageOpacity = this.getImageOpacity();
196 | const imageTranslate = this.getImageTranslate();
197 | const imageScale = this.getImageScale();
198 |
199 | return (
200 |
211 | );
212 | }
213 |
214 | renderPlainBackground() {
215 | const {backgroundColor} = this.props;
216 |
217 | const imageOpacity = this.getImageOpacity();
218 | const imageTranslate = this.getImageTranslate();
219 | const imageScale = this.getImageScale();
220 |
221 | return (
222 |
230 | );
231 | }
232 |
233 | renderNavbarBackground() {
234 | const {navbarColor} = this.props;
235 | const navBarOpacity = this.getNavBarOpacity();
236 |
237 | return (
238 |
248 | );
249 | }
250 |
251 | renderHeaderBackground() {
252 | const {backgroundImage, backgroundColor} = this.props;
253 | const imageOpacity = this.getImageOpacity();
254 |
255 | return (
256 |
265 | {backgroundImage && this.renderBackgroundImage()}
266 | {!backgroundImage && this.renderPlainBackground()}
267 |
268 | );
269 | }
270 |
271 | renderHeaderTitle() {
272 | const {title, titleStyle, headerTitleStyle} = this.props;
273 | const titleTranslateY = this.getTitleTranslateY();
274 | const titleOpacity = this.getTitleOpacity();
275 |
276 | return (
277 |
287 | {typeof title === 'string' && (
288 | {title}
289 | )}
290 | {typeof title !== 'string' && title}
291 |
292 | );
293 | }
294 |
295 | renderHeaderForeground() {
296 | const {renderNavBar} = this.props;
297 | const navBarOpacity = this.getNavBarForegroundOpacity();
298 |
299 | return (
300 |
308 | {renderNavBar()}
309 |
310 | );
311 | }
312 |
313 | renderScrollView() {
314 | const {
315 | renderContent,
316 | scrollEventThrottle,
317 | scrollViewStyle,
318 | contentContainerStyle,
319 | innerContainerStyle,
320 | scrollViewProps,
321 | } = this.props;
322 | const {scrollY} = this.state;
323 | const {onScroll} = scrollViewProps;
324 |
325 | // remove scrollViewProps.onScroll in renderScrollViewProps so we can still get default scroll behavior
326 | // if a caller passes in `onScroll` prop
327 | const renderableScrollViewProps = Object.assign({}, scrollViewProps);
328 | delete renderableScrollViewProps.onScroll;
329 |
330 | return (
331 |
343 |
345 | {renderContent()}
346 |
347 |
348 | );
349 | }
350 |
351 | render() {
352 | const {navbarColor, statusBarColor, containerStyle} = this.props;
353 | return (
354 |
355 |
356 | {this.renderScrollView()}
357 | {this.renderNavbarBackground()}
358 | {this.renderHeaderBackground()}
359 | {this.renderHeaderTitle()}
360 | {this.renderHeaderForeground()}
361 |
362 | );
363 | }
364 | }
365 |
366 | RNParallax.propTypes = {
367 | renderNavBar: PropTypes.func,
368 | renderContent: PropTypes.func.isRequired,
369 | backgroundColor: PropTypes.string,
370 | backgroundImage: PropTypes.any,
371 | navbarColor: PropTypes.string,
372 | title: PropTypes.any,
373 | titleStyle: PropTypes.any,
374 | headerTitleStyle: PropTypes.any,
375 | headerMaxHeight: PropTypes.number,
376 | headerMinHeight: PropTypes.number,
377 | scrollEventThrottle: PropTypes.number,
378 | extraScrollHeight: PropTypes.number,
379 | backgroundImageScale: PropTypes.number,
380 | contentContainerStyle: PropTypes.any,
381 | innerContainerStyle: PropTypes.any,
382 | scrollViewStyle: PropTypes.any,
383 | containerStyle: PropTypes.any,
384 | alwaysShowTitle: PropTypes.bool,
385 | alwaysShowNavBar: PropTypes.bool,
386 | statusBarColor: PropTypes.string,
387 | scrollViewProps: PropTypes.object,
388 | };
389 |
390 | RNParallax.defaultProps = {
391 | renderNavBar: () => ,
392 | navbarColor: DEFAULT_NAVBAR_COLOR,
393 | backgroundColor: DEFAULT_BACKGROUND_COLOR,
394 | backgroundImage: null,
395 | title: null,
396 | titleStyle: styles.headerText,
397 | headerTitleStyle: null,
398 | headerMaxHeight: DEFAULT_HEADER_MAX_HEIGHT,
399 | headerMinHeight: DEFAULT_HEADER_MIN_HEIGHT,
400 | scrollEventThrottle: SCROLL_EVENT_THROTTLE,
401 | extraScrollHeight: DEFAULT_EXTRA_SCROLL_HEIGHT,
402 | backgroundImageScale: DEFAULT_BACKGROUND_IMAGE_SCALE,
403 | contentContainerStyle: null,
404 | innerContainerStyle: null,
405 | scrollViewStyle: null,
406 | containerStyle: null,
407 | alwaysShowTitle: true,
408 | alwaysShowNavBar: true,
409 | statusBarColor: null,
410 | scrollViewProps: {},
411 | };
412 |
413 | export default RNParallax;
414 |
--------------------------------------------------------------------------------