├── example
├── .watchmanconfig
├── .babelrc
├── app.json
├── App.test.js
├── .gitignore
├── README.md
├── package.json
├── App.js
└── .flowconfig
├── .gitignore
├── .babelrc
├── LICENSE
├── package.json
├── README.md
└── src
└── index.js
/example/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 | yarn.lock
4 | package-lock.json
--------------------------------------------------------------------------------
/example/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["babel-preset-expo"],
3 | "env": {
4 | "development": {
5 | "plugins": ["transform-react-jsx-source"]
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["react-native"],
3 | "env": {
4 | "development": {
5 | "plugins": ["transform-react-jsx-source"]
6 | }
7 | },
8 | "retainLines": true
9 | }
10 |
--------------------------------------------------------------------------------
/example/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "react-native-offscreen-toolbar example",
4 | "sdkVersion": "25.0.0",
5 | "androidStatusBar": {
6 | "backgroundColor": "#0288D1"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/example/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import App from './App';
3 |
4 | import renderer from 'react-test-renderer';
5 |
6 | it('renders without crashing', () => {
7 | const rendered = renderer.create().toJSON();
8 | expect(rendered).toBeTruthy();
9 | });
10 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | # expo
2 | .expo/
3 |
4 | # dependencies
5 | /node_modules
6 |
7 | # misc
8 | .env.local
9 | .env.development.local
10 | .env.test.local
11 | .env.production.local
12 |
13 | npm-debug.log*
14 | yarn-debug.log*
15 | yarn-error.log*
16 |
17 | yarn.lock
18 | package-lock.json
19 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | # react-native-offscreen-toolbar: example application
2 |
3 | Example usage of the library, using a simple `FlatList` for the scrollable content and `ToolbarAndroid` for the toolbar.
4 |
5 | Created via [create-react-native-app](https://github.com/react-community/create-react-native-app)
6 |
7 | 
8 |
9 |
10 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "version": "1.0.0",
4 | "private": true,
5 | "devDependencies": {
6 | "react-native-scripts": "1.10.0",
7 | "jest-expo": "25.0.0",
8 | "react-test-renderer": "16.2.0"
9 | },
10 | "main": "./node_modules/react-native-scripts/build/bin/crna-entry.js",
11 | "scripts": {
12 | "start": "react-native-scripts start",
13 | "eject": "react-native-scripts eject",
14 | "android": "react-native-scripts android",
15 | "ios": "react-native-scripts ios",
16 | "test": "node node_modules/jest/bin/jest.js"
17 | },
18 | "jest": {
19 | "preset": "jest-expo"
20 | },
21 | "dependencies": {
22 | "expo": "^25.0.0",
23 | "react": "16.2.0",
24 | "react-native": "0.52.0",
25 | "react-native-offscreen-toolbar": "~1.0.0"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018, Pedro Lopes
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/example/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, View, Text, ToolbarAndroid, FlatList } from 'react-native';
3 | import OffscreenToolbar from 'react-native-offscreen-toolbar';
4 |
5 | const TOOLBAR_HEIGHT = 56;
6 | const DUMMY_DATA = new Array(50).fill({}).map((elem, index) => ({key: `List Item ${index}`}));
7 |
8 | export default class App extends React.Component {
9 | render() {
10 | const toolbar = () => ();
11 | const listItem = ({item}) => {item.key};
12 | const scrollable = () => ();
13 | return (
14 |
15 |
20 |
21 | );
22 | }
23 | }
24 |
25 | const styles = StyleSheet.create({
26 | container: {
27 | flex: 1,
28 | },
29 | toolbar: {
30 | backgroundColor: '#03A9F4',
31 | height: TOOLBAR_HEIGHT,
32 | alignSelf: 'stretch',
33 | },
34 | listItem: {
35 | height: 48,
36 | paddingLeft: 16,
37 | paddingRight: 16,
38 | textAlignVertical: 'center',
39 | fontSize: 16,
40 | }
41 | });
42 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-offscreen-toolbar",
3 | "version": "1.0.10",
4 | "description": "Component which animates the toolbar off-screen while scrolling, a material design pattern",
5 | "main": "src/index.js",
6 | "files": [
7 | "./src/index.js"
8 | ],
9 | "scripts": {
10 | "test": "echo \"Error: no test specified\" && exit 1"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "https://github.com/lopespm/react-native-offscreen-toolbar.git"
15 | },
16 | "keywords": [
17 | "react-native",
18 | "react-component",
19 | "react-native-component",
20 | "react",
21 | "mobile",
22 | "ui",
23 | "component",
24 | "toolbar",
25 | "navbar",
26 | "header",
27 | "material",
28 | "material-design"
29 | ],
30 | "author": "Pedro Lopes",
31 | "license": "MIT",
32 | "bugs": {
33 | "url": "https://github.com/lopespm/react-native-offscreen-toolbar/issues"
34 | },
35 | "homepage": "https://github.com/lopespm/react-native-offscreen-toolbar",
36 | "dependencies": {
37 | "prop-types": "^15.6.0"
38 | },
39 | "peerDependencies": {
40 | "react": ">=16.0.0",
41 | "react-native": ">=0.50.0"
42 | },
43 | "devDependencies": {
44 | "react": "^16.0.0",
45 | "react-native": "^0.50.0",
46 | "babel-eslint": "^8.1.2",
47 | "babel-preset-react-native-stage-0": "^1.0.1",
48 | "eslint-config-airbnb": "^16.1.0",
49 | "eslint-plugin-import": "^2.8.0",
50 | "eslint-plugin-jsx-a11y": "^6.0.3",
51 | "eslint-plugin-react": "^7.5.1"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-native-offscreen-toolbar [](https://badge.fury.io/js/react-native-offscreen-toolbar)
2 |
3 | Component generalization of [Janic Duplessis' solution](https://medium.com/appandflow/react-native-collapsible-navbar-e51a049b560a) to animate the toolbar off-screen while scrolling, a common [material design pattern](https://material.io/guidelines/patterns/scrolling-techniques.html#scrolling-techniques-behavior)
4 |
5 |   
6 |
7 | Library usage in both the example bundled with the library and in the search screen of a to be released application
8 |
9 |
10 | ## Installation
11 |
12 | `$ npm install react-native-offscreen-toolbar --save`
13 |
14 | ## Usage
15 |
16 | ```js
17 | import OffscreenToolbar from 'react-native-offscreen-toolbar';
18 |
19 | export default class YourComponent extends React.Component {
20 | render() {
21 | const toolbar = () => ();
22 | const listItem = ({item}) => {item.key};
23 | const scrollable = () => ();
24 | return (
25 |
26 |
29 |
30 | );
31 | }
32 | }
33 | ```
34 |
35 | ### Properties
36 |
37 | | Prop | Description | Default |
38 | |---|---|---|
39 | |**`toolbar`**|Component for the toolbar/navbar. |*None*|
40 | |**`scrollable`**|Component for the scrollable, a `FlatList` or `ScrollView` for example. |*None*|
41 | |**`scrollableOverlay`**|Optional component for the scrollable overlay. |*None*|
42 | |**`toolbarHeight`**|Toolbar height used when calculating the animations. |`56`|
43 | |**`scrollablePaddingTop`**|Since the scrollable is placed behind the toolbar, this padding is typically used to make the scrollable content appear below the toolbar. |`64`|
44 |
45 |
46 | ## Example
47 |
48 | * [`Example` project bundled with this module, using a simple FlatList](https://github.com/lopespm/react-native-offscreen-toolbar/tree/master/example)
--------------------------------------------------------------------------------
/example/.flowconfig:
--------------------------------------------------------------------------------
1 | [ignore]
2 | ; We fork some components by platform
3 | .*/*[.]android.js
4 |
5 | ; Ignore templates for 'react-native init'
6 | /node_modules/react-native/local-cli/templates/.*
7 |
8 | ; Ignore RN jest
9 | /node_modules/react-native/jest/.*
10 |
11 | ; Ignore RNTester
12 | /node_modules/react-native/RNTester/.*
13 |
14 | ; Ignore the website subdir
15 | /node_modules/react-native/website/.*
16 |
17 | ; Ignore the Dangerfile
18 | /node_modules/react-native/danger/dangerfile.js
19 |
20 | ; Ignore Fbemitter
21 | /node_modules/fbemitter/.*
22 |
23 | ; Ignore "BUCK" generated dirs
24 | /node_modules/react-native/\.buckd/
25 |
26 | ; Ignore unexpected extra "@providesModule"
27 | .*/node_modules/.*/node_modules/fbjs/.*
28 |
29 | ; Ignore polyfills
30 | /node_modules/react-native/Libraries/polyfills/.*
31 |
32 | ; Ignore various node_modules
33 | /node_modules/react-native-gesture-handler/.*
34 | /node_modules/expo/.*
35 | /node_modules/react-navigation/.*
36 | /node_modules/xdl/.*
37 | /node_modules/reqwest/.*
38 | /node_modules/metro-bundler/.*
39 |
40 | [include]
41 |
42 | [libs]
43 | node_modules/react-native/Libraries/react-native/react-native-interface.js
44 | node_modules/react-native/flow/
45 | node_modules/expo/flow/
46 |
47 | [options]
48 | emoji=true
49 |
50 | module.system=haste
51 |
52 | module.file_ext=.js
53 | module.file_ext=.jsx
54 | module.file_ext=.json
55 | module.file_ext=.ios.js
56 |
57 | munge_underscores=true
58 |
59 | module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub'
60 |
61 | suppress_type=$FlowIssue
62 | suppress_type=$FlowFixMe
63 | suppress_type=$FlowFixMeProps
64 | suppress_type=$FlowFixMeState
65 | suppress_type=$FixMe
66 |
67 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(5[0-6]\\|[1-4][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native_oss[a-z,_]*\\)?)\\)
68 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(5[0-6]\\|[1-4][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native_oss[a-z,_]*\\)?)\\)?:? #[0-9]+
69 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
70 | suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError
71 |
72 | unsafe.enable_getters_and_setters=true
73 |
74 | [version]
75 | ^0.56.0
76 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { StyleSheet, View, Animated, StatusBar } from 'react-native';
3 | import PropTypes from 'prop-types';
4 |
5 | const STATUS_BAR_HEIGHT = StatusBar.currentHeight;
6 |
7 | class OffscreenToolbar extends Component {
8 | static propTypes = {
9 | toolbar: PropTypes.func.isRequired,
10 | scrollableOverlay: PropTypes.func,
11 | scrollable: PropTypes.func.isRequired,
12 | toolbarHeight: PropTypes.number.isRequired,
13 | scrollablePaddingTop: PropTypes.number.isRequired,
14 | };
15 |
16 | static defaultProps = {
17 | toolbarHeight: 56,
18 | scrollablePaddingTop: 56 + 8,
19 | };
20 |
21 | constructor(props) {
22 | super(props);
23 |
24 | const scrollAnim = new Animated.Value(0);
25 | const offsetAnim = new Animated.Value(0);
26 |
27 | this.state = {
28 | scrollAnim,
29 | offsetAnim,
30 | clampedScroll: Animated.diffClamp(
31 | Animated.add(
32 | scrollAnim.interpolate({
33 | inputRange: [0, 1],
34 | outputRange: [0, 1],
35 | extrapolateLeft: 'clamp',
36 | }),
37 | offsetAnim,
38 | ),
39 | 0,
40 | props.toolbarHeight,
41 | ),
42 | };
43 | }
44 |
45 | clampedScrollValue = 0;
46 | offsetValue = 0;
47 | scrollValue = 0;
48 |
49 | componentDidMount() {
50 | this.state.scrollAnim.addListener(({ value }) => {
51 | const diff = value - this.scrollValue;
52 | this.scrollValue = value;
53 | this.clampedScrollValue = Math.min(
54 | Math.max(this.clampedScrollValue + diff, 0),
55 | this.props.toolbarHeight
56 | );
57 | });
58 | this.state.offsetAnim.addListener(({ value }) => {
59 | this.offsetValue = value;
60 | });
61 | }
62 |
63 | componentWillUnmount() {
64 | this.state.scrollAnim.removeAllListeners();
65 | this.state.offsetAnim.removeAllListeners();
66 | }
67 |
68 | onScrollEndDrag = () => {
69 | this.scrollEndTimer = setTimeout(this.onMomentumScrollEnd, 250);
70 | };
71 |
72 | onMomentumScrollBegin = () => {
73 | clearTimeout(this.scrollEndTimer);
74 | };
75 |
76 | onMomentumScrollEnd = () => {
77 | const statusAndToolbarHeight = this.props.toolbarHeight + STATUS_BAR_HEIGHT;
78 | const targetOffset = this.isToolbarNearHidingPosition()
79 | ? this.offsetValue + statusAndToolbarHeight
80 | : this.offsetValue - statusAndToolbarHeight ;
81 |
82 | Animated.timing(this.state.offsetAnim, {
83 | toValue: targetOffset,
84 | duration: 350,
85 | }).start();
86 | };
87 |
88 | isToolbarNearHidingPosition() {
89 | const toolbarHeight = this.props.toolbarHeight;
90 | const statusAndToolbarHeight = this.props.toolbarHeight + STATUS_BAR_HEIGHT;
91 | return this.scrollValue > statusAndToolbarHeight &&
92 | this.clampedScrollValue > (toolbarHeight) / 2;
93 | }
94 |
95 | scrollableWithScrollHooks = () => {
96 | const scrollable = this.props.scrollable();
97 | return React.cloneElement(
98 | scrollable,
99 | {
100 | scrollEventThrottle: 1,
101 | onMomentumScrollBegin: this.onMomentumScrollBegin,
102 | onMomentumScrollEnd: this.onMomentumScrollEnd,
103 | onScrollEndDrag: this.onScrollEndDrag,
104 | onScroll: Animated.event([{ nativeEvent: { contentOffset: { y: this.state.scrollAnim } } }]),
105 | contentContainerStyle: { paddingTop: this.props.scrollablePaddingTop },
106 | style: { ...scrollable.props.style },
107 | },
108 | );
109 | }
110 |
111 | render() {
112 | const toolbarHeight = this.props.toolbarHeight;
113 | const toolbarTranslationY = this.state.clampedScroll.interpolate({
114 | inputRange: [0, toolbarHeight],
115 | outputRange: [0, -toolbarHeight],
116 | extrapolate: 'clamp',
117 | });
118 |
119 | return (
120 |
121 | {this.scrollableWithScrollHooks()}
122 | {this.props.scrollableOverlay ? this.props.scrollableOverlay() : null}
123 |
124 | {this.props.toolbar()}
125 |
126 |
127 | );
128 | }
129 | }
130 |
131 | const styles = StyleSheet.create({
132 | container: {
133 | flex: 1,
134 | },
135 | toolbarContainer: {
136 | position: 'absolute',
137 | top: 0,
138 | left: 0,
139 | right: 0,
140 | },
141 | });
142 |
143 | export default OffscreenToolbar;
144 |
--------------------------------------------------------------------------------