Skip to content

Commit

Permalink
feat: BottomSheetScreen
Browse files Browse the repository at this point in the history
  • Loading branch information
mstrk committed Feb 12, 2021
1 parent f6d6220 commit 60ab774
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 0 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"react-native-svg": "12.1.0",
"react-native-vector-icons": "7.1.0",
"react-navigation-shared-element": "5.0.0-alpha1",
"reanimated-bottom-sheet": "1.0.0-alpha.22",
"semver": "7.3.2",
"storeon": "3.1.1"
},
Expand Down
13 changes: 13 additions & 0 deletions src/routes/MainNavigator/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { BottomNavigator } from 'routes/BottomNavigator'
import { AppTitleContainer } from 'components/AppTitleContainer'
import { GiveBeerListScreen } from 'screens/GiveBeerList'
import { GiveBeerScreen } from 'screens/GiveBeer'
import { BottomSheetScreen } from 'screens/BottomSheet'

import styles from './styles'
import { MainNavigatorParamsList } from './types'
Expand Down Expand Up @@ -73,6 +74,7 @@ export const MainNavigator = () => {
<Stack.Navigator
initialRouteName='Home'
headerMode='screen'
mode='modal'
screenOptions={{ header: renderHeader }}
>
<Stack.Screen
Expand Down Expand Up @@ -106,6 +108,17 @@ export const MainNavigator = () => {
return [`item.${id}.avatar`]
}}
/>

<Stack.Screen
name='BottomSheet'
component={BottomSheetScreen}
options={{
header: () => null,
animationEnabled: false,
gestureEnabled: false,
cardStyle: { backgroundColor: 'transparent' },
}}
/>
</Stack.Navigator>
)
}
7 changes: 7 additions & 0 deletions src/routes/MainNavigator/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,11 @@ export type MainNavigatorParamsList = {
Home: undefined,
GiveBeerList: undefined,
GiveBeer: User,
BottomSheet: {
size?: React.ReactText,
middleSnapPoints?: React.ReactText[],
onClose?: () => void,
renderContent: (close: () => void) => React.ReactNode,
renderHandle?: () => React.ReactNode,
},
}
105 changes: 105 additions & 0 deletions src/screens/BottomSheet/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import React, { useCallback, useEffect, useRef } from 'react'
import { StyleSheet, View, TouchableWithoutFeedback, BackHandler, Keyboard } from 'react-native'
import Animated, { Value } from 'react-native-reanimated'
import BottomSheet from 'reanimated-bottom-sheet'
import { useSafeArea } from 'react-native-safe-area-context'
import { useFocusEffect } from '@react-navigation/native'
import { useTheme } from 'react-native-paper'
import { AppTheme } from 'constants/theme'

import styles from './styles'
import { BottomSheetScreenProps } from './types'

export const BottomSheetScreen: React.FC<BottomSheetScreenProps> = ({ navigation, route }) => {
const { size = 300, middleSnapPoints = [], onClose, renderContent, renderHandle } = route.params
const allPoints = [size, ...middleSnapPoints, 0]
const safeArea = useSafeArea()
const { colors } = useTheme() as AppTheme
const snapPoints = useRef(
allPoints.map(
(point, index) => index === allPoints.length - 2 && typeof point === 'number' ? point + safeArea.bottom : point,
),
)
const bsRef = useRef<BottomSheet>(null)
const overlayOpacity = useRef(new Value(0))

// Animate on open
useEffect(() => {
if (bsRef.current) {
bsRef.current.snapTo(snapPoints.current.length - 2)
}
}, [bsRef, snapPoints])

// Animate on close - onCloseEnd will be called after
const onOverlayPress = useCallback(() => {
if (bsRef.current) {
bsRef.current.snapTo(snapPoints.current.length - 1)
}
}, [bsRef, snapPoints])

// pop position in the navigation stack so the screen behind become interactable
const handleOnCloseEnd = useCallback(() => {
onClose && onClose()
navigation.goBack()
}, [navigation, onClose])

// Dismiss keyboard & andle backpress on android
useFocusEffect(
useCallback(() => {
Keyboard.dismiss()

const onBackPress = () => {
onOverlayPress()

return true
}

BackHandler.addEventListener('hardwareBackPress', onBackPress)

return () => BackHandler.removeEventListener('hardwareBackPress', onBackPress)
}, [onOverlayPress]),
)

// header and handle on the top of the sheet
const renderHeader = () => (
<View style={[styles.header, { backgroundColor: colors.surface }]}>
<View style={styles.panelHeader}>
{renderHandle ? renderHandle() : <View style={styles.panelHandle} />}
</View>
</View>
)

// sheet dynamic content render
const renderBSContent = () => (
<View style={[styles.content, { paddingBottom: safeArea.bottom, backgroundColor: colors.surface }]}>
{renderContent(onOverlayPress)}
</View>
)

// sheet overlay animation
const overlayStyles = {
backgroundColor: '#000000',
opacity: overlayOpacity.current.interpolate({ inputRange: [0, 1], outputRange: [0.5, 0] }),
}

return (
<View style={StyleSheet.absoluteFill}>
<TouchableWithoutFeedback onPress={onOverlayPress}>
<Animated.View
style={[StyleSheet.absoluteFill, overlayStyles]}
/>
</TouchableWithoutFeedback>

<BottomSheet
ref={bsRef}
callbackNode={overlayOpacity.current}
snapPoints={snapPoints.current}
initialSnap={snapPoints.current.length - 1}
renderContent={renderBSContent}
renderHeader={renderHeader}
onCloseEnd={handleOnCloseEnd}
enabledContentGestureInteraction={false}
/>
</View>
)
}
25 changes: 25 additions & 0 deletions src/screens/BottomSheet/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { StyleSheet } from 'react-native'

export default StyleSheet.create({
header: {
shadowColor: '#000000',
paddingTop: 20,
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
},
panelHeader: {
alignItems: 'center',
},
panelHandle: {
width: 40,
height: 8,
borderRadius: 4,
backgroundColor: '#00000040',
marginBottom: 10,
},
content: {
height: '100%',
justifyContent: 'center',
alignItems: 'center',
},
})
9 changes: 9 additions & 0 deletions src/screens/BottomSheet/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

import { StackNavigationProp } from '@react-navigation/stack'
import { MainNavigatorParamsList } from 'routes/MainNavigator/types'
import { RouteProp } from '@react-navigation/native'

export interface BottomSheetScreenProps {
navigation: StackNavigationProp<MainNavigatorParamsList, 'BottomSheet'>,
route: RouteProp<MainNavigatorParamsList, 'BottomSheet'>,
}
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6955,6 +6955,11 @@ readable-stream@^2.0.1, readable-stream@^2.2.2, readable-stream@~2.3.6:
string_decoder "~1.1.1"
util-deprecate "~1.0.1"

reanimated-bottom-sheet@1.0.0-alpha.22:
version "1.0.0-alpha.22"
resolved "https://registry.yarnpkg.com/reanimated-bottom-sheet/-/reanimated-bottom-sheet-1.0.0-alpha.22.tgz#01a200946f1a461f01f1e773e5b4961c2df2e53b"
integrity sha512-NxecCn+2iA4YzkFuRK5/b86GHHS2OhZ9VRgiM4q18AC20YE/psRilqxzXCKBEvkOjP5AaAvY0yfE7EkEFBjTvw==

regenerate-unicode-properties@^8.2.0:
version "8.2.0"
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec"
Expand Down

0 comments on commit 60ab774

Please sign in to comment.