Unistyles' StyleSheet.create((theme) => ...) wraps styled components in <UnistylesComponent> and patches native view properties via C++. When these dynamic styles are applied to Reanimated's Animated.View, a theme change causes both systems to fight over the same native node, crashing with "Unable to find node on an unmounted component." Fix: use plain React Native StyleSheet for static positioning on Animated.View, and pass theme-dependent values (backgroundColor) as inline styles from useUnistyles() instead of Unistyles dynamic sheets. Also: - Add gestureAnimatingRef to prevent double withTiming during gesture opens - Remove all debug instrumentation added during investigation - Add Maestro flow and bash script for automated theme-toggle verification - Add docs/MOBILE_TESTING.md covering Maestro patterns, self-verification loops, the Unistyles+Reanimated pitfall, and simulator commands
118 lines
3.4 KiB
TypeScript
118 lines
3.4 KiB
TypeScript
import { useCallback, useMemo } from "react";
|
|
import { Gesture } from "react-native-gesture-handler";
|
|
import { Extrapolation, interpolate, runOnJS, useSharedValue } from "react-native-reanimated";
|
|
import { useExplorerSidebarAnimation } from "@/contexts/explorer-sidebar-animation-context";
|
|
|
|
interface UseExplorerOpenGestureParams {
|
|
enabled: boolean;
|
|
onOpen: () => void;
|
|
}
|
|
|
|
export function useExplorerOpenGesture({ enabled, onOpen }: UseExplorerOpenGestureParams) {
|
|
const {
|
|
translateX,
|
|
backdropOpacity,
|
|
windowWidth,
|
|
animateToOpen,
|
|
animateToClose,
|
|
isGesturing,
|
|
gestureAnimatingRef,
|
|
openGestureRef,
|
|
} = useExplorerSidebarAnimation();
|
|
const touchStartX = useSharedValue(0);
|
|
const touchStartY = useSharedValue(0);
|
|
|
|
const handleGestureOpen = useCallback(() => {
|
|
gestureAnimatingRef.current = true;
|
|
onOpen();
|
|
}, [onOpen, gestureAnimatingRef]);
|
|
|
|
return useMemo(
|
|
() =>
|
|
Gesture.Pan()
|
|
.withRef(openGestureRef)
|
|
.enabled(enabled)
|
|
.manualActivation(true)
|
|
.onTouchesDown((event) => {
|
|
const touch = event.changedTouches[0];
|
|
if (!touch) {
|
|
return;
|
|
}
|
|
touchStartX.value = touch.absoluteX;
|
|
touchStartY.value = touch.absoluteY;
|
|
})
|
|
.onTouchesMove((event, stateManager) => {
|
|
const touch = event.changedTouches[0];
|
|
if (!touch || event.numberOfTouches !== 1) {
|
|
stateManager.fail();
|
|
return;
|
|
}
|
|
|
|
const deltaX = touch.absoluteX - touchStartX.value;
|
|
const deltaY = touch.absoluteY - touchStartY.value;
|
|
const absDeltaX = Math.abs(deltaX);
|
|
const absDeltaY = Math.abs(deltaY);
|
|
|
|
// Fail quickly on rightward or clearly vertical intent.
|
|
if (deltaX >= 10) {
|
|
stateManager.fail();
|
|
return;
|
|
}
|
|
if (absDeltaY > 10 && absDeltaY > absDeltaX) {
|
|
stateManager.fail();
|
|
return;
|
|
}
|
|
|
|
// Activate only on intentional leftward movement.
|
|
if (deltaX <= -15 && absDeltaX > absDeltaY) {
|
|
stateManager.activate();
|
|
}
|
|
})
|
|
.onStart(() => {
|
|
isGesturing.value = true;
|
|
})
|
|
.onUpdate((event) => {
|
|
// Right sidebar: start from closed position (+windowWidth) and move towards 0.
|
|
const newTranslateX = Math.max(
|
|
0,
|
|
Math.min(windowWidth, windowWidth + event.translationX),
|
|
);
|
|
translateX.value = newTranslateX;
|
|
backdropOpacity.value = interpolate(
|
|
newTranslateX,
|
|
[windowWidth, 0],
|
|
[0, 1],
|
|
Extrapolation.CLAMP,
|
|
);
|
|
})
|
|
.onEnd((event) => {
|
|
isGesturing.value = false;
|
|
const shouldOpenByPosition = translateX.value < (windowWidth * 2) / 3;
|
|
const shouldOpenByVelocity = event.velocityX < -500;
|
|
const shouldOpen = shouldOpenByPosition || shouldOpenByVelocity;
|
|
if (shouldOpen) {
|
|
animateToOpen();
|
|
runOnJS(handleGestureOpen)();
|
|
} else {
|
|
animateToClose();
|
|
}
|
|
})
|
|
.onFinalize(() => {
|
|
isGesturing.value = false;
|
|
}),
|
|
[
|
|
enabled,
|
|
windowWidth,
|
|
translateX,
|
|
backdropOpacity,
|
|
animateToOpen,
|
|
animateToClose,
|
|
isGesturing,
|
|
openGestureRef,
|
|
handleGestureOpen,
|
|
touchStartX,
|
|
touchStartY,
|
|
],
|
|
);
|
|
}
|