Skip to content

Commit

Permalink
Merge pull request #53298 from software-mansion-labs/migrate/switch_a…
Browse files Browse the repository at this point in the history
…nd_tooltip_animate_to_reanimate

Migrate animated to reanimated Tooltip and Switch
  • Loading branch information
mountiny authored Nov 30, 2024
2 parents 32ce9d4 + daf4697 commit 9056c61
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 91 deletions.
45 changes: 23 additions & 22 deletions src/components/Switch.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React, {useEffect, useRef} from 'react';
import {Animated, InteractionManager} from 'react-native';
import React from 'react';
import {InteractionManager} from 'react-native';
import Animated, {interpolateColor, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useNativeDriver from '@libs/useNativeDriver';
import CONST from '@src/CONST';
import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
Expand Down Expand Up @@ -35,7 +35,7 @@ const OFFSET_X = {

function Switch({isOn, onToggle, accessibilityLabel, disabled, showLockIcon, disabledAction}: SwitchProps) {
const styles = useThemeStyles();
const offsetX = useRef(new Animated.Value(isOn ? OFFSET_X.ON : OFFSET_X.OFF));
const offsetX = useSharedValue(isOn ? OFFSET_X.ON : OFFSET_X.OFF);
const theme = useTheme();

const handleSwitchPress = () => {
Expand All @@ -44,22 +44,22 @@ function Switch({isOn, onToggle, accessibilityLabel, disabled, showLockIcon, dis
disabledAction?.();
return;
}
offsetX.set(withTiming(isOn ? OFFSET_X.OFF : OFFSET_X.ON, {duration: 300}));
onToggle(!isOn);
});
};

useEffect(() => {
Animated.timing(offsetX.current, {
toValue: isOn ? OFFSET_X.ON : OFFSET_X.OFF,
duration: 300,
useNativeDriver,
}).start();
}, [isOn]);
const animatedThumbStyle = useAnimatedStyle(() => ({
transform: [{translateX: offsetX.get()}],
}));

const animatedSwitchTrackStyle = useAnimatedStyle(() => ({
backgroundColor: interpolateColor(offsetX.get(), [OFFSET_X.OFF, OFFSET_X.ON], [theme.icon, theme.success]),
}));

return (
<PressableWithFeedback
disabled={!disabledAction && disabled}
style={[styles.switchTrack, !isOn && styles.switchInactive]}
onPress={handleSwitchPress}
onLongPress={handleSwitchPress}
role={CONST.ROLE.SWITCH}
Expand All @@ -69,16 +69,17 @@ function Switch({isOn, onToggle, accessibilityLabel, disabled, showLockIcon, dis
hoverDimmingValue={1}
pressDimmingValue={0.8}
>
{/* eslint-disable-next-line react-compiler/react-compiler */}
<Animated.View style={[styles.switchThumb, styles.switchThumbTransformation(offsetX.current)]}>
{(!!disabled || !!showLockIcon) && (
<Icon
src={Expensicons.Lock}
fill={isOn ? theme.text : theme.icon}
width={styles.toggleSwitchLockIcon.width}
height={styles.toggleSwitchLockIcon.height}
/>
)}
<Animated.View style={[styles.switchTrack, animatedSwitchTrackStyle]}>
<Animated.View style={[styles.switchThumb, animatedThumbStyle]}>
{(!!disabled || !!showLockIcon) && (
<Icon
src={Expensicons.Lock}
fill={isOn ? theme.text : theme.icon}
width={styles.toggleSwitchLockIcon.width}
height={styles.toggleSwitchLockIcon.height}
/>
)}
</Animated.View>
</Animated.View>
</PressableWithFeedback>
);
Expand Down
12 changes: 7 additions & 5 deletions src/components/Tooltip/BaseGenericTooltip/index.native.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {Portal} from '@gorhom/portal';
import React, {useMemo, useRef, useState} from 'react';
import {Animated, InteractionManager, View} from 'react-native';
import {InteractionManager, View} from 'react-native';
// eslint-disable-next-line no-restricted-imports
import type {View as RNView} from 'react-native';
import Animated, {useAnimatedStyle} from 'react-native-reanimated';
import TransparentOverlay from '@components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/TransparentOverlay/TransparentOverlay';
import Text from '@components/Text';
import useStyleUtils from '@hooks/useStyleUtils';
Expand Down Expand Up @@ -46,13 +47,11 @@ function BaseGenericTooltip({
const rootWrapper = useRef<RNView>(null);

const StyleUtils = useStyleUtils();

const {animationStyle, rootWrapperStyle, textStyle, pointerWrapperStyle, pointerStyle} = useMemo(
const {rootWrapperStyle, textStyle, pointerWrapperStyle, pointerStyle} = useMemo(
() =>
StyleUtils.getTooltipStyles({
// eslint-disable-next-line react-compiler/react-compiler
tooltip: rootWrapper.current,
currentSize: animation,
windowWidth,
xOffset,
yOffset,
Expand All @@ -70,7 +69,6 @@ function BaseGenericTooltip({
}),
[
StyleUtils,
animation,
windowWidth,
xOffset,
yOffset,
Expand All @@ -87,6 +85,10 @@ function BaseGenericTooltip({
],
);

const animationStyle = useAnimatedStyle(() => {
return StyleUtils.getTooltipAnimatedStyles({tooltipContentWidth: contentMeasuredWidth, tooltipWrapperHeight: wrapperMeasuredHeight, currentSize: animation});
});

let content;
if (renderTooltipContent) {
content = <View>{renderTooltipContent()}</View>;
Expand Down
12 changes: 8 additions & 4 deletions src/components/Tooltip/BaseGenericTooltip/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/* eslint-disable react-compiler/react-compiler */
import React, {useLayoutEffect, useMemo, useRef, useState} from 'react';
import ReactDOM from 'react-dom';
import {Animated, View} from 'react-native';
import {View} from 'react-native';
import Animated, {useAnimatedStyle} from 'react-native-reanimated';
import TransparentOverlay from '@components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/TransparentOverlay/TransparentOverlay';
import Text from '@components/Text';
import useStyleUtils from '@hooks/useStyleUtils';
Expand All @@ -15,6 +16,7 @@ import type {BaseGenericTooltipProps} from './types';
// We also update the state on layout changes which will be triggered often.
// There will be n number of tooltip components in the page.
// It's good to memoize this one.

function BaseGenericTooltip({
animation,
windowWidth,
Expand Down Expand Up @@ -64,11 +66,10 @@ function BaseGenericTooltip({
}
}, []);

const {animationStyle, rootWrapperStyle, textStyle, pointerWrapperStyle, pointerStyle} = useMemo(
const {rootWrapperStyle, textStyle, pointerWrapperStyle, pointerStyle} = useMemo(
() =>
StyleUtils.getTooltipStyles({
tooltip: rootWrapper.current,
currentSize: animation,
windowWidth,
xOffset,
yOffset,
Expand All @@ -85,7 +86,6 @@ function BaseGenericTooltip({
}),
[
StyleUtils,
animation,
windowWidth,
xOffset,
yOffset,
Expand All @@ -102,6 +102,10 @@ function BaseGenericTooltip({
],
);

const animationStyle = useAnimatedStyle(() => {
return StyleUtils.getTooltipAnimatedStyles({tooltipContentWidth: contentMeasuredWidth, tooltipWrapperHeight: wrapperMeasuredHeight, currentSize: animation});
});

let content;
if (renderTooltipContent) {
content = <View ref={viewRef(contentRef)}>{renderTooltipContent()}</View>;
Expand Down
4 changes: 2 additions & 2 deletions src/components/Tooltip/BaseGenericTooltip/types.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type {Animated} from 'react-native';
import type {SharedValue} from 'react-native-reanimated';
import type {SharedTooltipProps} from '@components/Tooltip/types';

type BaseGenericTooltipProps = {
/** Window width */
windowWidth: number;

/** Tooltip Animation value */
animation: Animated.Value;
animation: SharedValue<number>;

/** The distance between the left side of the wrapper view and the left side of the window */
xOffset: number;
Expand Down
70 changes: 34 additions & 36 deletions src/components/Tooltip/GenericTooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import React, {memo, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react';
import React, {memo, useCallback, useEffect, useState} from 'react';
import type {LayoutRectangle} from 'react-native';
import {Animated} from 'react-native';
import {cancelAnimation, useSharedValue, withDelay, withTiming} from 'react-native-reanimated';
import useLocalize from '@hooks/useLocalize';
import usePrevious from '@hooks/usePrevious';
import useWindowDimensions from '@hooks/useWindowDimensions';
import Log from '@libs/Log';
import StringUtils from '@libs/StringUtils';
import TooltipRefManager from '@libs/TooltipRefManager';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import callOrReturn from '@src/types/utils/callOrReturn';
Expand Down Expand Up @@ -60,9 +59,9 @@ function GenericTooltip({
const [shouldUseOverlay, setShouldUseOverlay] = useState(shouldUseOverlayProp);

// Whether the tooltip is first tooltip to activate the TooltipSense
const isTooltipSenseInitiator = useRef(false);
const animation = useRef(new Animated.Value(0));
const isAnimationCanceled = useRef(false);
const animation = useSharedValue<number>(0);
const isTooltipSenseInitiator = useSharedValue<boolean>(true);
const isAnimationCanceled = useSharedValue<boolean>(false);
const prevText = usePrevious(text);

useEffect(() => {
Expand All @@ -79,34 +78,40 @@ function GenericTooltip({
setIsRendered(true);
setIsVisible(true);

animation.current.stopAnimation();
cancelAnimation(animation);

// When TooltipSense is active, immediately show the tooltip
if (TooltipSense.isActive() && !shouldForceAnimate) {
animation.current.setValue(1);
animation.set(1);
} else {
isTooltipSenseInitiator.current = true;
Animated.timing(animation.current, {
toValue: 1,
duration: 140,
delay: 500,
useNativeDriver: false,
}).start(({finished}) => {
isAnimationCanceled.current = !finished;
});
isTooltipSenseInitiator.set(true);
animation.set(
withDelay(
500,
withTiming(
1,
{
duration: 140,
},
(finished) => {
isAnimationCanceled.set(!finished);
},
),
),
);
}
TooltipSense.activate();
}, [shouldForceAnimate]);
}, [animation, isAnimationCanceled, isTooltipSenseInitiator, shouldForceAnimate]);

// eslint-disable-next-line rulesdir/prefer-early-return
useEffect(() => {
// if the tooltip text changed before the initial animation was finished, then the tooltip won't be shown
// we need to show the tooltip again
if (isVisible && isAnimationCanceled.current && text && prevText !== text) {
isAnimationCanceled.current = false;
if (isVisible && isAnimationCanceled.get() && text && prevText !== text) {
isAnimationCanceled.set(false);
showTooltip();
}
}, [isVisible, text, prevText, showTooltip]);
}, [isVisible, text, prevText, showTooltip, isAnimationCanceled]);

/**
* Update the tooltip's target bounding rectangle
Expand All @@ -125,24 +130,19 @@ function GenericTooltip({
* Hide the tooltip in an animation.
*/
const hideTooltip = useCallback(() => {
animation.current.stopAnimation();
cancelAnimation(animation);

if (TooltipSense.isActive() && !isTooltipSenseInitiator.current) {
animation.current.setValue(0);
if (TooltipSense.isActive() && !isTooltipSenseInitiator.get()) {
// eslint-disable-next-line react-compiler/react-compiler
animation.set(0);
} else {
// Hide the first tooltip which initiated the TooltipSense with animation
isTooltipSenseInitiator.current = false;
Animated.timing(animation.current, {
toValue: 0,
duration: 140,
useNativeDriver: false,
}).start();
isTooltipSenseInitiator.set(false);
animation.set(0);
}

TooltipSense.deactivate();

setIsVisible(false);
}, []);
}, [animation, isTooltipSenseInitiator]);

const onPressOverlay = useCallback(() => {
if (!shouldUseOverlay) {
Expand All @@ -153,8 +153,6 @@ function GenericTooltip({
onHideTooltip();
}, [shouldUseOverlay, onHideTooltip, hideTooltip]);

useImperativeHandle(TooltipRefManager.ref, () => ({hideTooltip}), [hideTooltip]);

// Skip the tooltip and return the children if the text is empty, we don't have a render function.
if (StringUtils.isEmptyString(text) && renderTooltipContent == null) {
// eslint-disable-next-line react-compiler/react-compiler
Expand All @@ -166,7 +164,7 @@ function GenericTooltip({
{isRendered && (
<BaseGenericTooltip
// eslint-disable-next-line react-compiler/react-compiler
animation={animation.current}
animation={animation}
windowWidth={windowWidth}
xOffset={xOffset}
yOffset={yOffset}
Expand Down
6 changes: 0 additions & 6 deletions src/styles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3128,7 +3128,6 @@ const styles = (theme: ThemeColors) =>
justifyContent: 'center',
borderRadius: 20,
padding: 15,
backgroundColor: theme.success,
},

switchInactive: {
Expand All @@ -3146,11 +3145,6 @@ const styles = (theme: ThemeColors) =>
backgroundColor: theme.appBG,
},

switchThumbTransformation: (translateX: AnimatableNumericValue) =>
({
transform: [{translateX}],
} satisfies ViewStyle),

radioButtonContainer: {
backgroundColor: theme.componentBG,
borderRadius: 14,
Expand Down
Loading

0 comments on commit 9056c61

Please sign in to comment.