import React, { Dispatch, SetStateAction, useRef, useState } from 'react'; import { StyleSheet, View, Text, Dimensions, LayoutChangeEvent, LayoutRectangle, } from 'react-native'; import { GestureDetector, Gesture, ScrollView, } from 'react-native-gesture-handler'; import Animated, { useSharedValue, useAnimatedStyle, withTiming, runOnJS, } from 'react-native-reanimated'; const DRAG_ANIMATION_DURATION = 300; const TAP_ANIMATION_DURATION = 100; const OPERATIONS_TOGGLE_OFFSET = 75; const OUTPUT_TOGGLE_OFFSET = 100; const window = Dimensions.get('window'); export default function CalculatorUI() { const outputOffset = useSharedValue(0); const [history, setHistory] = useState(Array()); const [expression, setExpression] = useState(''); function measure({ nativeEvent: { layout: { height }, }, }: LayoutChangeEvent) { outputOffset.value = -height; } return ( ); } interface OutputProps { offset: Animated.SharedValue; expression: string; history: string[]; } function Output({ offset, expression, history }: OutputProps) { const layout = useRef({}); const scrollView = useRef(); const drag = useSharedValue(0); const dragOffset = useSharedValue(0); const [opened, setOpened] = useState(false); function measure({ nativeEvent: { layout: newLayout } }: LayoutChangeEvent) { layout.current = newLayout; } function open() { drag.value = withTiming(-offset.value, { duration: DRAG_ANIMATION_DURATION, }); dragOffset.value = -offset.value; setOpened(true); } function close() { drag.value = withTiming(0, { duration: DRAG_ANIMATION_DURATION }); dragOffset.value = 0; setOpened(false); } const translationStyle = useAnimatedStyle(() => { return { transform: [{ translateY: offset.value + drag.value }], }; }); const dragGesture = Gesture.Pan() .onUpdate((e) => { 'worklet'; const translatedOffset = dragOffset.value + e.translationY; if (translatedOffset > -offset.value) { drag.value = -offset.value; } else if (translatedOffset < 0) { drag.value = 0; } else { drag.value = translatedOffset; } }) .onEnd((e) => { 'worklet'; const translatedOffset = dragOffset.value + e.translationY; if (opened) { if (translatedOffset < -offset.value - OUTPUT_TOGGLE_OFFSET) { runOnJS(close)(); } else { runOnJS(open)(); } } else { if (translatedOffset > OUTPUT_TOGGLE_OFFSET) { runOnJS(open)(); } else { runOnJS(close)(); } } }); scrollView.current?.scrollToEnd({ animated: true }); return ( { if (!opened) { ref?.scrollToEnd({ animated: false }); } scrollView.current = ref; }} enabled={opened} contentContainerStyle={{ flexGrow: 1 }}> {history.map((exp: string) => { return ; })} ); } interface ExpressionProps { expression: string; } function Expression({ expression }: ExpressionProps) { return ( {expression} {expression} ); } interface InputProps { setHistory: Dispatch>; setExpression: Dispatch>; measure: (e: LayoutChangeEvent) => void; offset: Animated.SharedValue; expression: string; } function Input({ setHistory, setExpression, measure, offset, expression, }: InputProps) { const translationStyle = useAnimatedStyle(() => { return { transform: [{ translateY: offset.value }], }; }); function append(symbol: string) { if (symbol === '<') { setHistory((h) => h.concat(expression)); setExpression((_e) => ''); } else { setExpression((e) => e + symbol); } } return ( ); } interface NumPadProps { append: (text: string) => void; } function NumPad({ append }: NumPadProps) { const buttons = ['7', '8', '9', '4', '5', '6', '1', '2', '3', '<', '0', '.']; return ( {buttons.map((text) => { return