Skip to content

Instantly share code, notes, and snippets.

@vvsevolodovich
Created July 17, 2020 18:40
Show Gist options
  • Save vvsevolodovich/4de13f1628788333f25a894f6911547b to your computer and use it in GitHub Desktop.
Save vvsevolodovich/4de13f1628788333f25a894f6911547b to your computer and use it in GitHub Desktop.
BottomSheet reference implementation
// @flow
import * as React from 'react'
import { Animated, StyleSheet, Text, View, Dimensions, TouchableWithoutFeedback } from 'react-native'
import {
PanGestureHandler,
NativeViewGestureHandler,
State,
TapGestureHandler
} from 'react-native-gesture-handler'
import type { TextStyleProp } from 'react-native/Libraries/StyleSheet/StyleSheet'
import style from './style'
const { height: windowHeight } = Dimensions.get('window')
const DEFAULT_SNAP_POINTS_FROM_TOP = [50, windowHeight * 0.4, windowHeight * 0.8]
const DEFAULT_DISMISS_POINT_FROM_TOP = windowHeight * 0.9
type ContentComponentProps = {
dismiss: Function
}
export type BottomSheetProps = {
title?: ?string,
titleTextStyle?: TextStyleProp,
HeaderRight?: React.Node,
snapPointsFromTop: Array<number>,
dismissPointFromTop?: number,
ContentComponent: React.ComponentType<ContentComponentProps>
}
type Props = {
onDismiss: Function
} & BottomSheetProps
type BottomSheetState = {
lastSnap: number
}
export default class BottomSheet extends React.Component<Props, BottomSheetState> {
masterdrawer: React.Ref<*> = React.createRef()
drawer: React.Ref<*> = React.createRef()
drawerheader: React.Ref<*> = React.createRef()
scroll: React.Ref<*> = React.createRef()
_lastScrollYValue: number
_dragY: Animated.Value
_lastScrollY: Animated.Value
_translateYOffset: Animated.Value
// $FlowFixMe
_reverseLastScrollY: AnimatedMultiplication
// $FlowFixMe
_translateY: AnimatedAddition
_onRegisterLastScroll: AnimationEvent
_onGestureEvent: AnimationEvent
static defaultProps = {
snapPointsFromTop: DEFAULT_SNAP_POINTS_FROM_TOP,
dismissPointFromTop: DEFAULT_DISMISS_POINT_FROM_TOP
}
constructor(props: Props) {
super(props)
const { snapPointsFromTop } = props
const START = snapPointsFromTop[0]
const END = snapPointsFromTop[snapPointsFromTop.length - 1]
this.state = {
lastSnap: END,
}
this._lastScrollYValue = 0
this._lastScrollY = new Animated.Value(0)
this._onRegisterLastScroll = Animated.event(
[{ nativeEvent: { contentOffset: { y: this._lastScrollY } } }],
{ useNativeDriver: true }
)
this._lastScrollY.addListener(({ value }) => {
this._lastScrollYValue = value
})
this._dragY = new Animated.Value(0)
this._onGestureEvent = Animated.event(
[{ nativeEvent: { translationY: this._dragY } }],
{ useNativeDriver: true }
)
this._reverseLastScrollY = Animated.multiply(
new Animated.Value(-1),
this._lastScrollY
)
this._translateYOffset = new Animated.Value(END)
this._translateY = Animated.add(
this._translateYOffset,
Animated.add(this._dragY, this._reverseLastScrollY)
).interpolate({
inputRange: [START, END],
outputRange: [START, END],
extrapolateLeft: 'clamp'
})
}
_onHeaderHandlerStateChange = ({ nativeEvent }) => {
if (nativeEvent.oldState === State.BEGAN) {
this._lastScrollY.setValue(0)
}
this._onHandlerStateChange({ nativeEvent })
}
_onHandlerStateChange = ({ nativeEvent }) => {
const { snapPointsFromTop, onDismiss, dismissPointFromTop } = this.props
if (nativeEvent.oldState === State.ACTIVE) {
let { velocityY, translationY } = nativeEvent
translationY -= this._lastScrollYValue
const dragToss = 0.05
const endOffsetY =
this.state.lastSnap + translationY + dragToss * velocityY
if (typeof dismissPointFromTop === 'number' && endOffsetY > dismissPointFromTop) {
return onDismiss()
}
let destSnapPoint = snapPointsFromTop[0]
for (let i = 0; i < snapPointsFromTop.length; i++) {
const snapPoint = snapPointsFromTop[i]
const distFromSnap = Math.abs(snapPoint - endOffsetY)
if (distFromSnap < Math.abs(destSnapPoint - endOffsetY)) {
destSnapPoint = snapPoint
}
}
this.setState({ lastSnap: destSnapPoint })
this._translateYOffset.extractOffset()
this._translateYOffset.setValue(translationY)
this._translateYOffset.flattenOffset()
this._dragY.setValue(0)
Animated.spring(this._translateYOffset, {
velocity: velocityY,
tension: 68,
friction: 12,
toValue: destSnapPoint,
useNativeDriver: true,
}).start()
}
}
render() {
const {
title,
titleTextStyle,
HeaderRight,
ContentComponent,
snapPointsFromTop,
onDismiss
} = this.props
return (
<TapGestureHandler
maxDurationMs={100000}
ref={this.masterdrawer}
maxDeltaY={this.state.lastSnap - snapPointsFromTop[0]}>
<View style={StyleSheet.absoluteFillObject} pointerEvents="box-none">
<TouchableWithoutFeedback>
<Animated.View
style={[
StyleSheet.absoluteFillObject,
{
transform: [{ translateY: this._translateY }]
},
]}>
{typeof title === 'string' ? (
<PanGestureHandler
ref={this.drawerheader}
simultaneousHandlers={[this.scroll, this.masterdrawer]}
shouldCancelWhenOutside={false}
onGestureEvent={this._onGestureEvent}
onHandlerStateChange={this._onHeaderHandlerStateChange}>
<Animated.View style={style.header}>
<Text style={[style.titleText, titleTextStyle]}>
{title}
</Text>
{HeaderRight ? (
<View style={style.headerRight}>
{HeaderRight}
</View>
) : null}
</Animated.View>
</PanGestureHandler>
) : null}
<PanGestureHandler
ref={this.drawer}
simultaneousHandlers={[this.scroll, this.masterdrawer]}
shouldCancelWhenOutside={false}
onGestureEvent={this._onGestureEvent}
onHandlerStateChange={this._onHandlerStateChange}>
<Animated.View style={style.container}>
<NativeViewGestureHandler
ref={this.scroll}
waitFor={this.masterdrawer}
simultaneousHandlers={this.drawer}>
<Animated.ScrollView
style={[
style.scrollView,
{ marginBottom: snapPointsFromTop[0], backgroundColor: 'white' },
]}
bounces={false}
onScrollBeginDrag={this._onRegisterLastScroll}
scrollEventThrottle={1}>
<ContentComponent dismiss={onDismiss} />
</Animated.ScrollView>
</NativeViewGestureHandler>
</Animated.View>
</PanGestureHandler>
</Animated.View>
</TouchableWithoutFeedback>
</View>
</TapGestureHandler>
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment