├── .babelrc
├── .gitignore
├── LICENSE
├── README.md
├── demo.gif
├── example
└── example.js
├── index.js
├── package-lock.json
├── package.json
└── src
├── Animator.js
└── BottomDrawer.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["module:metro-react-native-babel-preset"]
3 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/**/*
2 | .DS_Store
3 | .watchmanconfig
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Jack Klein
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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Bottom drawer for React Native
2 |
3 |
4 |
5 |
6 |
7 | ## Content
8 |
9 | - [Installation](#installation)
10 | - [Usage example](#usage-example)
11 | - [Configuration](#configuration)
12 | - [Questions?](#questions)
13 |
14 | ## Installation
15 |
16 | Install `rn-bottom-drawer`.
17 |
18 | ```
19 | npm install rn-bottom-drawer --save
20 | ```
21 |
22 | ## Usage Example
23 | (go to the example folder for a more fleshed out example)
24 |
25 | ```javascript
26 | import React from 'react';
27 | import { View, Text } from 'react-native';
28 | import BottomDrawer from 'rn-bottom-drawer';
29 |
30 | const TAB_BAR_HEIGHT = 49;
31 |
32 | export default class App extends React.Component {
33 | renderContent = () => {
34 | return (
35 |
36 | Get directions to your location
37 |
38 | )
39 | }
40 |
41 | render() {
42 | return (
43 |
47 | {this.renderContent()}
48 |
49 | )
50 | }
51 | }
52 |
53 | ```
54 | [Refer to this code](https://github.com/jacklein/rn-bottom-drawer/issues/7#issuecomment-465554054) if you want to put a **scrollview** within the bottom drawer
55 |
56 | ## Configuration
57 |
58 | | Prop | Type | Default | Description |
59 | | ---- | ---- | ----| ---- |
60 | | containerHeight | number | -- | The height of the drawer. |
61 | | offset | number | 0 | If your app uses tab navigation or a header, **offset** equals their combined heights. In the demo gif, the offset is the header + tab heights so that the drawer renders correctly within the map view. |
62 | | downDisplay | number | containerHeight / 1.5 | When the drawer is swiped into down position, **downDisplay** controls how far it settles below its up position. For example, if its value is 20, the drawer will settle 20 points below the up position. The default value shows 1/3 of the container (if containerHeight = 60, the default downDisplay value = 40). |
63 | | backgroundColor | string | '#ffffff' | The background color of the drawer. |
64 | | startUp | bool | true | If **true**, the drawer will start in up position. If **false**, it will start in down position. |
65 | | roundedEdges | bool | true | If **true**, the top of the drawer will have rounded edges. |
66 | | shadow | bool | true | if **true**, the top of the drawer will have a shadow. |
67 | | onExpanded | func | -- | A callback function triggered when the drawer is swiped into up position |
68 | | onCollapsed | func | -- | A callback function triggered when the drawer is swiped into down position |
69 |
70 | ### Questions?
71 | Feel free to contact me at [jackdillklein@gmail.com](mailto:jackdillklein@gmail.com) or [create an issue](https://github.com/jacklein/rn-bottom-drawer/issues/new)
72 |
--------------------------------------------------------------------------------
/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jacklein/rn-bottom-drawer/35513d5e627871feeb72cbb437dd33acf72518f8/demo.gif
--------------------------------------------------------------------------------
/example/example.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View, Text,StyleSheet } from 'react-native';
3 | import { Button } from 'react-native-elements';
4 | import BottomDrawer from 'rn-bottom-drawer';
5 |
6 | // this example assumes you're using a header and a tab bar
7 | const TAB_BAR_HEIGHT = 49;
8 | const HEADER_HEIGHT = 60;
9 |
10 | export default class App extends React.Component {
11 | renderContent = () => {
12 | return (
13 |
14 | Get directions to your location
15 |
16 |
17 |
18 |
19 |
20 | )
21 | }
22 |
23 | render() {
24 | return (
25 | {console.log('expanded')}}
29 | onCollapsed = {() => {console.log('collapsed')}}
30 | >
31 | {this.renderContent()}
32 |
33 | )
34 | }
35 | }
36 |
37 | const styles = StyleSheet.create({
38 | contentContainer: {
39 | flex: 1,
40 | alignItems: 'center',
41 | justifyContent: 'space-around'
42 | },
43 | buttonContainer: {
44 | flexDirection: 'row',
45 | },
46 | text: {
47 | paddingHorizontal: 5
48 | }
49 | });
50 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import BottomDrawer from './src/BottomDrawer';
2 |
3 | export default BottomDrawer;
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rn-bottom-drawer",
3 | "version": "1.4.3",
4 | "description": "a bottom drawer component for react native",
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/jacklein/rn-bottom-drawer.git"
12 | },
13 | "keywords": [
14 | "react-native",
15 | "drawer"
16 | ],
17 | "author": "Jack K",
18 | "license": "ISC",
19 | "bugs": {
20 | "url": "https://github.com/jacklein/rn-bottom-drawer/issues"
21 | },
22 | "homepage": "https://github.com/jacklein/rn-bottom-drawer#readme",
23 | "dependencies": {
24 | "prop-types": "^15.6.2"
25 | },
26 | "devDependencies": {
27 | "metro-react-native-babel-preset": "^0.51.0",
28 | "react": "^16.6.3",
29 | "react-native": "^0.57.8"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Animator.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import {
3 | PanResponder,
4 | Animated,
5 | Dimensions,
6 | StyleSheet
7 | } from 'react-native';
8 |
9 | const SCREEN_HEIGHT = Dimensions.get('window').height;
10 | const SCREEN_WIDTH = Dimensions.get('window').width;
11 |
12 | export default class Animator extends Component{
13 | constructor(props){
14 | super(props);
15 |
16 | this.position = new Animated.ValueXY(this.props.currentPosition);
17 |
18 | this._panResponder = PanResponder.create({
19 | onStartShouldSetPanResponder: () => true,
20 | onPanResponderMove: this._handlePanResponderMove,
21 | onPanResponderRelease: this._handlePanResponderRelease
22 | });
23 | }
24 |
25 | render() {
26 | return (
27 |
38 | {this.props.children}
39 |
40 | )
41 | }
42 |
43 | _handlePanResponderMove = (e, gesture) => {
44 | if (this._swipeInBounds(gesture)) {
45 | this.position.setValue({ y: this.props.currentPosition.y + gesture.dy });
46 | } else {
47 | this.position.setValue({ y: this.props.upPosition.y - this._calculateEase(gesture) });
48 | }
49 | }
50 |
51 | _handlePanResponderRelease = (e, gesture) => {
52 | if (gesture.dy > this.props.toggleThreshold && this.props.currentPosition === this.props.upPosition) {
53 | this._transitionTo(this.props.downPosition, this.props.onCollapsed);
54 | } else if (gesture.dy < -this.props.toggleThreshold && this.props.currentPosition === this.props.downPosition) {
55 | this._transitionTo(this.props.upPosition, this.props.onExpanded);
56 | } else {
57 | this._resetPosition();
58 | }
59 | }
60 |
61 | // returns true if the swipe is within the height of the drawer.
62 | _swipeInBounds(gesture) {
63 | return this.props.currentPosition.y + gesture.dy > this.props.upPosition.y;
64 | }
65 |
66 | _calculateEase(gesture) {
67 | return Math.min(Math.sqrt(gesture.dy * -1), Math.sqrt(SCREEN_HEIGHT));
68 | }
69 |
70 | _transitionTo(position, callback) {
71 | Animated.spring(this.position, {
72 | toValue: position
73 | }).start(() => this.props.onExpanded());
74 |
75 | this.props.setCurrentPosition(position);
76 | callback();
77 | }
78 |
79 | _resetPosition() {
80 | Animated.spring(this.position, {
81 | toValue: this.props.currentPosition
82 | }).start();
83 | }
84 | }
85 |
86 | const styles = {
87 | animationContainer: (height, color) => ({
88 | width: SCREEN_WIDTH,
89 | position: 'absolute',
90 | height: height + Math.sqrt(SCREEN_HEIGHT),
91 | backgroundColor: color,
92 | }),
93 | roundedEdges: rounded => {
94 | return rounded == true && {
95 | borderTopLeftRadius: 10,
96 | borderTopRightRadius: 10,
97 | }
98 | },
99 | shadow: shadow => {
100 | return shadow == true && {
101 | shadowColor: '#CECDCD',
102 | shadowRadius: 3,
103 | shadowOpacity: 5,
104 | }
105 | },
106 | }
--------------------------------------------------------------------------------
/src/BottomDrawer.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import {
4 | View,
5 | Dimensions,
6 | } from 'react-native';
7 |
8 | import Animator from './Animator';
9 |
10 | const SCREEN_HEIGHT = Dimensions.get('window').height;
11 |
12 | export default class BottomDrawer extends Component{
13 | static propTypes = {
14 | /**
15 | * Height of the drawer.
16 | */
17 | containerHeight: PropTypes.number.isRequired,
18 |
19 | /**
20 | * The amount of offset to apply to the drawer's position.
21 | * If the app uses a header and tab navigation, offset should equal
22 | * the sum of those two components' heights.
23 | */
24 | offset: PropTypes.number,
25 |
26 | /**
27 | * Set to true to have the drawer start in up position.
28 | */
29 | startUp: PropTypes.bool,
30 |
31 | /**
32 | * How much the drawer's down display falls beneath the up display.
33 | * Ex: if set to 20, the down display will be 20 points underneath the up display.
34 | */
35 | downDisplay: PropTypes.number,
36 |
37 | /**
38 | * The background color of the drawer.
39 | */
40 | backgroundColor: PropTypes.string,
41 |
42 | /**
43 | * Set to true to give the top of the drawer rounded edges.
44 | */
45 | roundedEdges: PropTypes.bool,
46 |
47 | /**
48 | * Set to true to give the drawer a shadow.
49 | */
50 | shadow: PropTypes.bool,
51 |
52 | /**
53 | * A callback function triggered when the drawer swiped into up position
54 | */
55 | onExpanded: PropTypes.func,
56 |
57 | /**
58 | * A callback function triggered when the drawer swiped into down position
59 | */
60 | onCollapsed: PropTypes.func
61 | }
62 |
63 | static defaultProps = {
64 | offset: 0,
65 | startUp: true,
66 | backgroundColor: '#ffffff',
67 | roundedEdges: true,
68 | shadow: true,
69 | onExpanded: () => {},
70 | onCollapsed: () => {}
71 | }
72 |
73 | constructor(props){
74 | super(props);
75 |
76 | /**
77 | * TOGGLE_THRESHOLD is how much the user has to swipe the drawer
78 | * before its position changes between up / down.
79 | */
80 | this.TOGGLE_THRESHOLD = this.props.containerHeight / 11;
81 | this.DOWN_DISPLAY = this.props.downDisplay || this.props.containerHeight / 1.5;
82 |
83 | /**
84 | * UP_POSITION and DOWN_POSITION calculate the two (x,y) values for when
85 | * the drawer is swiped into up position and down position.
86 | */
87 | this.UP_POSITION = this._calculateUpPosition(SCREEN_HEIGHT, this.props.containerHeight, this.props.offset)
88 | this.DOWN_POSITION = this._calculateDownPosition(this.UP_POSITION, this.DOWN_DISPLAY)
89 |
90 | this.state = { currentPosition: this.props.startUp ? this.UP_POSITION : this.DOWN_POSITION };
91 | }
92 |
93 | render() {
94 | return (
95 | this.setCurrentPosition(position)}
98 | toggleThreshold = {this.TOGGLE_THRESHOLD}
99 | upPosition = {this.UP_POSITION}
100 | downPosition = {this.DOWN_POSITION}
101 | roundedEdges = {this.props.roundedEdges}
102 | shadow = {this.props.shadow}
103 | containerHeight = {this.props.containerHeight}
104 | backgroundColor = {this.props.backgroundColor}
105 | onExpanded = {() => this.props.onExpanded()}
106 | onCollapsed = {() => this.props.onCollapsed()}
107 | >
108 | {this.props.children}
109 |
110 |
111 |
112 | )
113 | }
114 |
115 | setCurrentPosition(position) {
116 | this.setState({ currentPosition: position })
117 | }
118 |
119 | _calculateUpPosition(screenHeight, containerHeight, offset) {
120 | return {
121 | x: 0,
122 | y: screenHeight - (containerHeight + offset)
123 | }
124 | }
125 |
126 | _calculateDownPosition(upPosition, downDisplay) {
127 | return {
128 | x: 0,
129 | y: upPosition.y + downDisplay
130 | };
131 | }
132 | }
--------------------------------------------------------------------------------