|
| 1 | +import React, { forwardRef, useRef, type Ref, useImperativeHandle } from 'react' |
| 2 | +import { type ViewStyle, useWindowDimensions } from 'react-native' |
| 3 | +import { TrueSheet, type TrueSheetProps } from '@lodev09/react-native-true-sheet' |
| 4 | +import Animated, { useAnimatedStyle, useSharedValue, withDecay } from 'react-native-reanimated' |
| 5 | +import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler' |
| 6 | + |
| 7 | +import { DARK, DARK_GRAY, GRABBER_COLOR, SPACING, times } from '../utils' |
| 8 | +import { Button, DemoContent, Footer } from '../components' |
| 9 | + |
| 10 | +const BOXES_COUNT = 20 |
| 11 | +const CONTAINER_HEIGHT = 200 |
| 12 | +const BOX_GAP = SPACING |
| 13 | +const BOX_SIZE = CONTAINER_HEIGHT - SPACING * 2 |
| 14 | + |
| 15 | +interface GestureSheetProps extends TrueSheetProps {} |
| 16 | + |
| 17 | +export const GestureSheet = forwardRef((props: GestureSheetProps, ref: Ref<TrueSheet>) => { |
| 18 | + const sheetRef = useRef<TrueSheet>(null) |
| 19 | + |
| 20 | + const scrollX = useSharedValue(0) |
| 21 | + const dimensions = useWindowDimensions() |
| 22 | + |
| 23 | + const dismiss = async () => { |
| 24 | + await sheetRef.current?.dismiss() |
| 25 | + } |
| 26 | + |
| 27 | + const $animatedContainer: ViewStyle = useAnimatedStyle(() => ({ |
| 28 | + transform: [{ translateX: scrollX.value }], |
| 29 | + })) |
| 30 | + |
| 31 | + const pan = Gesture.Pan() |
| 32 | + .onChange((e) => { |
| 33 | + scrollX.value += e.changeX |
| 34 | + }) |
| 35 | + .onFinalize((e) => { |
| 36 | + scrollX.value = withDecay({ |
| 37 | + velocity: e.velocityX, |
| 38 | + rubberBandEffect: true, |
| 39 | + clamp: [-((BOX_SIZE + BOX_GAP) * BOXES_COUNT) + dimensions.width - SPACING, 0], |
| 40 | + }) |
| 41 | + }) |
| 42 | + .activeOffsetX([-10, 10]) |
| 43 | + |
| 44 | + useImperativeHandle<TrueSheet | null, TrueSheet | null>(ref, () => sheetRef.current) |
| 45 | + |
| 46 | + return ( |
| 47 | + <TrueSheet |
| 48 | + sizes={['auto']} |
| 49 | + ref={sheetRef} |
| 50 | + contentContainerStyle={$content} |
| 51 | + blurTint="dark" |
| 52 | + backgroundColor={DARK} |
| 53 | + cornerRadius={12} |
| 54 | + grabberProps={{ color: GRABBER_COLOR }} |
| 55 | + onDismiss={() => console.log('Gesture sheet dismissed!')} |
| 56 | + onPresent={({ index, value }) => |
| 57 | + console.log(`Gesture sheet presented with size of ${value} at index: ${index}`) |
| 58 | + } |
| 59 | + onSizeChange={({ index, value }) => console.log(`Resized to:`, value, 'at index:', index)} |
| 60 | + FooterComponent={<Footer />} |
| 61 | + {...props} |
| 62 | + > |
| 63 | + <GestureHandlerRootView> |
| 64 | + <GestureDetector gesture={pan}> |
| 65 | + <Animated.View style={[$panContainer, $animatedContainer]}> |
| 66 | + {times(BOXES_COUNT, (i) => ( |
| 67 | + <DemoContent key={i} text={String(i + 1)} style={$box} /> |
| 68 | + ))} |
| 69 | + </Animated.View> |
| 70 | + </GestureDetector> |
| 71 | + <Button text="Dismis" onPress={dismiss} /> |
| 72 | + </GestureHandlerRootView> |
| 73 | + </TrueSheet> |
| 74 | + ) |
| 75 | +}) |
| 76 | + |
| 77 | +GestureSheet.displayName = 'GestureSheet' |
| 78 | + |
| 79 | +const $content: ViewStyle = { |
| 80 | + padding: SPACING, |
| 81 | +} |
| 82 | + |
| 83 | +const $panContainer: ViewStyle = { |
| 84 | + height: CONTAINER_HEIGHT, |
| 85 | + flexDirection: 'row', |
| 86 | + paddingVertical: SPACING, |
| 87 | + marginBottom: SPACING, |
| 88 | + gap: BOX_GAP, |
| 89 | +} |
| 90 | + |
| 91 | +const $box: ViewStyle = { |
| 92 | + backgroundColor: DARK_GRAY, |
| 93 | + width: BOX_SIZE, |
| 94 | + height: BOX_SIZE, |
| 95 | + alignItems: 'center', |
| 96 | + justifyContent: 'center', |
| 97 | +} |
0 commit comments