Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix RequestMoney tabs animations #32577

Merged
Merged
79 changes: 15 additions & 64 deletions src/components/TabSelector/TabSelector.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<number | string>;
};

type IconAndTitle = {
Expand All @@ -49,66 +45,21 @@ function getIconAndTitle(route: string, translate: LocaleContextProps['translate
}
}

function getOpacity(position: Animated.AnimatedInterpolation<number>, 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 (
<View style={styles.tabSelector}>
{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,
Expand All @@ -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 (
Expand All @@ -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}
/>
);
})}
</View>
}),
[navigation, onTabPress, state.index, state.routes, translate],
);

return <View style={styles.tabSelector}>{tabs}</View>;
}

TabSelector.displayName = 'TabSelector';
Expand Down
60 changes: 43 additions & 17 deletions src/components/TabSelector/TabSelectorItem.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -16,21 +18,45 @@ type TabSelectorItemProps = {
/** Title of the tab */
title?: string;

/** Animated background color value for the tab button */
backgroundColor?: string | Animated.AnimatedInterpolation<string>;

/** Animated opacity value while the tab is in inactive state */
inactiveOpacity?: number | Animated.AnimatedInterpolation<number>;

/** Animated opacity value while the tab is in active state */
activeOpacity?: number | Animated.AnimatedInterpolation<number>;

/** 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],
);

Comment on lines +47 to +55
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be handy just to note here why we are using this approach instead of styles

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of the code was moved from old definition in TabSelector, but I am not that familiar with the style conventions, so if you feel like there is a room for improvement, then let me know and we can fix that 👍

const activeOpacityValue = focusValueRef.current;
const inactiveOpacityValue = focusValueRef.current.interpolate({inputRange: [0, 1], outputRange: [1, 0]});

return (
<PressableWithFeedback
accessibilityLabel={title}
Expand All @@ -39,16 +65,16 @@ function TabSelectorItem({icon, title = '', onPress = () => {}, backgroundColor
onPress={onPress}
>
{({hovered}) => (
<Animated.View style={[styles.tabSelectorButton, StyleSheet.absoluteFill, styles.tabBackground(hovered, isActive, backgroundColor)]}>
<Animated.View style={[styles.tabSelectorButton, StyleSheet.absoluteFill, getBackgroundColorStyle(hovered)]}>
<TabIcon
icon={icon}
activeOpacity={styles.tabOpacity(hovered, isActive, activeOpacity, inactiveOpacity).opacity}
inactiveOpacity={styles.tabOpacity(hovered, isActive, inactiveOpacity, activeOpacity).opacity}
activeOpacity={activeOpacityValue}
inactiveOpacity={inactiveOpacityValue}
/>
<TabLabel
title={title}
activeOpacity={styles.tabOpacity(hovered, isActive, activeOpacity, inactiveOpacity).opacity}
inactiveOpacity={styles.tabOpacity(hovered, isActive, inactiveOpacity, activeOpacity).opacity}
activeOpacity={activeOpacityValue}
inactiveOpacity={inactiveOpacityValue}
/>
</Animated.View>
)}
Expand Down
4 changes: 4 additions & 0 deletions src/libs/Navigation/OnyxTabNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>;
Expand Down Expand Up @@ -37,6 +40,7 @@ function OnyxTabNavigator({id, selectedTab = '', children, onTabSelected = () =>
{...rest}
id={id}
initialRouteName={selectedTab}
screenOptions={screenOptions}
backBehavior="initialRoute"
keyboardDismissMode="none"
screenListeners={{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const tabNavigatorAnimationEnabled = true;

export default tabNavigatorAnimationEnabled;
3 changes: 3 additions & 0 deletions src/libs/Navigation/tabNavigatorAnimationEnabled/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const tabNavigatorAnimationEnabled = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment for reference


export default tabNavigatorAnimationEnabled;
8 changes: 1 addition & 7 deletions src/pages/iou/MoneyRequestSelectorPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,7 @@ function MoneyRequestSelectorPage(props) {
<OnyxTabNavigator
id={CONST.TAB.RECEIPT_TAB_ID}
selectedTab={props.selectedTab}
tabBar={({state, navigation, position}) => (
<TabSelector
state={state}
navigation={navigation}
position={position}
/>
)}
tabBar={TabSelector}
>
<TopTab.Screen
name={CONST.TAB_REQUEST.MANUAL}
Expand Down
14 changes: 0 additions & 14 deletions src/styles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3701,20 +3701,6 @@ const styles = (theme: ThemeColors) =>
color: isSelected ? theme.text : theme.textSupporting,
lineHeight: 14,
} satisfies TextStyle),

tabBackground: (hovered: boolean, isFocused: boolean, background: string | Animated.AnimatedInterpolation<string>) => ({
backgroundColor: hovered && !isFocused ? theme.highlightBG : background,
}),

tabOpacity: (
hovered: boolean,
isFocused: boolean,
activeOpacityValue: number | Animated.AnimatedInterpolation<number>,
inactiveOpacityValue: number | Animated.AnimatedInterpolation<number>,
) => ({
opacity: hovered && !isFocused ? inactiveOpacityValue : activeOpacityValue,
}),

overscrollSpacer: (backgroundColor: string, height: number) =>
({
backgroundColor,
Expand Down
Loading