Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions apps/common-app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,18 @@ 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 V3NestedText from './src/v3_api/nested_text/nested_text';
import { Icon } from '@swmansion/icons';

interface Example {
Expand All @@ -93,6 +105,23 @@ 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 },
{ name: 'V3 Nested Text', component: V3NestedText },
],
},
{
sectionTitle: 'New api',
data: [
Expand Down
88 changes: 87 additions & 1 deletion apps/common-app/src/common.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import React from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { Text, StyleSheet, ViewStyle, StyleProp } from 'react-native';
import Animated, {
Easing,
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated';

const styles = StyleSheet.create({
lipsum: {
padding: 10,
},
feedback: {
marginTop: 20,
fontSize: 16,
fontWeight: '600',
},
});

type Props = {
Expand All @@ -25,9 +36,84 @@ export class LoremIpsum extends React.Component<Props> {
}
}

type FeedbackProps = {
text: string;
highlight: string;
color?: string;
resetState?: () => void;
duration?: number;
};

// this piece of code is certainly not beutiful, but functional. It enables simple testing without the console
export function Feedback({
text,
highlight,
color = 'black',
resetState,
duration = 1000,
}: FeedbackProps) {
const opacity = useSharedValue(0);
const [activeHighlight, setActiveHighlight] = useState<string>(highlight);
const [activeText, setActiveText] = useState<string>(text);
const [activeColor, setActiveColor] = useState<string>(color);
const timerRef = useRef<number>(null);
useEffect(() => {
if (highlight === '') {
return;
}
if (timerRef.current) {
clearTimeout(timerRef.current);
}
setActiveHighlight(highlight);
setActiveText(text);
setActiveColor(color);
opacity.value = 1;
if (resetState) {
resetState();
}
timerRef.current = setTimeout(() => {
opacity.value = withTiming(0, {
duration: 500,
easing: Easing.out(Easing.ease),
});
}, duration);
}, [text, highlight, opacity, duration]);

const animatedStyle = useAnimatedStyle(() => ({
opacity: opacity.value,
}));
if (!activeHighlight || !activeText.includes(activeHighlight)) {
return (
<Animated.Text style={[styles.feedback, animatedStyle]}>
{text}
</Animated.Text>
);
}

const parts = activeText.split(activeHighlight);

return (
<Animated.Text style={[styles.feedback, animatedStyle]}>
{parts.map((part, index) => (
<React.Fragment key={index}>
<Text>{part}</Text>
{index < parts.length - 1 && (
<Text style={{ color: activeColor }}>{activeHighlight}</Text>
)}
</React.Fragment>
))}
</Animated.Text>
);
}

export const COLORS = {
offWhite: '#f8f9ff',
headerSeparator: '#eef0ff',
NAVY: '#001A72',
KINDA_RED: '#FFB2AD',
YELLOW: '#FFF096',
KINDA_GREEN: '#C4E7DB',
KINDA_BLUE: '#A0D5EF',
};

const LOREM_IPSUM = `
Expand Down
163 changes: 163 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,163 @@
import React, { useState } from 'react';
import {
Dimensions,
NativeScrollEvent,
NativeSyntheticEvent,
StyleSheet,
View,
} from 'react-native';
import {
GestureDetector,
PanGestureEvent,
useSimultaneousGestures,
usePanGesture,
useTapGesture,
useNativeGesture,
} 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 onHandlerDeactivate = (e: PanGestureEvent) => {
'worklet';
const dragToss = 0.01;
const endOffsetY =
bottomSheetTranslateY.value + translationY.value + e.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 = usePanGesture({
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.translationY - scrollOffset.value;
} else {
translationY.value = e.translationY;
}
},
onDeactivate: onHandlerDeactivate,
});

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

const headerGesture = usePanGesture({
onUpdate: (e) => {
'worklet';
translationY.value = e.translationY;
},
onDeactivate: onHandlerDeactivate,
});

const scrollViewGesture = useNativeGesture({
requireToFail: 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 = useSimultaneousGestures(
panGesture,
scrollViewGesture
);

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

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

export default Example;
Loading