diff --git a/native/components/full-screen-view-modal.react.js b/native/components/full-screen-view-modal.react.js --- a/native/components/full-screen-view-modal.react.js +++ b/native/components/full-screen-view-modal.react.js @@ -28,6 +28,7 @@ useSharedValue, withTiming, Easing, + withDecay, } from 'react-native-reanimated'; import type { EventResult } from 'react-native-reanimated'; import { SafeAreaView } from 'react-native-safe-area-context'; @@ -67,23 +68,17 @@ Extrapolate, block, set, - call, cond, not, and, or, eq, neq, - greaterThan, add, sub, multiply, divide, - pow, max, - min, - round, - abs, interpolateNode, startClock, stopClock, @@ -123,6 +118,8 @@ easing: Easing.out(Easing.ease), }; +const decayConfig = { deceleration: 0.99 }; + type TouchableOpacityInstance = React.AbstractComponent< React.ElementConfig, NativeMethods, @@ -161,6 +158,7 @@ +mediaIconsRef: { current: ?React.ElementRef }, +onCloseButtonLayout: () => void, +onMediaIconsLayout: () => void, + +close: () => void, }; class FullScreenViewModal extends React.PureComponent { @@ -344,7 +342,6 @@ const dismissingFromPan = new Value(0); - const roundedCurScale = divide(round(multiply(curScale, 1000)), 1000); const gestureActive = or(pinchActive, panActive); const activeInteraction = or( gestureActive, @@ -353,17 +350,6 @@ ); const updates = [ - this.backdropOpacityUpdate( - panJustEnded, - pinchActive, - panVelocityX, - panVelocityY, - roundedCurScale, - curX, - curY, - curBackdropOpacity, - dismissingFromPan, - ), this.recenter( resetXClock, resetYClock, @@ -454,57 +440,6 @@ return max(vertPop, 0); } - backdropOpacityUpdate( - // Inputs - panJustEnded: Node, - pinchActive: Node, - panVelocityX: Node, - panVelocityY: Node, - roundedCurScale: Node, - // Outputs - curX: Value, - curY: Value, - curBackdropOpacity: Value, - dismissingFromPan: Value, - ): Node { - const progressiveOpacity = max( - min( - sub(1, abs(divide(curX, this.frameWidth))), - sub(1, abs(divide(curY, this.frameHeight))), - ), - 0, - ); - - const resetClock = new Clock(); - - const velocity = pow(add(pow(panVelocityX, 2), pow(panVelocityY, 2)), 0.5); - const shouldGoBack = and( - panJustEnded, - or(greaterThan(velocity, 50), greaterThan(0.7, progressiveOpacity)), - ); - - const decayClock = new Clock(); - const decayItems = [ - set(curX, runDecay(decayClock, panVelocityX, curX, false)), - set(curY, runDecay(decayClock, panVelocityY, curY)), - ]; - - return cond( - [panJustEnded, dismissingFromPan], - decayItems, - cond( - or(pinchActive, greaterThan(roundedCurScale, 1)), - set(curBackdropOpacity, runTiming(resetClock, curBackdropOpacity, 1)), - [ - stopClock(resetClock), - set(curBackdropOpacity, progressiveOpacity), - set(dismissingFromPan, shouldGoBack), - cond(shouldGoBack, [decayItems, call([], this.close)]), - ], - ), - ); - } - recenter( // Inputs resetXClock: Clock, @@ -768,7 +703,7 @@ style={[styles.closeButtonContainer, closeButtonStyle]} > {view} ); } - - close = () => { - this.props.navigation.goBackOnce(); - }; } const styles = StyleSheet.create({ @@ -887,6 +818,10 @@ }; }, []); + const close = React.useCallback(() => { + props.navigation.goBackOnce(); + }, [props.navigation]); + const [closeButtonEnabled, setCloseButtonEnabled] = React.useState(true); const [actionLinksEnabled, setActionLinksEnabled] = React.useState(true); @@ -1020,11 +955,13 @@ ); const lastPinchScale = useSharedValue(1); + const pinchActive = useSharedValue(false); const pinchStart = React.useCallback(() => { 'worklet'; lastPinchScale.value = 1; - }, [lastPinchScale]); + pinchActive.value = true; + }, [lastPinchScale, pinchActive]); const pinchUpdate = React.useCallback( ({ scale, focalX, focalY }: PinchGestureEvent) => { @@ -1044,6 +981,11 @@ [centerX, centerY, curScale, curX, curY, lastPinchScale], ); + const pinchEnd = React.useCallback(() => { + 'worklet'; + pinchActive.value = false; + }, [pinchActive]); + const panActive = useSharedValue(false); const lastPanTranslationX = useSharedValue(0); @@ -1081,10 +1023,44 @@ [curX, curY, lastPanTranslationX, lastPanTranslationY, panActive], ); - const panEnd = React.useCallback(() => { - 'worklet'; - panActive.value = false; - }, [panActive]); + const progressiveOpacity = useDerivedValue(() => { + return Math.max( + Math.min( + 1 - Math.abs(curX.value / frameWidth.value), + 1 - Math.abs(curY.value / frameHeight.value), + ), + 0, + ); + }); + + const panEnd = React.useCallback( + ({ velocityX, velocityY }: PanGestureEvent) => { + 'worklet'; + if (!panActive.value) { + return; + } + panActive.value = false; + const velocity = Math.pow( + Math.pow(velocityX, 2) + Math.pow(velocityY, 2), + 0.5, + ); + const shouldGoBack = velocity > 50 || 0.7 > progressiveOpacity.value; + if (shouldGoBack && !pinchActive.value && roundedCurScale.value <= 1) { + curX.value = withDecay({ velocity: velocityX, ...decayConfig }); + curY.value = withDecay({ velocity: velocityY, ...decayConfig }); + runOnJS(close)(); + } + }, + [ + close, + curX, + curY, + panActive, + progressiveOpacity, + pinchActive, + roundedCurScale, + ], + ); const curCloseButtonOpacity = useSharedValue(1); const curActionLinksOpacity = useSharedValue(1); @@ -1211,10 +1187,32 @@ ], ); + const backdropReset = useSharedValue(1); + + useAnimatedReaction( + () => pinchActive.value || roundedCurScale.value > 1, + (isReset, wasReset) => { + if (isReset && !wasReset) { + backdropReset.value = progressiveOpacity.value; + backdropReset.value = withTiming(1, defaultTimingConfig); + } + }, + ); + + // TODO: use it later + // eslint-disable-next-line no-unused-vars + const curBackdropOpacity = useDerivedValue(() => { + if (pinchActive.value || roundedCurScale.value > 1) { + return backdropReset.value; + } + return progressiveOpacity.value; + }); + const gesture = React.useMemo(() => { const pinchGesture = Gesture.Pinch() .onStart(pinchStart) - .onUpdate(pinchUpdate); + .onUpdate(pinchUpdate) + .onEnd(pinchEnd); const panGesture = Gesture.Pan() .averageTouches(true) .onStart(panStart) @@ -1238,6 +1236,7 @@ panStart, panUpdate, pinchStart, + pinchEnd, pinchUpdate, singleTapUpdate, ]); @@ -1255,6 +1254,7 @@ mediaIconsRef={mediaIconsRef} onCloseButtonLayout={onCloseButtonLayout} onMediaIconsLayout={onMediaIconsLayout} + close={close} /> ); }); diff --git a/native/flow-typed/npm/react-native-reanimated_v2.x.x.js b/native/flow-typed/npm/react-native-reanimated_v2.x.x.js --- a/native/flow-typed/npm/react-native-reanimated_v2.x.x.js +++ b/native/flow-typed/npm/react-native-reanimated_v2.x.x.js @@ -594,6 +594,20 @@ delayedAnimation: T, ) => T; + declare type WithDecayConfig = {| + +deceleration?: number, + +velocity?: number, + +clamp?: [number, number], + +velocityFactor?: number, + +rubberBandEffect?: boolean, + +rubberBandFactor?: number, + |}; + + declare type WithDecay = ( + userConfig: WithDecayConfig, + callback?: AnimationCallback, + ) => number; + declare type RunOnJS = (func: F) => F; declare type CancelAnimation = (animation: SharedValue) => void; @@ -665,6 +679,7 @@ declare export var withSpring: WithSpring; declare export var withTiming: WithTiming; declare export var withDelay: WithDelay; + declare export var withDecay: WithDecay; declare export var runOnJS: RunOnJS; declare export var cancelAnimation: CancelAnimation; declare export var useAnimatedKeyboard: UseAnimatedKeyboard;