├── package.json ├── README.md ├── ScreenUtil.js └── index.js /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_args": [ 3 | [ 4 | "git+https://github.com/thisSillyCow/react-native-parallax-header.git", 5 | "H:\\developmentProject\\reactNativeNovel" 6 | ] 7 | ], 8 | "_from": "git+https://github.com/thisSillyCow/react-native-parallax-header.git", 9 | "_id": "react-native-parallax-header@git+https://github.com/thisSillyCow/react-native-parallax-header.git#2a4f856337e755a131a54e349b134d65277cbc86", 10 | "_inBundle": false, 11 | "_integrity": "", 12 | "_location": "/react-native-parallax-header", 13 | "_phantomChildren": {}, 14 | "_requested": { 15 | "type": "git", 16 | "raw": "git+https://github.com/thisSillyCow/react-native-parallax-header.git", 17 | "rawSpec": "git+https://github.com/thisSillyCow/react-native-parallax-header.git", 18 | "saveSpec": "git+https://github.com/thisSillyCow/react-native-parallax-header.git", 19 | "fetchSpec": "https://github.com/thisSillyCow/react-native-parallax-header.git", 20 | "gitCommittish": null 21 | }, 22 | "_requiredBy": [ 23 | "/" 24 | ], 25 | "_resolved": "git+https://github.com/thisSillyCow/react-native-parallax-header.git#2a4f856337e755a131a54e349b134d65277cbc86", 26 | "_shasum": "d02b6465ad02892832966f37af8e9148ed0f727d", 27 | "_spec": "git+https://github.com/thisSillyCow/react-native-parallax-header.git", 28 | "_where": "H:\\developmentProject\\reactNativeNovel", 29 | "author": { 30 | "name": "Chiew Carol" 31 | }, 32 | "bugs": { 33 | "url": "https://github.com/kyaroru/RNParallax/issues" 34 | }, 35 | "bundleDependencies": false, 36 | "deprecated": false, 37 | "description": "A react native scroll view component with Parallax header :p", 38 | "homepage": "https://github.com/kyaroru/RNParallax#readme", 39 | "keywords": [ 40 | "parallax", 41 | "react-native-parallax", 42 | "header", 43 | "scrollview" 44 | ], 45 | "license": "ISC", 46 | "main": "index.js", 47 | "name": "react-native-parallax-header", 48 | "repository": { 49 | "type": "git", 50 | "url": "git+https://github.com/kyaroru/RNParallax.git" 51 | }, 52 | "scripts": { 53 | "test": "echo \"Error: no test specified\" && exit 1" 54 | }, 55 | "version": "1.1.3" 56 | } 57 | -------------------------------------------------------------------------------- /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 | ```jsx 29 | import Icon from 'react-native-vector-icons/MaterialIcons'; 30 | import ReactNativeParallaxHeader from 'react-native-parallax-header'; 31 | 32 | const IS_IPHONE_X = SCREEN_HEIGHT === 812 || SCREEN_HEIGHT === 896; 33 | const STATUS_BAR_HEIGHT = Platform.OS === 'ios' ? (IS_IPHONE_X ? 44 : 20) : 0; 34 | const HEADER_HEIGHT = Platform.OS === 'ios' ? (IS_IPHONE_X ? 88 : 64) : 64; 35 | const NAV_BAR_HEIGHT = HEADER_HEIGHT - STATUS_BAR_HEIGHT; 36 | 37 | const images = { 38 | background: require('../img/test.jpg'), // Put your own image here 39 | }; 40 | 41 | const styles = StyleSheet.create({ 42 | container: { 43 | flex: 1, 44 | }, 45 | contentContainer: { 46 | flexGrow: 1, 47 | }, 48 | navContainer: { 49 | height: HEADER_HEIGHT, 50 | marginHorizontal: 10, 51 | }, 52 | statusBar: { 53 | height: STATUS_BAR_HEIGHT, 54 | backgroundColor: 'transparent', 55 | }, 56 | navBar: { 57 | height: NAV_BAR_HEIGHT, 58 | justifyContent: 'space-between', 59 | alignItems: 'center', 60 | flexDirection: 'row', 61 | backgroundColor: 'transparent', 62 | }, 63 | titleStyle: { 64 | color: 'white', 65 | fontWeight: 'bold', 66 | fontSize: 18, 67 | }, 68 | }); 69 | 70 | renderNavBar = () => ( 71 | 72 | 73 | 74 | {}}> 75 | 76 | 77 | {}}> 78 | 79 | 80 | 81 | 82 | ) 83 | 84 | render() { 85 | return ( 86 | 87 | console.log('onScrollBeginDrag'), 103 | onScrollEndDrag: () => console.log('onScrollEndDrag'), 104 | }} 105 | /> 106 | 107 | ); 108 | } 109 | ``` 110 | 111 | ## API Usage 112 | | Property | Type | Required | Description | Default | 113 | | -------- | ---- | -------- | ----------- | ------- | 114 | | `renderNavBar` | `func` | No | This renders the nav bar component | Empty `` | 115 | | `renderContent` | `func` | **YES** | This renders the scroll view content | - | 116 | | `headerMaxHeight` | `number` | No | This is the header maximum height | Default to `170` | 117 | | `headerMinHeight` | `number` | No | This is the header minimum height | Default to common ios & android navbar height (have support for iPhone X too :p) | 118 | | `backgroundImage` | `image source` | No | This renders the background image of the header (**if specified, background color will not take effect**) | Default to `null` | 119 | | `backgroundImageScale` | `number` | No | This is the image scale - either enlarge or shrink (after scrolling to bottom & exceed the headerMaxHeight) | Default is `1.5` | 120 | | `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` | 121 | | `extraScrollHeight` | `number` | No | This is the extra scroll height (after scrolling to bottom & exceed the headerMaxHeight) | Default is `30` | 122 | | `navbarColor` | `string` | No | This is the background color of the navbar (after scroll up) | Default color is `#3498db` | 123 | | `statusBarColor` | `string` | No | This is the status bar color (for android) navBarColor will be used if no statusBarColor is passed in | Default to `null` | 124 | | `title` | `any` | No | This is the title to be display in the header, can be string or component | Default to `null` | 125 | | `titleStyle` | `style` | No | This is the title style to override default font size/color | Default to `color: ‘white’ `text and `fontSize: 16` | 126 | | `headerTitleStyle` | `style` | No | This is the header title animated view style to override default `` style | Default to `null` | 127 | | `scrollEventThrottle` | `number` | No | This is the scroll event throttle | Default is `16` | 128 | | `contentContainerStyle` | `style` | No | This is the contentContainerStyle style to override default `` contentContainerStyle style | Default to null | 129 | | `containerStyle` | `style` | No | This is the style to override default outermost `` style | Default to null | 130 | | `scrollViewStyle` | `style` | No | This is the scrollview style to override default `` style | Default to null | 131 | | `innerContainerStyle` | `style` | No | This is the inner content style to override default `` style inside `` component | Default to null | 132 | | `alwaysShowTitle` | `bool` | No | This is to determine whether show or hide the title after scroll | Default to `true` | 133 | | `alwaysShowNavBar` | `bool` | No | This is to determine whether show or hide the navBar before scroll | Default to `true` | 134 | | `scrollViewProps` | `object` | No | This is to override default scroll view properties | Default to `{}` | 135 | -------------------------------------------------------------------------------- /ScreenUtil.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 屏幕工具类 3 | * ui设计基准,iphone 6 4 | * width:750px 5 | * height:1334px 6 | */ 7 | import { PixelRatio, Dimensions, Platform, StatusBar } from 'react-native' 8 | export let screenW = Dimensions.get('window').width 9 | export let screenH = Dimensions.get('window').height 10 | export let pixelRatio = PixelRatio.get() 11 | //像素密度 12 | export const DEFAULT_DENSITY = 1 13 | //px转换成dp 14 | //以iphone6为基准,如果以其他尺寸为基准的话,请修改下面的750和1334为对应尺寸即可. 15 | const w2 = 750 / DEFAULT_DENSITY 16 | //px转换成dp 17 | const h2 = 1334 / DEFAULT_DENSITY 18 | // iPhoneX 19 | export const X_WIDTH = 375 20 | export const X_HEIGHT = 812 21 | //iPhoneX底部高度 22 | export const IPHONEX_BOTTOM_HEIGHT = 54; 23 | 24 | export const STATUS_HEIGHT = Platform.OS === 'ios' ? 20 : (Platform.Version > 19 ? StatusBar.currentHeight : 0); 25 | 26 | // 边缘圆角 27 | export const radiusNum = scaleSize(5); 28 | /** 29 | * 设置字体的size(单位px) 30 | * @param size 传入设计稿上的px 31 | * @returns {Number} 返回实际sp 32 | */ 33 | export function setSpText(size) { 34 | isIphoneX() 35 | let scaleWidth = screenW / w2 36 | let scaleHeight = screenH / h2 37 | let scale = Math.min(scaleWidth, scaleHeight) 38 | size = Math.round(size * 2 * scale + 0.5) 39 | return size / DEFAULT_DENSITY 40 | } 41 | export const getUrlParam = (name) => { 42 | const that = this 43 | var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i'); 44 | var r = window.location.search.substr(1).match(reg) 45 | if (r != null) { 46 | return r[2] 47 | } 48 | } 49 | 50 | export const getParams = (params, name) => { 51 | const that = this 52 | var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i'); 53 | var r = params.substr(1).match(reg) 54 | if (r != null) { 55 | return r[2] 56 | } 57 | } 58 | 59 | /** 60 | * 屏幕适配,缩放size 61 | * @param size 62 | * @returns {Number} 63 | */ 64 | export function scaleSize(size) { 65 | isIphoneX() 66 | let scaleWidth = screenW / w2 67 | let scaleHeight = screenH / h2 68 | let scale = Math.min(scaleWidth, scaleHeight) 69 | size = Math.round(size * 2 * scale + 0.5) 70 | return size / DEFAULT_DENSITY 71 | } 72 | 73 | /** 74 | * 判断android API是否小于19(4.4以下),如果是则不能使用沉浸状态栏 75 | * 76 | */ 77 | export function isLT19() { 78 | return Platform.OS === 'android' && Platform.Version < 19 79 | } 80 | 81 | //时间处理 82 | Date.prototype.format = function (format) { 83 | let date = { 84 | 'M+': this.getMonth() + 1, 85 | 'd+': this.getDate(), 86 | 'h+': this.getHours(), 87 | 'm+': this.getMinutes(), 88 | 's+': this.getSeconds(), 89 | 'q+': Math.floor((this.getMonth() + 3) / 3), 90 | 'S+': this.getMilliseconds() 91 | } 92 | if (/(y+)/i.test(format)) { 93 | format = format.replace( 94 | RegExp.$1, 95 | (this.getFullYear() + '').substr(4 - RegExp.$1.length) 96 | ) 97 | } 98 | for (let k in date) { 99 | if (new RegExp('(' + k + ')').test(format)) { 100 | format = format.replace( 101 | RegExp.$1, 102 | RegExp.$1.length === 1 103 | ? date[k] 104 | : ('00' + date[k]).substr(('' + date[k]).length) 105 | ) 106 | } 107 | } 108 | return format 109 | } 110 | 111 | export function TimesAgo(timestamp) { 112 | var mistiming = Math.round(new Date() / 1000) - timestamp; 113 | var arrr = ['年', '个月', '星期', '天', '小时', '分钟', '秒']; 114 | var arrn = [31536000, 2592000, 604800, 86400, 3600, 60, 1]; 115 | for (var i = 6; i >= 0; i--) { 116 | var inm = Math.floor(mistiming / arrn[i]); 117 | if (inm != 0) { 118 | return inm + arrr[i] + '前'; 119 | } 120 | } 121 | } 122 | 123 | //获取时间差 current:1497235409744 当前时间 start:1497235419744 开始时间 124 | export function getRemainingime(current, start) { 125 | let time = start - current 126 | return time / 1000 //["0", "0", "2", "7", "33", "30"]0年0月2日 7时33分30秒 127 | } 128 | 129 | //1497235419 130 | export function getRemainingimeDistance(distance) { 131 | let time = distance * 1000 132 | if (time < 0) { 133 | return ['0', '0', '0', '0', '0', '0'] 134 | } 135 | let year = Math.floor(time / (365 * 30 * 24 * 3600 * 1000)) //年 136 | let month = Math.floor(time / (30 * 24 * 3600 * 1000)) //月 137 | let days = Math.floor(time / (24 * 3600 * 1000)) //日 138 | let temp1 = time % (24 * 3600 * 1000) 139 | let hours = Math.floor(temp1 / (3600 * 1000)) //时 140 | let temp2 = temp1 % (3600 * 1000) 141 | let minutes = Math.floor(temp2 / (60 * 1000)) //分 142 | let temp3 = temp2 % (60 * 1000) 143 | let seconds = Math.round(temp3 / 1000) //秒 144 | 145 | let strs = [ 146 | year, 147 | toNormal(month), 148 | toNormal(days), 149 | toNormal(hours), 150 | toNormal(minutes), 151 | toNormal(seconds) 152 | ] 153 | return strs //["0", "0", "2", "7", "33", "30"]0年0月2日 7时33分30秒 154 | } 155 | 156 | export function toNormal(time) { 157 | return time >= 10 ? time : '0' + time 158 | } 159 | 160 | //转换成日期 161 | export function toDate(timestamp, format1 = 'yyyy-MM-dd hh:mm:ss') { 162 | try { 163 | let date = new Date() 164 | date.setTime(timestamp) 165 | return date.format(format1) //2014-07-10 10:21:12 166 | } catch (erro) { 167 | return '' 168 | } 169 | } 170 | 171 | //1970/1/1至今的秒数 172 | export function toTimestamp(date) { 173 | let timestamp = Date.parse(date) 174 | return timestamp / 1000 // 1497233827569/1000 175 | } 176 | 177 | //CST时间=>转换成日期yyyy-MM-dd hh:mm:ss 178 | export function getTaskTime(strDate) { 179 | if (null == strDate || '' == strDate) { 180 | return '' 181 | } 182 | let dateStr = strDate.trim().split(' ') 183 | let strGMT = 184 | dateStr[0] + 185 | ' ' + 186 | dateStr[1] + 187 | ' ' + 188 | dateStr[2] + 189 | ' ' + 190 | dateStr[5] + 191 | ' ' + 192 | dateStr[3] + 193 | ' GMT+0800' 194 | let date = new Date(Date.parse(strGMT)) 195 | let y = date.getFullYear() 196 | let m = date.getMonth() + 1 197 | m = m < 10 ? '0' + m : m 198 | let d = date.getDate() 199 | d = d < 10 ? '0' + d : d 200 | let h = date.getHours() 201 | let minute = date.getMinutes() 202 | minute = minute < 10 ? '0' + minute : minute 203 | let second = date.getSeconds() 204 | second = second < 10 ? '0' + second : second 205 | 206 | return y + '-' + m + '-' + d + ' ' + h + ':' + minute + ':' + second 207 | } 208 | 209 | //1497235419 210 | export function getRemainingimeDistance2(distance) { 211 | let time = distance 212 | let days = Math.floor(time / (24 * 3600 * 1000)) 213 | let temp1 = time % (24 * 3600 * 1000) 214 | let hours = Math.floor(temp1 / (3600 * 1000)) 215 | let temp2 = temp1 % (3600 * 1000) 216 | let minutes = Math.floor(temp2 / (60 * 1000)) 217 | if (time <= 60 * 1000) { 218 | minutes = 1 219 | } 220 | let temp3 = temp2 % (60 * 1000) 221 | let seconds = Math.round(temp3 / 1000) 222 | return [hours, minutes] //["0", "0", "2", "7", "33", "30"]0年0月2日 7时33分30秒 223 | } 224 | 225 | /** 226 | * 判断是否为iphoneX 227 | * @returns {boolean} 228 | */ 229 | export function isIphoneX() { 230 | const dimen = Dimensions.get('window'); 231 | return ( 232 | Platform.OS === 'ios' && 233 | ((dimen.height === 812 || dimen.width === 812) || (dimen.height === 896 || dimen.width === 896)) 234 | ) 235 | } 236 | 237 | 238 | export function isIphoneNum(mun) { 239 | const dimen = Dimensions.get('window'); 240 | var topMum = mun; 241 | if (Platform.OS === 'ios' && !Platform.isPad && !Platform.isTVOS 242 | && ((dimen.height === 812 || dimen.width === 812) || (dimen.height === 896 || dimen.width === 896))) { 243 | topMum = mun + 20; 244 | } 245 | return scaleSize(topMum) 246 | } 247 | 248 | /** 249 | * 根据是否是iPhoneX返回不同的样式 250 | * @param iphoneXStyle 251 | * @param iosStyle 252 | * @param androidStyle 253 | * @returns {*} 254 | */ 255 | export function ifIphoneX(iphoneXStyle, iosStyle = {}, androidStyle) { 256 | if (isIphoneX()) { 257 | return iphoneXStyle 258 | } else if (Platform.OS === 'ios') { 259 | return iosStyle 260 | } else { 261 | if (androidStyle) return androidStyle 262 | return iosStyle 263 | } 264 | } 265 | 266 | 267 | export function guid() { 268 | function S4() { 269 | return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); 270 | } 271 | return (S4() + S4() + S4() + S4() + S4() + S4() + S4() + S4()); 272 | } 273 | export function Accuracy(num, len) { 274 | const wordCount = num + '' 275 | var countNum = num; 276 | if (wordCount.length > 4 && wordCount.length < 9) { 277 | countNum = parseInt(wordCount.substring(0, wordCount.length - 4) + '.' + wordCount.substring(wordCount.length - 4, wordCount.length)).toFixed(len || 0) 278 | } else if (wordCount.length > 8) { 279 | countNum = parseInt(wordCount.substring(0, wordCount.length - 8) + '.' + wordCount.substring(wordCount.length - 8, wordCount.length - 7)).toFixed(len || 0) 280 | } 281 | return countNum; 282 | } 283 | export function AccuracyNUm(num) { 284 | const wordCount = num + '' 285 | var countNum = ''; 286 | if (wordCount.length > 4 && wordCount.length < 9) { 287 | countNum = '万' 288 | } else if (wordCount.length > 8) { 289 | countNum = '亿' 290 | } 291 | return countNum; 292 | 293 | } 294 | 295 | export function insertionSort(array) { 296 | for (var i = 1; i < array.length; i++) { 297 | var key = array[i]; 298 | var j = i - 1; 299 | while (j >= 0 && array[j] > key) { 300 | array[j + 1] = array[j]; 301 | j--; 302 | } 303 | array[j + 1] = key; 304 | } 305 | return array; 306 | } 307 | export function insertionSortJSON(array) { 308 | for (var i = 1; i < array.length; i++) { 309 | var key = array[i]; 310 | var j = i - 1; 311 | while (j >= 0 && array[j].seqNum > key.seqNum) { 312 | array[j + 1] = array[j]; 313 | j--; 314 | } 315 | array[j + 1] = key; 316 | } 317 | return array; 318 | } 319 | 320 | export default class ScreenUtil { 321 | static screenW = screenW 322 | static screenH = screenH 323 | static pixelRatio = pixelRatio 324 | static DEFAULT_DENSITY = DEFAULT_DENSITY 325 | static setSpText(size) { 326 | return setSpText(size) 327 | } 328 | 329 | static scaleSize(size) { 330 | return scaleSize(size) 331 | } 332 | 333 | static getRemainingimeDistance(distance) { 334 | return getRemainingimeDistance(distance) 335 | } 336 | 337 | static toDate(timestamp, format1 = 'yyyy-MM-dd hh:mm:ss') { 338 | return toDate(timestamp, format1) 339 | } 340 | 341 | static toTimestamp(date) { 342 | return toTimestamp(date) 343 | } 344 | 345 | static getTaskTime(strDate) { 346 | return getTaskTime(strDate) 347 | } 348 | static guid() { 349 | return guid() 350 | } 351 | 352 | static Accuracy(num) { 353 | return Accuracy(num) 354 | } 355 | static AccuracyNUm(num) { 356 | return AccuracyNUm(num) 357 | } 358 | static getRemainingimeDistance2(distance) { 359 | return getRemainingimeDistance2(distance) 360 | } 361 | static insertionSort(array) { 362 | return insertionSort(array) 363 | } 364 | static insertionSortJSON(array) { 365 | return insertionSortJSON(array) 366 | } 367 | } 368 | 369 | 370 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { StyleSheet, Platform, Animated, Text, View, Dimensions, StatusBar, } from 'react-native'; 4 | import { screenW, screenH, setSpText, scaleSize, isIphoneNum } from "./ScreenUtil"; 5 | const { height: SCREEN_HEIGHT, } = Dimensions.get('window'); 6 | const IS_IPHONE_X = SCREEN_HEIGHT === scaleSize(812) || SCREEN_HEIGHT === scaleSize(896); 7 | const STATUS_BAR_HEIGHT = Platform.OS === 'ios' ? (IS_IPHONE_X ? scaleSize(44) : scaleSize(20)) : scaleSize(0); 8 | const NAV_BAR_HEIGHT = Platform.OS === 'ios' ? (IS_IPHONE_X ? scaleSize(88) : scaleSize(64)) : scaleSize(64); 9 | 10 | const SCROLL_EVENT_THROTTLE = scaleSize(16); 11 | const DEFAULT_HEADER_MAX_HEIGHT = scaleSize(170); 12 | const DEFAULT_HEADER_MIN_HEIGHT = NAV_BAR_HEIGHT; 13 | const DEFAULT_EXTRA_SCROLL_HEIGHT = scaleSize(30); 14 | const DEFAULT_BACKGROUND_IMAGE_SCALE = 1.5; 15 | 16 | const DEFAULT_NAVBAR_COLOR = 'rgba(255,255,255,.5)'; 17 | const DEFAULT_BACKGROUND_COLOR = 'rgba(255,255,255,.9)'; 18 | const DEFAULT_TITLE_COLOR = 'white'; 19 | 20 | const styles = StyleSheet.create({ 21 | containerTitle: { 22 | position: 'absolute', 23 | top: 0, 24 | left: 0, 25 | right: 0, 26 | height: scaleSize(70), 27 | }, 28 | container: { 29 | backgroundColor: 'white', 30 | flex: 1, 31 | }, 32 | scrollView: { 33 | flex: 1, 34 | }, 35 | header: { 36 | position: 'absolute', 37 | top: 0, 38 | left: 0, 39 | right: 0, 40 | backgroundColor: DEFAULT_NAVBAR_COLOR, 41 | overflow: 'hidden', 42 | }, 43 | backgroundImage: { 44 | position: 'absolute', 45 | top: 0, 46 | left: 0, 47 | right: 0, 48 | width: null, 49 | height: DEFAULT_HEADER_MAX_HEIGHT, 50 | resizeMode: 'cover', 51 | }, 52 | bar: { 53 | backgroundColor: 'transparent', 54 | height: DEFAULT_HEADER_MIN_HEIGHT, 55 | position: 'absolute', 56 | top: 0, 57 | left: 0, 58 | right: 0, 59 | }, 60 | headerTitle: { 61 | backgroundColor: 'transparent', 62 | position: 'absolute', 63 | top: 0, 64 | left: 0, 65 | right: 0, 66 | paddingTop: STATUS_BAR_HEIGHT, 67 | alignItems: 'center', 68 | justifyContent: 'center', 69 | }, 70 | headerText: { 71 | color: DEFAULT_TITLE_COLOR, 72 | textAlign: 'center', 73 | fontSize: setSpText(16), 74 | }, 75 | absoluteAll: { 76 | width: screenW, 77 | height: scaleSize(280), 78 | position: 'absolute', 79 | top: 0, 80 | left: 0, 81 | }, 82 | }); 83 | 84 | class RNParallax extends Component { 85 | constructor() { 86 | super(); 87 | this.state = { 88 | scrollY: new Animated.Value(0), 89 | }; 90 | } 91 | 92 | getHeaderMaxHeight() { 93 | const { headerMaxHeight } = this.props; 94 | return headerMaxHeight; 95 | } 96 | 97 | getHeaderMinHeight() { 98 | const { headerMinHeight } = this.props; 99 | return headerMinHeight; 100 | } 101 | 102 | getHeaderScrollDistance() { 103 | return this.getHeaderMaxHeight() - this.getHeaderMinHeight(); 104 | } 105 | 106 | getExtraScrollHeight() { 107 | const { extraScrollHeight } = this.props; 108 | return extraScrollHeight; 109 | } 110 | 111 | getBackgroundImageScale() { 112 | const { backgroundImageScale } = this.props; 113 | return backgroundImageScale; 114 | } 115 | 116 | getInputRange() { 117 | return [-this.getExtraScrollHeight(), 0, this.getHeaderScrollDistance()]; 118 | } 119 | 120 | getHeaderHeight() { 121 | const { scrollY } = this.state; 122 | return scrollY.interpolate({ 123 | inputRange: this.getInputRange(), 124 | outputRange: [this.getHeaderMaxHeight() + this.getExtraScrollHeight(), this.getHeaderMaxHeight(), this.getHeaderMinHeight()], 125 | extrapolate: 'clamp', 126 | }); 127 | } 128 | 129 | getNavBarOpacity() { 130 | const { scrollY } = this.state; 131 | return scrollY.interpolate({ 132 | inputRange: this.getInputRange(), 133 | outputRange: [0, 1, 1], 134 | extrapolate: 'clamp', 135 | }); 136 | } 137 | 138 | getNavBarForegroundOpacity() { 139 | const { scrollY } = this.state; 140 | const { alwaysShowNavBar } = this.props; 141 | return scrollY.interpolate({ 142 | inputRange: this.getInputRange(), 143 | outputRange: [alwaysShowNavBar ? 1 : 0, alwaysShowNavBar ? 1 : 0, 1], 144 | extrapolate: 'clamp', 145 | }); 146 | } 147 | 148 | getImageOpacity() { 149 | const { scrollY } = this.state; 150 | return scrollY.interpolate({ 151 | inputRange: this.getInputRange(), 152 | outputRange: [1, 1, 0], 153 | extrapolate: 'clamp', 154 | }); 155 | } 156 | 157 | getImageTranslate() { 158 | const { scrollY } = this.state; 159 | return scrollY.interpolate({ 160 | inputRange: this.getInputRange(), 161 | outputRange: [0, 0, -50], 162 | extrapolate: 'clamp', 163 | }); 164 | } 165 | 166 | getImageScale() { 167 | const { scrollY } = this.state; 168 | return scrollY.interpolate({ 169 | inputRange: this.getInputRange(), 170 | outputRange: [this.getBackgroundImageScale(), 1, 1], 171 | extrapolate: 'clamp', 172 | }); 173 | } 174 | 175 | getTitleTranslateY() { 176 | const { scrollY } = this.state; 177 | return scrollY.interpolate({ 178 | inputRange: this.getInputRange(), 179 | outputRange: [5, 0, 0], 180 | extrapolate: 'clamp', 181 | }); 182 | } 183 | 184 | getTitleOpacity() { 185 | const { scrollY } = this.state; 186 | const { alwaysShowTitle } = this.props; 187 | return scrollY.interpolate({ 188 | inputRange: this.getInputRange(), 189 | outputRange: [1, 1, alwaysShowTitle ? 1 : 0], 190 | extrapolate: 'clamp', 191 | }); 192 | } 193 | getTitleOpacityHide() { 194 | const { scrollY } = this.state; 195 | const { alwaysShowTitle } = this.props; 196 | return scrollY.interpolate({ 197 | inputRange: this.getInputRange(), 198 | outputRange: [1, 1, alwaysShowTitle ? 0 : 1], 199 | extrapolate: 'clamp', 200 | }); 201 | } 202 | 203 | renderBackgroundImage() { 204 | const { backgroundImage } = this.props; 205 | const imageOpacity = this.getImageOpacity(); 206 | const imageTranslate = this.getImageTranslate(); 207 | const imageScale = this.getImageScale(); 208 | 209 | return ( 210 | 221 | ); 222 | } 223 | 224 | renderPlainBackground() { 225 | const { backgroundColor } = this.props; 226 | const imageOpacity = this.getImageOpacity(); 227 | const imageTranslate = this.getImageTranslate(); 228 | const imageScale = this.getImageScale(); 229 | return ( 230 | 238 | ); 239 | } 240 | 241 | renderNavbarBackground() { 242 | const { renderContainer, renderContainerStyle } = this.props; 243 | const titleTranslateY = this.getTitleTranslateY(); 244 | const titleOpacity = this.getTitleOpacityHide(); 245 | return ( 246 | 256 | {renderContainer()} 257 | 258 | ); 259 | } 260 | 261 | renderHeaderBackground() { 262 | const { backgroundImage, backgroundColor } = this.props; 263 | const imageOpacity = this.getImageOpacity(); 264 | 265 | return ( 266 | 276 | {backgroundImage && this.renderBackgroundImage()} 277 | {!backgroundImage && this.renderPlainBackground()} 278 | 279 | ); 280 | } 281 | 282 | renderHeaderTitle() { 283 | const { title, } = this.props; 284 | const titleTranslateY = this.getTitleTranslateY(); 285 | const titleOpacity = this.getTitleOpacity(); 286 | return ( 287 | 298 | {title()} 299 | 300 | ); 301 | } 302 | 303 | renderHeaderForeground() { 304 | const { renderNavBar } = this.props; 305 | const navBarOpacity = this.getNavBarForegroundOpacity(); 306 | return ( 307 | 316 | {renderNavBar()} 317 | 318 | ); 319 | } 320 | renderScrollView() { 321 | const { 322 | renderContent, scrollEventThrottle, scrollViewStyle, contentContainerStyle, innerContainerStyle, scrollViewProps, 323 | } = this.props; 324 | const { scrollY } = this.state; 325 | return ( 326 | 336 | 337 | {renderContent()} 338 | 339 | 340 | ); 341 | } 342 | 343 | render() { 344 | const { navbarColor, statusBarColor, containerStyle } = this.props; 345 | return ( 346 | 347 | 348 | {this.renderHeaderTitle()} 349 | {this.renderScrollView()} 350 | {this.renderNavbarBackground()} 351 | {this.renderHeaderForeground()} 352 | 353 | ); 354 | } 355 | } 356 | 357 | RNParallax.propTypes = { 358 | renderNavBar: PropTypes.func, 359 | renderContent: PropTypes.func.isRequired, 360 | backgroundColor: PropTypes.string, 361 | backgroundImage: PropTypes.any, 362 | navbarColor: PropTypes.string, 363 | title: PropTypes.any, 364 | titleStyle: PropTypes.any, 365 | headerTitleStyle: PropTypes.any, 366 | headerMaxHeight: PropTypes.number, 367 | headerMinHeight: PropTypes.number, 368 | scrollEventThrottle: PropTypes.number, 369 | extraScrollHeight: PropTypes.number, 370 | backgroundImageScale: PropTypes.number, 371 | contentContainerStyle: PropTypes.any, 372 | innerContainerStyle: PropTypes.any, 373 | scrollViewStyle: PropTypes.any, 374 | containerStyle: PropTypes.any, 375 | alwaysShowTitle: PropTypes.bool, 376 | alwaysShowNavBar: PropTypes.bool, 377 | statusBarColor: PropTypes.string, 378 | scrollViewProps: PropTypes.object, 379 | }; 380 | 381 | RNParallax.defaultProps = { 382 | renderNavBar: () => , 383 | navbarColor: DEFAULT_NAVBAR_COLOR, 384 | backgroundColor: DEFAULT_BACKGROUND_COLOR, 385 | backgroundImage: null, 386 | title: null, 387 | titleStyle: styles.headerText, 388 | headerTitleStyle: null, 389 | headerMaxHeight: DEFAULT_HEADER_MAX_HEIGHT, 390 | headerMinHeight: DEFAULT_HEADER_MIN_HEIGHT, 391 | scrollEventThrottle: SCROLL_EVENT_THROTTLE, 392 | extraScrollHeight: DEFAULT_EXTRA_SCROLL_HEIGHT, 393 | backgroundImageScale: DEFAULT_BACKGROUND_IMAGE_SCALE, 394 | contentContainerStyle: null, 395 | innerContainerStyle: null, 396 | scrollViewStyle: null, 397 | containerStyle: null, 398 | alwaysShowTitle: true, 399 | alwaysShowNavBar: true, 400 | statusBarColor: null, 401 | scrollViewProps: {}, 402 | }; 403 | 404 | export default RNParallax; 405 | --------------------------------------------------------------------------------