Skip to content
Draft
27 changes: 27 additions & 0 deletions apps/common-app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,17 @@ import LongPressExample from './src/simple/longPress';
import ManualExample from './src/simple/manual';
import SimpleFling from './src/simple/fling';

import Lock from './src/v3_api/lock/lock';
import V3Fling from './src/v3_api/fling/fling';
import LogicDetectorExample from './src/v3_api/svg/svg';
import V3Hover from './src/v3_api/hover/index';
import V3Overlap from './src/v3_api/overlap/index';
import V3Calculator from './src/v3_api/calculator/index';
import V3Velocity from './src/v3_api/velocity_test/index';
import V3BottomSheet from './src/v3_api/bottom_sheet/index';
import V3ChatHeads from './src/v3_api/chat_heads/index';
import V3HoverableIcons from './src/v3_api/hoverable_icons/index';
import V3Camera from './src/v3_api/camera/index';
import { Icon } from '@swmansion/icons';

interface Example {
Expand All @@ -98,6 +109,22 @@ const EXAMPLES: ExamplesSection[] = [
sectionTitle: 'Empty',
data: [{ name: 'Empty Example', component: EmptyExample }],
},
{
sectionTitle: 'V3 api',
data: [
{ name: 'V3 Fling', component: V3Fling },
{ name: 'Svg', component: LogicDetectorExample },
{ name: 'Lock', component: Lock },
{ name: 'V3 Hover', component: V3Hover },
{ name: 'V3 Overlap', component: V3Overlap },
{ name: 'V3 Calculator', component: V3Calculator },
{ name: 'V3 Velocity Test', component: V3Velocity },
{ name: 'V3 Bottom Sheet', component: V3BottomSheet },
{ name: 'V3 Chat Heads', component: V3ChatHeads },
{ name: 'V3 Hoverable Icons', component: V3HoverableIcons },
{ name: 'V3 Camera', component: V3Camera },
],
},
{
sectionTitle: 'New api',
data: [
Expand Down
162 changes: 162 additions & 0 deletions apps/common-app/src/v3_api/bottom_sheet/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import React, { useState } from 'react';
import {
Dimensions,
NativeScrollEvent,
NativeSyntheticEvent,
StyleSheet,
View,
} from 'react-native';
import {
NativeDetector,
PanGestureEvent,
useNative,
usePan,
useSimultaneous,
useTap,
} from 'react-native-gesture-handler';
import Animated, {
runOnJS,
useAnimatedStyle,
useSharedValue,
withSpring,
} from 'react-native-reanimated';
import { LoremIpsum } from '../../common';

const HEADER_HEIGTH = 50;
const windowHeight = Dimensions.get('window').height;
const SNAP_POINTS_FROM_TOP = [50, windowHeight * 0.4, windowHeight * 0.8];

const FULLY_OPEN_SNAP_POINT = SNAP_POINTS_FROM_TOP[0];
const CLOSED_SNAP_POINT = SNAP_POINTS_FROM_TOP[SNAP_POINTS_FROM_TOP.length - 1];

function Example() {
const [snapPoint, setSnapPoint] = useState(CLOSED_SNAP_POINT);
const translationY = useSharedValue(0);
const scrollOffset = useSharedValue(0);
const bottomSheetTranslateY = useSharedValue(CLOSED_SNAP_POINT);

const onHandlerEndOnJS = (point: number) => {
setSnapPoint(point);
};
const onHandlerEnd = (e: PanGestureEvent) => {
'worklet';
const dragToss = 0.01;
const endOffsetY =
bottomSheetTranslateY.value +
translationY.value +
e.handlerData.velocityY * dragToss;

// calculate nearest snap point
let destSnapPoint = FULLY_OPEN_SNAP_POINT;

if (
snapPoint === FULLY_OPEN_SNAP_POINT &&
endOffsetY < FULLY_OPEN_SNAP_POINT
) {
return;
}
for (const snapPoint of SNAP_POINTS_FROM_TOP) {
const distFromSnap = Math.abs(snapPoint - endOffsetY);
if (distFromSnap < Math.abs(destSnapPoint - endOffsetY)) {
destSnapPoint = snapPoint;
}
}

// update current translation to be able to animate withSpring to snapPoint
bottomSheetTranslateY.value =
bottomSheetTranslateY.value + translationY.value;
translationY.value = 0;

bottomSheetTranslateY.value = withSpring(destSnapPoint, {
mass: 0.5,
});
runOnJS(onHandlerEndOnJS)(destSnapPoint);
};
const panGesture = usePan({
onUpdate: (e) => {
'worklet';
// when bottom sheet is not fully opened scroll offset should not influence
// its position (prevents random snapping when opening bottom sheet when
// the content is already scrolled)
if (snapPoint === FULLY_OPEN_SNAP_POINT) {
translationY.value = e.handlerData.translationY - scrollOffset.value;
} else {
translationY.value = e.handlerData.translationY;
}
},
onEnd: onHandlerEnd,
});

const blockScrollUntilAtTheTop = useTap({
maxDeltaY: snapPoint - FULLY_OPEN_SNAP_POINT,
maxDuration: 100000,
simultaneousWithExternalGesture: panGesture,
});

const headerGesture = usePan({
onUpdate: (e) => {
'worklet';
translationY.value = e.handlerData.translationY;
},
onEnd: onHandlerEnd,
});

const scrollViewGesture = useNative({
requireExternalGestureToFail: blockScrollUntilAtTheTop,
});

const bottomSheetAnimatedStyle = useAnimatedStyle(() => {
const translateY = bottomSheetTranslateY.value + translationY.value;

const minTranslateY = Math.max(FULLY_OPEN_SNAP_POINT, translateY);
const clampedTranslateY = Math.min(CLOSED_SNAP_POINT, minTranslateY);
return {
transform: [{ translateY: clampedTranslateY }],
};
});

const simultanousGesture = useSimultaneous(panGesture, scrollViewGesture);

return (
<View style={styles.container}>
<LoremIpsum words={200} />
<NativeDetector gesture={blockScrollUntilAtTheTop}>
<Animated.View style={[styles.bottomSheet, bottomSheetAnimatedStyle]}>
<NativeDetector gesture={headerGesture}>
<View style={styles.header} />
</NativeDetector>
<NativeDetector gesture={simultanousGesture}>
<Animated.ScrollView
bounces={false}
scrollEventThrottle={1}
onScrollBeginDrag={(
e: NativeSyntheticEvent<NativeScrollEvent>
) => {
scrollOffset.value = e.nativeEvent.contentOffset.y;
}}>
<LoremIpsum />
<LoremIpsum />
<LoremIpsum />
</Animated.ScrollView>
</NativeDetector>
</Animated.View>
</NativeDetector>
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
},
header: {
height: HEADER_HEIGTH,
backgroundColor: 'coral',
},
bottomSheet: {
...StyleSheet.absoluteFillObject,
backgroundColor: '#ff9f7A',
},
});

export default Example;
Loading
Loading