From 6d3729406bb75a6016b25c7302f9c6ace67613d9 Mon Sep 17 00:00:00 2001 From: Nemo Date: Tue, 24 Mar 2026 14:15:02 +0800 Subject: [PATCH] fix: rewrite SlideToConfirm with Reanimated only (no RNGH); remove GestureHandlerRootView --- app/_layout.tsx | 9 +- components/SlideToConfirm.tsx | 197 ++++++++++++++++++---------------- 2 files changed, 107 insertions(+), 99 deletions(-) diff --git a/app/_layout.tsx b/app/_layout.tsx index 943ccc6..5e5fd36 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -2,7 +2,6 @@ import '../global.css'; import { useEffect } from 'react'; import { Stack, router } from 'expo-router'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { useAuthStore } from '../stores/authStore'; const queryClient = new QueryClient(); @@ -25,10 +24,8 @@ export default function RootLayout() { }, [isLoading, token]); return ( - - - - - + + + ); } diff --git a/components/SlideToConfirm.tsx b/components/SlideToConfirm.tsx index 4b6d3ab..961f910 100644 --- a/components/SlideToConfirm.tsx +++ b/components/SlideToConfirm.tsx @@ -1,123 +1,134 @@ -import { useCallback } from 'react'; +/** + * SlideToConfirm — No RNGH dependency. + * Uses Reanimated useSharedValue (JSI-direct, sets on UI thread immediately) + * + onTouchStart/Move/End on the handle for gesture capture. + */ +import { useRef } from 'react'; import { StyleSheet, Text, View, ViewStyle } from 'react-native'; -import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler'; -import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated'; +import Animated, { + runOnJS, + useAnimatedStyle, + useSharedValue, + withSpring, +} from 'react-native-reanimated'; interface Props { onConfirm: () => void; label?: string; - color?: string; - threshold?: number; disabled?: boolean; style?: ViewStyle; } -const TRACK_HEIGHT = 60; -const HANDLE_SIZE = 48; -const TRACK_PADDING = 5; +const HANDLE_SIZE = 56; +const THRESHOLD = 0.80; // 80% of track -export function SlideToConfirm({ - onConfirm, - label = 'Slide to confirm', - color = '#059669', - threshold = 0.82, - disabled = false, - style, -}: Props) { - const translateX = useSharedValue(0); - const maxX = useSharedValue(200); // updated on layout +export function SlideToConfirm({ onConfirm, label = 'Slide to confirm', disabled = false, style }: Props) { + const trackWidth = useRef(0); + const startX = useRef(0); + const confirmed = useRef(false); + const translateX = useSharedValue(0); - const triggerConfirm = useCallback(() => { onConfirm(); }, [onConfirm]); - - const pan = Gesture.Pan() - .enabled(!disabled) - .minDistance(0) - .activeOffsetX([-5, 5]) - .onUpdate((e) => { - 'worklet'; - translateX.value = Math.max(0, Math.min(e.translationX, maxX.value)); - }) - .onEnd(() => { - 'worklet'; - if (translateX.value >= maxX.value * threshold) { - translateX.value = withSpring(maxX.value, { damping: 20, stiffness: 200, mass: 0.8 }); - runOnJS(triggerConfirm)(); - } else { - translateX.value = withSpring(0, { damping: 20, stiffness: 200, mass: 0.8 }); - } - }); - - const animatedHandle = useAnimatedStyle(() => ({ + const handleStyle = useAnimatedStyle(() => ({ transform: [{ translateX: translateX.value }], })); - const animatedFill = useAnimatedStyle(() => ({ - width: translateX.value + HANDLE_SIZE + TRACK_PADDING, - })); + const labelStyle = useAnimatedStyle(() => { + const maxX = Math.max(trackWidth.current - HANDLE_SIZE, 1); + const ratio = translateX.value / maxX; + return { opacity: 1 - ratio * 1.5 }; + }); - const animatedLabel = useAnimatedStyle(() => ({ - opacity: Math.max(0, 1 - (translateX.value / Math.max(maxX.value * 0.5, 1))), - })); + const onTouchStart = (e: any) => { + if (disabled) return; + confirmed.current = false; + startX.current = e.nativeEvent.pageX - translateX.value; + }; + + const onTouchMove = (e: any) => { + if (disabled) return; + const maxX = Math.max(trackWidth.current - HANDLE_SIZE, 1); + const raw = e.nativeEvent.pageX - startX.current; + const clamped = Math.min(Math.max(raw, 0), maxX); + translateX.value = clamped; + + if (!confirmed.current && clamped / maxX >= THRESHOLD) { + confirmed.current = true; + translateX.value = withSpring(maxX, { damping: 18, stiffness: 220 }); + runOnJS(onConfirm)(); + } + }; + + const onTouchEnd = () => { + if (disabled || confirmed.current) return; + translateX.value = withSpring(0, { damping: 18, stiffness: 220 }); + }; return ( - // GestureHandlerRootView inside Modal is REQUIRED on iOS New Arch - // Modal creates a new UIWindow — RNGH's gesture manager must be re-rooted here - - { - const w = e.nativeEvent.layout.width; - maxX.value = w - HANDLE_SIZE - TRACK_PADDING * 2; - }} + { trackWidth.current = e.nativeEvent.layout.width; }} + > + {/* Label fades as handle moves right */} + + {label} + + + {/* Sliding handle */} + - {/* Colored fill that grows behind the handle */} - - - {/* Label fades as handle moves right */} - - {label} - - - {/* Draggable handle */} - - - - - - - + + + ); } const styles = StyleSheet.create({ - root: { width: '100%' }, track: { - height: TRACK_HEIGHT, borderRadius: TRACK_HEIGHT / 2, - backgroundColor: '#F1F5F9', borderWidth: 1.5, - justifyContent: 'center', alignItems: 'center', - overflow: 'hidden', position: 'relative', + height: 60, + backgroundColor: '#0E7490', + borderRadius: 30, + justifyContent: 'center', + overflow: 'hidden', }, - fill: { - position: 'absolute', left: 0, top: 0, - height: TRACK_HEIGHT, zIndex: 1, + trackDisabled: { + backgroundColor: '#94A3B8', + }, + labelWrap: { + alignItems: 'center', + justifyContent: 'center', }, label: { - position: 'absolute', zIndex: 2, - fontSize: 14, fontWeight: '700', color: '#64748B', - textAlign: 'center', width: '100%', paddingRight: 16, + color: 'rgba(255,255,255,0.85)', + fontSize: 15, + fontWeight: '600', + letterSpacing: 0.3, }, handle: { - position: 'absolute', zIndex: 3, - justifyContent: 'center', alignItems: 'center', - shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.18, shadowRadius: 4, elevation: 4, + width: HANDLE_SIZE, + height: HANDLE_SIZE, + borderRadius: HANDLE_SIZE / 2, + backgroundColor: '#fff', + alignItems: 'center', + justifyContent: 'center', + marginLeft: 3, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.18, + shadowRadius: 4, + elevation: 4, + }, + handleDisabled: { + backgroundColor: '#E2E8F0', + }, + arrow: { + fontSize: 28, + color: '#0891B2', + fontWeight: '800', + lineHeight: 32, }, - arrow: { fontSize: 26, color: '#fff', fontWeight: '800', marginLeft: 2 }, });