├── .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 | [![GitHub stars](https://img.shields.io/github/stars/kyaroru/RNParallax.svg)](https://github.com/kyaroru/RNParallax/stargazers) 4 | [![GitHub forks](https://img.shields.io/github/forks/kyaroru/RNParallax.svg)](https://github.com/kyaroru/RNParallax/network) 5 | [![GitHub issues](https://img.shields.io/github/issues/kyaroru/RNParallax.svg)](https://github.com/kyaroru/RNParallax/issues) 6 | 7 | [![NPM](https://nodei.co/npm/react-native-parallax-header.png?downloads=true&downloadRank=true&stars=true)](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 | ![iPhone X](https://i.gyazo.com/24343e2127b8e479a52f4bc5853ef457.gif) 20 | 21 | ### iPhone X or XS 22 | ![iPhone X](https://i.gyazo.com/b24881b191ce5a69e7de14b7d0bb688e.gif) 23 | 24 | ### iPhone 8 25 | ![iPhone 8](https://i.gyazo.com/eebeff28c7df7b0233fabb9cf2a9c5dc.gif) 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 | --------------------------------------------------------------------------------