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
75 changes: 11 additions & 64 deletions src/components/TabSelector/TabSelector.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import PropTypes from 'prop-types';
import React, {useCallback, useMemo, useState} from 'react';
import React, {useMemo} from 'react';
import {View} from 'react-native';
import _ from 'underscore';
import * as Expensicons from '@components/Icon/Expensicons';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@styles/themes/useTheme';
import {TabNavigatorAnimationEnabled} from '@libs/Navigation/OnyxTabNavigator';
import useThemeStyles from '@styles/useThemeStyles';
import CONST from '@src/CONST';
import TabSelectorItem from './TabSelectorItem';
Expand All @@ -22,18 +22,10 @@ const propTypes = {

/* Callback fired when tab is pressed */
onTabPress: PropTypes.func,

/* AnimatedValue for the position of the screen while swiping */
position: PropTypes.shape({
interpolate: PropTypes.func.isRequired,
}),
};

const defaultProps = {
onTabPress: () => {},
position: {
interpolate: () => {},
},
};

const getIconAndTitle = (route, translate) => {
Expand All @@ -53,56 +45,13 @@ const getIconAndTitle = (route, translate) => {
}
};

const getOpacity = (position, routesLength, tabIndex, active, affectedTabs) => {
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: _.map(inputRange, (i) => (affectedTabs.includes(tabIndex) && i === tabIndex ? activeValue : inactiveValue)),
});
}
return activeValue;
};

function TabSelector({state, navigation, onTabPress, position}) {
function TabSelector({state, navigation, onTabPress}) {
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, tabIndex, affectedTabs) => {
if (routesLength > 1) {
const inputRange = Array.from({length: routesLength}, (v, i) => i);

return position.interpolate({
inputRange,
outputRange: _.map(inputRange, (i) => (affectedTabs.includes(tabIndex) && i === tabIndex ? theme.border : theme.appBG)),
});
}
return theme.border;
},
[theme, position],
);

React.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}>
{_.map(state.routes, (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 tabs = useMemo(
() =>
_.map(state.routes, (route, index) => {
const isFocused = index === state.index;
const {icon, title} = getIconAndTitle(route.name, translate);

Expand All @@ -111,8 +60,6 @@ function TabSelector({state, navigation, onTabPress, position}) {
return;
}

setAffectedAnimatedTabs([state.index, index]);

const event = navigation.emit({
type: 'tabPress',
target: route.key,
Expand All @@ -133,15 +80,15 @@ function TabSelector({state, navigation, onTabPress, position}) {
icon={icon}
title={title}
onPress={onPress}
activeOpacity={activeOpacity}
inactiveOpacity={inactiveOpacity}
backgroundColor={backgroundColor}
isFocused={isFocused}
animationEnabled={TabNavigatorAnimationEnabled()}
/>
);
})}
</View>
}),
[navigation, onTabPress, state.index, state.routes, translate],
);

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

TabSelector.propTypes = propTypes;
Expand Down
65 changes: 43 additions & 22 deletions src/components/TabSelector/TabSelectorItem.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import PropTypes from 'prop-types';
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 '@styles/themes/useTheme';
import useThemeStyles from '@styles/useThemeStyles';
import CONST from '@src/CONST';
import TabIcon from './TabIcon';
import TabLabel from './TabLabel';

Expand All @@ -16,34 +18,53 @@ const propTypes = {
/** Title of the tab */
title: PropTypes.string,

/** Animated background color value for the tab button */
// eslint-disable-next-line
backgroundColor: PropTypes.any,

/** Animated opacity value while the label is inactive state */
// eslint-disable-next-line
inactiveOpacity: PropTypes.any,

/** Animated opacity value while the label is in active state */
// eslint-disable-next-line
activeOpacity: PropTypes.any,

/** Whether this tab is active */
isFocused: PropTypes.bool,

/** Whether animations should be skipped */
animationEnabled: PropTypes.bool,
};

const defaultProps = {
onPress: () => {},
icon: () => {},
title: '',
backgroundColor: '',
inactiveOpacity: 1,
activeOpacity: 0,
isFocused: false,
animationEnabled: true,
};

function TabSelectorItem({icon, title, onPress, backgroundColor, activeOpacity, inactiveOpacity, isFocused}) {
function TabSelectorItem({icon, title, onPress, isFocused, animationEnabled}) {
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) => {
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 (
<PressableWithFeedback
accessibilityLabel={title}
Expand All @@ -52,16 +73,16 @@ function TabSelectorItem({icon, title, onPress, backgroundColor, activeOpacity,
onPress={onPress}
>
{({hovered}) => (
<Animated.View style={[styles.tabSelectorButton, StyleSheet.absoluteFill, styles.tabBackground(hovered, isFocused, backgroundColor)]}>
<Animated.View style={[styles.tabSelectorButton, StyleSheet.absoluteFill, getBackgroundColorStyle(hovered)]}>
<TabIcon
icon={icon}
activeOpacity={styles.tabOpacity(hovered, isFocused, activeOpacity, inactiveOpacity).opacity}
inactiveOpacity={styles.tabOpacity(hovered, isFocused, inactiveOpacity, activeOpacity).opacity}
activeOpacity={activeOpacityValue}
inactiveOpacity={inactiveOpacityValue}
/>
<TabLabel
title={title}
activeOpacity={styles.tabOpacity(hovered, isFocused, activeOpacity, inactiveOpacity).opacity}
inactiveOpacity={styles.tabOpacity(hovered, isFocused, inactiveOpacity, activeOpacity).opacity}
activeOpacity={activeOpacityValue}
inactiveOpacity={inactiveOpacityValue}
/>
</Animated.View>
)}
Expand Down
15 changes: 15 additions & 0 deletions src/libs/Navigation/OnyxTabNavigator.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
import {createMaterialTopTabNavigator, MaterialTopTabNavigationEventMap} from '@react-navigation/material-top-tabs';
import {EventMapCore, NavigationState, ScreenListeners} from '@react-navigation/native';
import React from 'react';
import {Platform} from 'react-native';
import {withOnyx} from 'react-native-onyx';
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';

const TabNavigatorAnimationEnabled = () => {
switch (Platform.OS) {
kacper-mikolajczak marked this conversation as resolved.
Show resolved Hide resolved
case 'macos':
case 'windows':
case 'web':
return false;
default:
return true;
}
};

type OnyxTabNavigatorOnyxProps = {
selectedTab: OnyxEntry<string>;
};
Expand Down Expand Up @@ -34,6 +46,7 @@ function OnyxTabNavigator({id, selectedTab = '', children, screenListeners, ...r
{...rest}
id={id}
initialRouteName={selectedTab}
screenOptions={{animationEnabled: TabNavigatorAnimationEnabled()}}
kacper-mikolajczak marked this conversation as resolved.
Show resolved Hide resolved
backBehavior="initialRoute"
keyboardDismissMode="none"
screenListeners={{
Expand All @@ -59,3 +72,5 @@ export default withOnyx<OnyxTabNavigatorProps, OnyxTabNavigatorOnyxProps>({
key: ({id}) => `${ONYXKEYS.COLLECTION.SELECTED_TAB}${id}`,
},
})(OnyxTabNavigator);

export {TabNavigatorAnimationEnabled};
8 changes: 1 addition & 7 deletions src/pages/iou/MoneyRequestSelectorPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,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.MANUAL}
Expand Down
9 changes: 0 additions & 9 deletions src/styles/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3612,15 +3612,6 @@ const styles = (theme: ThemeColors) =>
fontWeight: isSelected ? fontWeightBold : '400',
color: isSelected ? theme.text : theme.textSupporting,
} satisfies TextStyle),

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

tabOpacity: (hovered: boolean, isFocused: boolean, activeOpacityValue: number, inactiveOpacityValue: number) => ({
opacity: hovered && !isFocused ? inactiveOpacityValue : activeOpacityValue,
}),

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