BottomSheet with ReactNative and PanResponder. Receipt of duck soup!

Andrii Drozdov
6 min readApr 14, 2019

--

Prologue.

I know that animation on ReactNative scares a lot of developers, but that is not so scary after all. A few simple functions can help you create BottomSheet without any issues.

So, here’s what we are going to do:

Episode 1. The begining.

Start a new project :)

Use react-native init BottomSheet to generate a project from scratch.

Create BottomSheet folder under src folder with index.js, styles.js, pan-responder.js, constants.js files, just for better decomposition.

Our file structure:

 | - /BottomSheet
| — - /src
| — — - /BottomSheet
| - — — - /index.js
| - — — - /constants.js
| - — — - /pan-responder.js
| - — — - /styles.js

Episode 2. Mark-up.

There is no difference if we are going to make it with a stateful or stateless component since we will totally avoid re-rendering of our component. Please, do not use the component state to keep Animated.Value unless you’re going to replace the value with a new Animated.Value object.

Our index.js file:

import React from 'react'
import { Animated, View, SafeAreaView, Text } from 'react-native'

import { animatedPosition, panGesture } from './pan-responder'
import styles from './styles'

function BottomSheet () {
return (
<Animated.View style={[styles.container, { bottom: animatedPosition }]}>
<View style={styles.gestureArea} {...panGesture.panHandlers}>
<View style={styles.pullItem} />
</View>

<SafeAreaView style={styles.content}>
<View style={styles.container}>
<Text style={styles.text}>Your awesome content</Text>
</View>
</SafeAreaView>
</Animated.View>
)
}

export default BottomSheet

Let’s walk through the code above.

  1. Imports:
    * Animated is a basic wrapper for animation in ReactNative. It can often be used to animate appearance for example. But in this case, we just animate UI to respond to our gestures.
    * SafeAreaView have two usages here. First, I use this as a container. Second, avoid content overlapping with system multi-tasking control.
  2. Mark-up:
    * <Animated.View /> basic way to make animation in ReactNative.
    Very important! Changes of in animatedPosition variable do NOT call re-render (we are creating a new instance of the new Animated.Value() and manipulating value inside the instance).
    * Nested <View {...panGesture.panHandlers} />. This is the main place, where magic is happening. Just remember this construction, I will describe everything in Episode 4.

Episode 3. Styling.

In my code, I always try to follow the SST rule (Single Source of Truth). In this tutorial, I follow it too, to control sizes and position of the BottomSheet.

So, let’s start with our BottomSheet config through the constants, in our constants.js file:

export const ANIMATED = {
HIDDEN: -350,
FULL_OPEN: -100,
VISIBLE: -300
}

HIDDEN — is equal to the component height, the only difference it should be positive.

FULL_OPEN — visible part of the component will be only 250 when BottomSheet is fully open. We need this to do our component a little bit larger than the screen.

VISIBLE — this value is required to calculate the visible part of the BottomSheet when it’s closed.

Notice, if you want to manipulate sheet size, you can adjust these values as you want and they will be applied to styles and to animation handlers.

Our styles.js file:

import { StyleSheet } from 'react-native'

import { COLORS } from '../../styles'

import { DEVICE } from '../../constants'
import { ANIMATED } from './constants'

export default StyleSheet.create({
container: {
position: 'absolute',

left: 0,
width: DEVICE.width,
height: Math.abs(ANIMATED.HIDDEN),

marginBottom: ANIMATED.HIDDEN - ANIMATED.VISIBLE,
paddingBottom: Math.abs(ANIMATED.FULL_OPEN),

borderTopLeftRadius: 10,
borderTopRightRadius: 10,
borderWidth: 1,
borderColor: COLORS.SECONDARY,

backgroundColor: COLORS.WHITE,

shadowColor: COLORS.BLACK,
shadowOffset: {
width: 0,
height: 3
},
shadowOpacity: 0.27,
shadowRadius: 4.65,

elevation: 6
},

gestureArea: {
width: DEVICE.width,
height: 40,

marginTop: -10,
position: 'absolute',

justifyContent: 'center',
alignItems: 'center',
},
pullItem: {
width: 40,
height: 5,

borderRadius: 20,

backgroundColor: COLORS.SECONDARY
},

content: {
marginVertical: 30,
paddingHorizontal: 10,

height: '100%',
}
})

Here you can find a little Math operation (eg. Math.abs is converting a negative number to a positive).

Let’s walk through the code:

  1. marginBottom: ANIMATED.HIDDEN — ANIMATED.VISIBLE this value is equal to 50 is done to make our BottomSheet partly visible for our default state.
  2. paddingBottom: Math.abs(ANIMATED.FULL_OPEN) is equal to 100. The reason for this is to make padding for our behind-the-screen content that will be invisible.
  3. About DEVICE variable is just a regular ReactNative way to get device dimensions (Dimensions.get()), alternative to this solution is to work with percentage (eg. you can use 100%).
  4. COLORS variable is a way to use the SST rule and set colour in one place.

The red area is totally touchable. It’s done to do more user-friendly behaviour. Responsible for its styling — gestureArea in our styles.js.

Green area is where our content goes. You can see on the bottom (white area) — this is SafeAreaView help us to avoid the system draggable zone. Styles that are responsible for this are content in our styles.js file.

Episode 4. The magic.

Let’s start with our code in pan-responder.js:

import { Animated, PanResponder } from 'react-native'

import { DEVICE } from '../../constants'
import { ANIMATED } from './constants'

const STARTING_POSITION = ANIMATED.HIDDEN - ANIMATED.FULL_OPEN

const animatedPosition = new Animated.Value(STARTING_POSITION)

function animateMove (toValue) {
Animated.spring(animatedPosition, {
toValue,
tension: 120
}).start()
}

function movementValue (gestureState) {
return DEVICE.height - gestureState.moveY + ANIMATED.VISIBLE
}

function onMoveShouldSetPanResponder (_, gestureState) {
return gestureState.dy >= 10 || gestureState.dy <= -10
}

function onPanResponderMove (_, gestureState) {
const toValue = Math.min(0, movementValue(gestureState))

animateMove(toValue)
}

function onPanResponderRelease (_, gestureState) {
const isMovedMoreThenThird = movementValue(gestureState) < ANIMATED.HIDDEN / 3
const toValue = isMovedMoreThenThird ? STARTING_POSITION : 0

animateMove(toValue)
}

const panGesture = PanResponder.create({
onPanResponderMove,
onPanResponderRelease,
onMoveShouldSetPanResponder,
onStartShouldSetPanResponderCapture: onMoveShouldSetPanResponder
})

export {
animatedPosition,
panGesture
}

Let's go through the code:

  1. STARTING_POSITION — is equal to -250. This value is the starting position and our BottomSheet is partly hidden.
  2. animatedPosition — is the starting point to use our Animated API. We just need to initialize a new instance to use it with our <Animated.View /> component.
  3. animateMove — helper function. A way to keep our code DRY. Basically, it is how we animate our movement. Notice we use tension as animation behaviour. There are plenty of alternatives to this behaviour that allow us to simulate a little jumping (physic) of the BottomSheet.
  4. movementValue — helper function. Used to calculate how far the user moves his finger.
  5. onMoveShouldSetPanResponderimportant function. Define if we will pass handling to PanResponder or deeper into the component tree. For example, if you will have a button under PanResponder handlers it will intercept any presses and your button will NEVER be clicked, so we need to avoid PanResponder interception, and define what is press and what is swipe. Basically, this is a way how we understand gesture false move.
  6. onPanResponderMove — handler to animate on gesture move and forbid animation above BottomSheet maximum top position.
  7. onPanResponderRelease — this can be ignored in regular cases, but when we are doing BottomSheet we should use this to give a directive to our BottomSheet on how it should behave when we release our finger. Basically, it’s a calculation, if we move our BottomSheet more then 1/3 it should be hidden, or else it should be visible.

Epilogue

All of this is easy to accomplish, and fully customizable put anything that you like into the content of the BottomSheet.

Code examples you can find on GitHub repo.

There is an example for both, stateful and stateless, components.

Have a great time!

Follow me for more!

--

--

Andrii Drozdov
Andrii Drozdov

Written by Andrii Drozdov

CTO & developer, to whom every breath, is an injection of bits into CPU of my life

Responses (2)