├── AnimatedHeader.js ├── Header.js ├── demo ├── android-gif.gif ├── ios-gif.gif └── ipx.gif ├── index.js ├── package.json └── readme.md /AnimatedHeader.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View, 4 | } from 'react-native'; 5 | import Header from './Header'; 6 | 7 | type Props = { 8 | style?: any; 9 | backText?: string; 10 | title?: string; 11 | renderLeft?: () => React.Component; 12 | renderRight?: () => React.Component; 13 | backStyle?: any; 14 | backTextStyle?: any; 15 | titleStyle?: any; 16 | toolbarColor?: string; 17 | headerMaxHeight?: number; 18 | disabled?: boolean; 19 | noBorder?: boolean; 20 | parallax?: boolean; 21 | imageSource?: any; 22 | }; 23 | 24 | export default class AnimatedHeader extends React.PureComponent { 25 | 26 | _onScroll = (e) => { 27 | this.header.onScroll(e); 28 | } 29 | 30 | render() { 31 | const arr = React.Children.toArray(this.props.children); 32 | if (arr.length === 0) { 33 | console.error('AnimatedHeader must have ScrollView or FlatList as a child'); 34 | } 35 | if (arr.length > 1) { 36 | console.error('Invalid child, only 1 child accepted') 37 | } 38 | const { headerMaxHeight } = this.props; 39 | const { style, ref, scrollEventThrottle, onScroll, contentContainerStyle, ...rest } = arr[0].props; 40 | const child = React.cloneElement(arr[0], { 41 | style: { ...style, flex: 1, }, 42 | ref: (r) => this.scrollView = r, 43 | scrollEventThrottle: 16, 44 | onScroll: this._onScroll, 45 | contentContainerStyle: { ...contentContainerStyle, paddingTop: headerMaxHeight || 200, }, 46 | ...rest 47 | }); 48 | 49 | return ( 50 | 51 | {child} 52 |
{this.header = r;}} 55 | /> 56 | 57 | ); 58 | } 59 | } -------------------------------------------------------------------------------- /Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Animated, Platform, StyleSheet, View, TouchableOpacity, Dimensions } from 'react-native'; 3 | 4 | const ios = Platform.OS === 'ios'; 5 | const {width, height} = Dimensions.get('window'); 6 | // from native-base 7 | const isIphoneX = ios && (height === 812 || width === 812); 8 | const iphoneXTopInset = 24; 9 | const initToolbarHeight = ios ? 46 : 56; 10 | 11 | const paddingTop = ios ? 18 : 0; 12 | const topInset = isIphoneX ? iphoneXTopInset : 0; 13 | 14 | const toolbarHeight = initToolbarHeight + topInset + paddingTop; 15 | 16 | export default class Header extends React.PureComponent { 17 | 18 | constructor(props) { 19 | super(props); 20 | this.headerHeight = props.headerMaxHeight; 21 | this.state = { 22 | scrollOffset: new Animated.Value(0), 23 | left: 0, 24 | bottom: 0, 25 | }; 26 | } 27 | 28 | onScroll = e => { 29 | if (this.props.disabled) { 30 | return; 31 | } 32 | this.state.scrollOffset.setValue(e.nativeEvent.contentOffset.y); 33 | }; 34 | 35 | onBackLayout = (e) => { 36 | const layout = e.nativeEvent.layout; 37 | const bottom = toolbarHeight - layout.y - layout.height - paddingTop - topInset; 38 | this.setState({bottom: bottom, left: e.nativeEvent.layout.x}) 39 | } 40 | 41 | _getFontSize = () => { 42 | const { scrollOffset } = this.state; 43 | const backFontSize = this.props.backTextStyle.fontSize || Header.defaultProps.backTextStyle.fontSize; 44 | const titleFontSize = this.props.titleStyle.fontSize || Header.defaultProps.titleStyle.fontSize; 45 | return scrollOffset.interpolate({ 46 | inputRange: [0, this.headerHeight - toolbarHeight], 47 | outputRange: [titleFontSize, backFontSize], 48 | extrapolate: 'clamp', 49 | }); 50 | } 51 | 52 | _getLeft = () => { 53 | const { scrollOffset } = this.state; 54 | const left = this.props.titleStyle.left || Header.defaultProps.titleStyle.left; 55 | return scrollOffset.interpolate({ 56 | inputRange: [0, this.headerHeight - toolbarHeight], 57 | outputRange: [left, this.state.left], 58 | extrapolate: 'clamp', 59 | }); 60 | } 61 | 62 | _getHeight = () => { 63 | const { scrollOffset } = this.state; 64 | return scrollOffset.interpolate({ 65 | inputRange: [0, this.headerHeight - toolbarHeight], 66 | outputRange: [this.headerHeight, toolbarHeight], 67 | extrapolate: 'clamp', 68 | }) 69 | } 70 | 71 | _getBottom = () => { 72 | const { scrollOffset } = this.state; 73 | const bottom = this.props.titleStyle.bottom || Header.defaultProps.titleStyle.bottom; 74 | return scrollOffset.interpolate({ 75 | inputRange: [0, this.headerHeight - toolbarHeight], 76 | outputRange: [bottom, this.state.bottom], 77 | extrapolate: 'clamp', 78 | }); 79 | } 80 | 81 | _getOpacity = () => { 82 | const { scrollOffset } = this.state; 83 | return this.props.backText ? scrollOffset.interpolate({ 84 | inputRange: [0, this.headerHeight - toolbarHeight], 85 | outputRange: [1, 0], 86 | extrapolate: 'clamp', 87 | }) : 0 88 | } 89 | 90 | _getImageOpacity = () => { 91 | const { scrollOffset } = this.state; 92 | return this.props.imageSource ? scrollOffset.interpolate({ 93 | inputRange: [0, this.headerHeight - toolbarHeight], 94 | outputRange: [1, 0], 95 | extrapolate: 'clamp', 96 | }) : 0 97 | } 98 | 99 | _getImageScaleStyle = () => { 100 | if (!this.props.parallax) { 101 | return undefined; 102 | } 103 | const { scrollOffset } = this.state; 104 | const scale = scrollOffset.interpolate({ 105 | inputRange: [-100, -0], 106 | outputRange: [1.5, 1], 107 | extrapolate: 'clamp', 108 | }) 109 | 110 | return { 111 | transform: [ 112 | { 113 | scale, 114 | } 115 | ] 116 | } 117 | } 118 | 119 | render() { 120 | const { imageSource, toolbarColor, titleStyle, onBackPress, backStyle, backTextStyle } = this.props; 121 | const height = this._getHeight(); 122 | const left = this._getLeft(); 123 | const bottom = this._getBottom(); 124 | const opacity = this._getOpacity(); 125 | const fontSize = this._getFontSize(); 126 | const imageOpacity = this._getImageOpacity(); 127 | const headerStyle = this.props.noBorder ? undefined : { borderBottomWidth: 1, borderColor: '#a7a6ab'} 128 | 129 | return ( 130 | 139 | {imageSource && } 144 | 145 | 146 | 147 | {this.props.renderLeft && this.props.renderLeft()} 148 | 149 | {this.props.backText || 'Back2'} 150 | 151 | 152 | {this.props.renderRight && this.props.renderRight()} 153 | 154 | 155 | 161 | {this.props.title} 162 | 163 | 164 | ); 165 | } 166 | } 167 | 168 | const styles = StyleSheet.create({ 169 | toolbarContainer: { 170 | height: toolbarHeight 171 | }, 172 | statusBar: { 173 | height: topInset + paddingTop 174 | }, 175 | toolbar: { 176 | flex: 1, 177 | flexDirection: 'row', 178 | alignItems: 'center' 179 | }, 180 | header: { 181 | position: 'absolute', 182 | top: 0, 183 | left: 0, 184 | right: 0, 185 | }, 186 | titleButton: { 187 | flexDirection: 'row', 188 | }, 189 | flexView: { 190 | flex: 1, 191 | }, 192 | }); 193 | 194 | 195 | Header.defaultProps = { 196 | backText: '', 197 | title: '', 198 | renderLeft: undefined, 199 | renderRight: undefined, 200 | backStyle: { marginLeft: 10 }, 201 | backTextStyle: { fontSize: 16 }, 202 | titleStyle: { fontSize: 20, left: 40, bottom: 30 }, 203 | toolbarColor: '#FFF', 204 | headerMaxHeight: 200, 205 | disabled: false, 206 | imageSource: undefined, 207 | } -------------------------------------------------------------------------------- /demo/android-gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maphongba008/react-native-animated-header/e71531f978bb363853b5871c26758cb543a4dc04/demo/android-gif.gif -------------------------------------------------------------------------------- /demo/ios-gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maphongba008/react-native-animated-header/e71531f978bb363853b5871c26758cb543a4dc04/demo/ios-gif.gif -------------------------------------------------------------------------------- /demo/ipx.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maphongba008/react-native-animated-header/e71531f978bb363853b5871c26758cb543a4dc04/demo/ipx.gif -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import AnimatedHeader from './AnimatedHeader'; 2 | 3 | export default AnimatedHeader; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-animated-header", 3 | "version": "1.0.6", 4 | "description": "Collapsing toolbar for Android and iOS", 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/maphongba008/react-native-animated-header.git" 12 | }, 13 | "keywords": [ 14 | "react", 15 | "react-native", 16 | "animated", 17 | "header" 18 | ], 19 | "author": "maphongba008", 20 | "license": "ISC", 21 | "bugs": { 22 | "url": "https://github.com/maphongba008/react-native-animated-header/issues" 23 | }, 24 | "homepage": "https://github.com/maphongba008/react-native-animated-header#readme" 25 | } 26 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | [![Version](https://img.shields.io/npm/v/react-native-animated-header.svg)](https://www.npmjs.com/package/react-native-animated-header) 3 | [![NPM](https://img.shields.io/npm/dm/react-native-animated-header.svg)](https://www.npmjs.com/package/react-native-animated-header) 4 | 5 | 6 | # react-native-animated-form 7 | 8 | Collapsing toolbar for Android and iOS 9 | 10 | ## Installation 11 | 12 | ```bash 13 | npm install --save react-native-animated-header 14 | or 15 | yarn add react-native-animated-header 16 | ``` 17 | 18 | ## Demo 19 | 20 | Android
21 | ![Android](https://raw.githubusercontent.com/maphongba008/react-native-animated-header/master/demo/android-gif.gif) 22 |
23 | iOS
24 | ![iOS](https://raw.githubusercontent.com/maphongba008/react-native-animated-header/master/demo/ios-gif.gif) 25 | iPhone X
26 | ![iPhoneX]
(https://raw.githubusercontent.com/maphongba008/react-native-animated-header/master/demo/ipx.gif) 27 | 28 | ## Usage 29 | 30 | ```javascript 31 | import React, { Component } from 'react'; 32 | import { Text, View, ScrollView } from 'react-native'; 33 | import { Icon } from 'native-base'; 34 | import AnimatedHeader from 'react-native-animated-header'; 35 | import Bg from './assets/bg.jpg'; 36 | 37 | getListItems = count => { 38 | const items = []; 39 | let i = 0; 40 | 41 | while (i < count) { 42 | i++; 43 | items.push( 44 | 45 | {`List Item ${i}`} 46 | 47 | ); 48 | } 49 | 50 | return items; 51 | }; 52 | 53 | export default class App extends Component { 54 | 55 | render() { 56 | return ( 57 | ()} 62 | renderRight={() => ()} 63 | backStyle={{ marginLeft: 10 }} 64 | backTextStyle={{fontSize: 14, color: '#000'}} 65 | titleStyle={{ fontSize: 22, left: 20, bottom: 20, color: '#000' }} 66 | headerMaxHeight={200} 67 | imageSource={Bg} 68 | toolbarColor='#FFF' 69 | disabled={false} 70 | > 71 | 72 | {getListItems(20)} 73 | 74 | 75 | ); 76 | } 77 | } 78 | 79 | ``` 80 | 81 | ## Properties 82 | 83 | name | description | type | isOptional | default 84 | :---- |:----------- | :----| ---- | :------- 85 | backText | Back text, leave it empty to hide | String | Yes | `undefined` 86 | title | Header title | String | Yes | `undefined` 87 | renderLeft | To render icon on the left | Function | Yes | `undefined` 88 | renderRight | To render icon on the right | Function | Yes | `undefined` 89 | backStyle | Style of back container | Object | Yes | { marginLeft: 10 } 90 | backTextStyle | Style of back text | Object | Yes | { fontSize: 16 } 91 | titleStyle | Style of title, use `left` and `bottom` for positioning the text | Object | Yes | { fontSize: 20, left: 40, bottom: 30 } 92 | toolbarColor | Toolbar background color | String | Yes | `#FFF` 93 | headerMaxHeight | Height of header when expanded | Number | Yes | `200` 94 | disabled | Do not allow header to collapse | Boolean | Yes | `false` 95 | noBorder | Hide header separator | Boolean | Yes | `false` 96 | imageSource | Image background for header | Image | Yes | `undefined` 97 | parallax | Use parallax effect | Boolean | Yes | `false` 98 | 99 | 100 | ## Warning 101 | 102 | `AnimatedHeader` only accept 1 child, `ScrollView` or `FlatList` 103 | 104 | ## Copyright and License 105 | 106 | MIT License 107 | 108 | Copyright (c) 2018 maphongba008 109 | --------------------------------------------------------------------------------