nexo/apps/mattermost/app/hooks/screen_transition_animation.ts

65 lines
2.7 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {useFocusEffect} from 'expo-router';
import {useCallback, useEffect, useRef} from 'react';
import {AppState, Platform} from 'react-native';
import {cancelAnimation, useReducedMotion, useSharedValue, useAnimatedStyle, withTiming} from 'react-native-reanimated';
import {useWindowDimensions} from './device';
/**
* Expo Router version - Uses focus/blur events instead of mount/unmount
* This handles the case where screens stay mounted when pushed/popped
*/
export const useScreenTransitionAnimation = (animated: boolean = true) => {
const {width} = useWindowDimensions();
const reducedMotion = useReducedMotion();
const shouldAnimate = animated && !reducedMotion;
const translateX = useSharedValue(shouldAnimate ? width : 0);
const animationDuration = Platform.OS === 'android' ? 250 : 350;
// Keep a ref so the useFocusEffect cleanup always reads the latest values
// without adding them as dependencies (which would re-trigger cleanup on rotation/motion changes)
const latestRef = useRef({width, shouldAnimate});
useEffect(() => {
latestRef.current = {width, shouldAnimate};
}, [width, shouldAnimate]);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{translateX: translateX.value}],
}), []);
useFocusEffect(
useCallback(() => {
const {shouldAnimate: sa} = latestRef.current;
translateX.value = sa ? withTiming(0, {duration: animationDuration}) : 0;
const sub = AppState.addEventListener('change', (state) => {
if (state === 'inactive' || state === 'background') {
// Freeze the animation at its current value so the UI thread doesn't
// race to completion while the app is backgrounded.
cancelAnimation(translateX);
} else if (state === 'active') {
cancelAnimation(translateX);
translateX.value = latestRef.current.width;
translateX.value = latestRef.current.shouldAnimate ? withTiming(0, {duration: animationDuration}) : 0;
}
});
return () => {
sub.remove();
const {width: w, shouldAnimate: saBlur} = latestRef.current;
translateX.value = saBlur ? withTiming(-w, {duration: animationDuration}) : 0;
};
}, [translateX, animationDuration]),
);
useEffect(() => {
if (!shouldAnimate) {
translateX.value = 0;
}
}, [translateX, shouldAnimate]);
return animatedStyle;
};