diff --git a/src/components/TabSelector/TabSelector.tsx b/src/components/TabSelector/TabSelector.tsx index 3809460436e5..fcbdbaddad5d 100644 --- a/src/components/TabSelector/TabSelector.tsx +++ b/src/components/TabSelector/TabSelector.tsx @@ -1,13 +1,12 @@ import {MaterialTopTabNavigationHelpers} from '@react-navigation/material-top-tabs/lib/typescript/src/types'; import {TabNavigationState} from '@react-navigation/native'; -import React, {useCallback, useEffect, useMemo, useState} from 'react'; -import type {Animated} from 'react-native'; +import React, {useMemo} from 'react'; import {View} from 'react-native'; import * as Expensicons from '@components/Icon/Expensicons'; import {LocaleContextProps} from '@components/LocaleContextProvider'; import useLocalize from '@hooks/useLocalize'; -import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import tabNavigatorAnimationEnabled from '@libs/Navigation/tabNavigatorAnimationEnabled'; import {RootStackParamList} from '@libs/Navigation/types'; import CONST from '@src/CONST'; import IconAsset from '@src/types/utils/IconAsset'; @@ -22,9 +21,6 @@ type TabSelectorProps = { /* Callback fired when tab is pressed */ onTabPress?: (name: string) => void; - - /* AnimatedValue for the position of the screen while swiping */ - position: Animated.AnimatedInterpolation; }; type IconAndTitle = { @@ -49,66 +45,21 @@ function getIconAndTitle(route: string, translate: LocaleContextProps['translate } } -function getOpacity(position: Animated.AnimatedInterpolation, routesLength: number, tabIndex: number, active: boolean, affectedTabs: number[]) { - const activeValue = active ? 1 : 0; - const inactiveValue = active ? 0 : 1; - - if (routesLength > 1) { - const inputRange = Array.from({length: routesLength}, (v, i) => i); - - return position.interpolate({ - inputRange, - outputRange: inputRange.map((i) => (affectedTabs.includes(tabIndex) && i === tabIndex ? activeValue : inactiveValue)), - }); - } - return activeValue; -} - -function TabSelector({state, navigation, onTabPress = () => {}, position}: TabSelectorProps) { +function TabSelector({state, navigation, onTabPress}: TabSelectorProps) { const {translate} = useLocalize(); - const theme = useTheme(); const styles = useThemeStyles(); - const defaultAffectedAnimatedTabs = useMemo(() => Array.from({length: state.routes.length}, (v, i) => i), [state.routes.length]); - const [affectedAnimatedTabs, setAffectedAnimatedTabs] = useState(defaultAffectedAnimatedTabs); - const getBackgroundColor = useCallback( - (routesLength: number, tabIndex: number, affectedTabs: number[]) => { - if (routesLength > 1) { - const inputRange = Array.from({length: routesLength}, (v, i) => i); - - return position.interpolate({ - inputRange, - outputRange: inputRange.map((i) => (affectedTabs.includes(tabIndex) && i === tabIndex ? theme.border : theme.appBG)), - }); - } - return theme.border; - }, - [theme, position], - ); - - useEffect(() => { - // It is required to wait transition end to reset affectedAnimatedTabs because tabs style is still animating during transition. - setTimeout(() => { - setAffectedAnimatedTabs(defaultAffectedAnimatedTabs); - }, CONST.ANIMATED_TRANSITION); - }, [defaultAffectedAnimatedTabs, state.index]); - - return ( - - {state.routes.map((route, index) => { - const activeOpacity = getOpacity(position, state.routes.length, index, true, affectedAnimatedTabs); - const inactiveOpacity = getOpacity(position, state.routes.length, index, false, affectedAnimatedTabs); - const backgroundColor = getBackgroundColor(state.routes.length, index, affectedAnimatedTabs); - const isActive = index === state.index; + const tabs = useMemo( + () => + state.routes.map((route, index) => { + const isFocused = index === state.index; const {icon, title} = getIconAndTitle(route.name, translate); const onPress = () => { - if (isActive) { + if (isFocused) { return; } - setAffectedAnimatedTabs([state.index, index]); - const event = navigation.emit({ type: 'tabPress', target: route.key, @@ -120,7 +71,7 @@ function TabSelector({state, navigation, onTabPress = () => {}, position}: TabSe navigation.navigate({key: route.key, merge: true}); } - onTabPress(route.name); + onTabPress?.(route.name); }; return ( @@ -129,15 +80,15 @@ function TabSelector({state, navigation, onTabPress = () => {}, position}: TabSe icon={icon} title={title} onPress={onPress} - activeOpacity={activeOpacity} - inactiveOpacity={inactiveOpacity} - backgroundColor={backgroundColor} - isActive={isActive} + isFocused={isFocused} + animationEnabled={tabNavigatorAnimationEnabled} /> ); - })} - + }), + [navigation, onTabPress, state.index, state.routes, translate], ); + + return {tabs}; } TabSelector.displayName = 'TabSelector'; diff --git a/src/components/TabSelector/TabSelectorItem.tsx b/src/components/TabSelector/TabSelectorItem.tsx index a246e1a14b54..5e68d9b7e09c 100644 --- a/src/components/TabSelector/TabSelectorItem.tsx +++ b/src/components/TabSelector/TabSelectorItem.tsx @@ -1,7 +1,9 @@ -import React from 'react'; +import React, {useCallback, useEffect, useRef} from 'react'; import {Animated, StyleSheet} from 'react-native'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; +import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import CONST from '@src/CONST'; import IconAsset from '@src/types/utils/IconAsset'; import TabIcon from './TabIcon'; import TabLabel from './TabLabel'; @@ -16,21 +18,45 @@ type TabSelectorItemProps = { /** Title of the tab */ title?: string; - /** Animated background color value for the tab button */ - backgroundColor?: string | Animated.AnimatedInterpolation; - - /** Animated opacity value while the tab is in inactive state */ - inactiveOpacity?: number | Animated.AnimatedInterpolation; - - /** Animated opacity value while the tab is in active state */ - activeOpacity?: number | Animated.AnimatedInterpolation; - /** Whether this tab is active */ - isActive?: boolean; + isFocused?: boolean; + + /** Whether animations should be skipped */ + animationEnabled?: boolean; }; -function TabSelectorItem({icon, title = '', onPress = () => {}, backgroundColor = '', activeOpacity = 0, inactiveOpacity = 1, isActive = false}: TabSelectorItemProps) { +function TabSelectorItem({icon, title = '', onPress = () => {}, isFocused = false, animationEnabled = true}: TabSelectorItemProps) { + const focusValueRef = useRef(new Animated.Value(isFocused ? 1 : 0)); const styles = useThemeStyles(); + const theme = useTheme(); + + useEffect(() => { + const focusValue = isFocused ? 1 : 0; + + if (animationEnabled) { + return Animated.timing(focusValueRef.current, { + toValue: focusValue, + duration: CONST.ANIMATED_TRANSITION, + useNativeDriver: true, + }).start(); + } + + focusValueRef.current.setValue(focusValue); + }, [animationEnabled, isFocused]); + + const getBackgroundColorStyle = useCallback( + (hovered: boolean) => { + if (hovered && !isFocused) { + return {backgroundColor: theme.highlightBG}; + } + return {backgroundColor: focusValueRef.current.interpolate({inputRange: [0, 1], outputRange: [theme.appBG, theme.border]})}; + }, + [theme, isFocused], + ); + + const activeOpacityValue = focusValueRef.current; + const inactiveOpacityValue = focusValueRef.current.interpolate({inputRange: [0, 1], outputRange: [1, 0]}); + return ( {}, backgroundColor onPress={onPress} > {({hovered}) => ( - + )} diff --git a/src/libs/Navigation/OnyxTabNavigator.tsx b/src/libs/Navigation/OnyxTabNavigator.tsx index ef41269045c6..506c60900d2f 100644 --- a/src/libs/Navigation/OnyxTabNavigator.tsx +++ b/src/libs/Navigation/OnyxTabNavigator.tsx @@ -6,6 +6,9 @@ import {OnyxEntry} from 'react-native-onyx/lib/types'; import Tab from '@userActions/Tab'; import ONYXKEYS from '@src/ONYXKEYS'; import ChildrenProps from '@src/types/utils/ChildrenProps'; +import tabNavigatorAnimationEnabled from './tabNavigatorAnimationEnabled'; + +const screenOptions = {animationEnabled: tabNavigatorAnimationEnabled}; type OnyxTabNavigatorOnyxProps = { selectedTab: OnyxEntry; @@ -37,6 +40,7 @@ function OnyxTabNavigator({id, selectedTab = '', children, onTabSelected = () => {...rest} id={id} initialRouteName={selectedTab} + screenOptions={screenOptions} backBehavior="initialRoute" keyboardDismissMode="none" screenListeners={{ diff --git a/src/libs/Navigation/tabNavigatorAnimationEnabled/index.native.ts b/src/libs/Navigation/tabNavigatorAnimationEnabled/index.native.ts new file mode 100644 index 000000000000..531df2999215 --- /dev/null +++ b/src/libs/Navigation/tabNavigatorAnimationEnabled/index.native.ts @@ -0,0 +1,3 @@ +const tabNavigatorAnimationEnabled = true; + +export default tabNavigatorAnimationEnabled; diff --git a/src/libs/Navigation/tabNavigatorAnimationEnabled/index.ts b/src/libs/Navigation/tabNavigatorAnimationEnabled/index.ts new file mode 100644 index 000000000000..6282734e15c8 --- /dev/null +++ b/src/libs/Navigation/tabNavigatorAnimationEnabled/index.ts @@ -0,0 +1,3 @@ +const tabNavigatorAnimationEnabled = false; + +export default tabNavigatorAnimationEnabled; diff --git a/src/pages/iou/MoneyRequestSelectorPage.js b/src/pages/iou/MoneyRequestSelectorPage.js index 7b87b50bb7f3..f1cfea83ce5d 100644 --- a/src/pages/iou/MoneyRequestSelectorPage.js +++ b/src/pages/iou/MoneyRequestSelectorPage.js @@ -119,13 +119,7 @@ function MoneyRequestSelectorPage(props) { ( - - )} + tabBar={TabSelector} > color: isSelected ? theme.text : theme.textSupporting, lineHeight: 14, } satisfies TextStyle), - - tabBackground: (hovered: boolean, isFocused: boolean, background: string | Animated.AnimatedInterpolation) => ({ - backgroundColor: hovered && !isFocused ? theme.highlightBG : background, - }), - - tabOpacity: ( - hovered: boolean, - isFocused: boolean, - activeOpacityValue: number | Animated.AnimatedInterpolation, - inactiveOpacityValue: number | Animated.AnimatedInterpolation, - ) => ({ - opacity: hovered && !isFocused ? inactiveOpacityValue : activeOpacityValue, - }), - overscrollSpacer: (backgroundColor: string, height: number) => ({ backgroundColor,