Created
July 17, 2020 18:40
-
-
Save vvsevolodovich/4de13f1628788333f25a894f6911547b to your computer and use it in GitHub Desktop.
BottomSheet reference implementation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// @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