-
Notifications
You must be signed in to change notification settings - Fork 3k
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
WIP: Replace react-native
Animated API with react-native-reanimated
#52956
Changes from 2 commits
74361e3
a7dfc67
334a0de
1bc7257
4385dcf
e20c704
46d8efb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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'; | ||
|
@@ -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, | ||
|
@@ -64,11 +66,11 @@ function BaseGenericTooltip({ | |
} | ||
}, []); | ||
|
||
const {animationStyle, rootWrapperStyle, textStyle, pointerWrapperStyle, pointerStyle} = useMemo( | ||
const {rootWrapperStyle, textStyle, pointerWrapperStyle, pointerStyle} = useMemo( | ||
() => | ||
StyleUtils.getTooltipStyles({ | ||
tooltip: rootWrapper.current, | ||
currentSize: animation, | ||
currentSize: animation.value, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here |
||
windowWidth, | ||
xOffset, | ||
yOffset, | ||
|
@@ -102,6 +104,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>; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,13 @@ | ||
import type {Animated} from 'react-native'; | ||
Check failure on line 1 in src/components/Tooltip/BaseGenericTooltip/types.ts
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Eslint failing @sumo-slonik |
||
import type {SharedTooltipProps} from '@components/Tooltip/types'; | ||
Check failure on line 2 in src/components/Tooltip/BaseGenericTooltip/types.ts
|
||
import { SharedValue } from "react-native-reanimated"; | ||
Check failure on line 3 in src/components/Tooltip/BaseGenericTooltip/types.ts
|
||
|
||
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; | ||
|
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, runOnJS, 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'; | ||
|
@@ -60,9 +59,9 @@ | |
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 animationSharedValue = useSharedValue<number>(0); | ||
const isTooltipSenseInitiatorShared = useSharedValue<boolean>(true); | ||
const isAnimationCanceledShared = useSharedValue<boolean>(false); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we don't need to add SharedValue and Shared suffixes to these variables, what do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we can do without it |
||
const prevText = usePrevious(text); | ||
|
||
useEffect(() => { | ||
|
@@ -79,34 +78,40 @@ | |
setIsRendered(true); | ||
setIsVisible(true); | ||
|
||
animation.current.stopAnimation(); | ||
cancelAnimation(animationSharedValue); | ||
|
||
// When TooltipSense is active, immediately show the tooltip | ||
if (TooltipSense.isActive() && !shouldForceAnimate) { | ||
animation.current.setValue(1); | ||
animationSharedValue.value = 1; | ||
} else { | ||
isTooltipSenseInitiator.current = true; | ||
Animated.timing(animation.current, { | ||
toValue: 1, | ||
duration: 140, | ||
delay: 500, | ||
useNativeDriver: false, | ||
}).start(({finished}) => { | ||
isAnimationCanceled.current = !finished; | ||
}); | ||
isTooltipSenseInitiatorShared.value = true; | ||
animationSharedValue.value = withDelay( | ||
500, | ||
withTiming( | ||
1, | ||
{ | ||
duration: 140, | ||
}, | ||
(finished) => { | ||
runOnJS(() => { | ||
isAnimationCanceledShared.value = !finished; | ||
})(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm wondering why do we need to run it on JS thread? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will correct it in that case if it is not needed :) |
||
}, | ||
), | ||
); | ||
} | ||
TooltipSense.activate(); | ||
}, [shouldForceAnimate]); | ||
Check warning on line 104 in src/components/Tooltip/GenericTooltip.tsx
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. More ESLint warining, you need to add these shared values as dependencies (they won't cause reruns as these are stable references) |
||
|
||
// 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 && isAnimationCanceledShared.value && text && prevText !== text) { | ||
isAnimationCanceledShared.value = false; | ||
showTooltip(); | ||
} | ||
}, [isVisible, text, prevText, showTooltip]); | ||
Check warning on line 114 in src/components/Tooltip/GenericTooltip.tsx
|
||
|
||
/** | ||
* Update the tooltip's target bounding rectangle | ||
|
@@ -125,24 +130,19 @@ | |
* Hide the tooltip in an animation. | ||
*/ | ||
const hideTooltip = useCallback(() => { | ||
animation.current.stopAnimation(); | ||
cancelAnimation(animationSharedValue); | ||
|
||
if (TooltipSense.isActive() && !isTooltipSenseInitiator.current) { | ||
animation.current.setValue(0); | ||
if (TooltipSense.isActive() && !isTooltipSenseInitiatorShared.value) { | ||
// eslint-disable-next-line react-compiler/react-compiler | ||
animationSharedValue.value = 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(); | ||
isTooltipSenseInitiatorShared.value = false; | ||
animationSharedValue.value = 0; | ||
} | ||
|
||
TooltipSense.deactivate(); | ||
|
||
setIsVisible(false); | ||
}, []); | ||
Check warning on line 145 in src/components/Tooltip/GenericTooltip.tsx
|
||
|
||
const onPressOverlay = useCallback(() => { | ||
if (!shouldUseOverlay) { | ||
|
@@ -153,8 +153,6 @@ | |
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 | ||
|
@@ -166,7 +164,7 @@ | |
{isRendered && ( | ||
<BaseGenericTooltip | ||
// eslint-disable-next-line react-compiler/react-compiler | ||
animation={animation.current} | ||
animation={animationSharedValue} | ||
windowWidth={windowWidth} | ||
xOffset={xOffset} | ||
yOffset={yOffset} | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -1,5 +1,6 @@ | ||||||
import type {StyleProp, TextStyle, View, ViewStyle} from 'react-native'; | ||||||
import {Animated, StyleSheet} from 'react-native'; | ||||||
import {StyleSheet} from 'react-native'; | ||||||
import type {SharedValue} from 'react-native-reanimated'; | ||||||
import FontUtils from '@styles/utils/FontUtils'; | ||||||
// eslint-disable-next-line no-restricted-imports | ||||||
import type StyleUtilGenerator from '@styles/utils/generators/types'; | ||||||
|
@@ -30,7 +31,7 @@ type TooltipStyles = { | |||||
|
||||||
type TooltipParams = { | ||||||
tooltip: View | HTMLDivElement | null; | ||||||
currentSize: Animated.Value; | ||||||
currentSize: number; | ||||||
windowWidth: number; | ||||||
xOffset: number; | ||||||
yOffset: number; | ||||||
|
@@ -47,7 +48,13 @@ type TooltipParams = { | |||||
shouldAddHorizontalPadding?: boolean; | ||||||
}; | ||||||
|
||||||
type GetTooltipStylesStyleUtil = {getTooltipStyles: (props: TooltipParams) => TooltipStyles}; | ||||||
type TooltipAnimationProps = { | ||||||
tooltipContentWidth?: number; | ||||||
tooltipWrapperHeight?: number; | ||||||
currentSize: SharedValue<number>; | ||||||
}; | ||||||
|
||||||
type GetTooltipStylesStyleUtil = {getTooltipStyles: (props: TooltipParams) => TooltipStyles; getTooltipAnimatedStyles: (props: TooltipAnimationProps) => {transform: [{scale: number}]}}; | ||||||
|
||||||
/** | ||||||
* Generate styles for the tooltip component. | ||||||
|
@@ -108,7 +115,7 @@ const createTooltipStyleUtils: StyleUtilGenerator<GetTooltipStylesStyleUtil> = ( | |||||
const isTooltipSizeReady = tooltipWidth !== undefined && tooltipHeight !== undefined; | ||||||
|
||||||
// Set the scale to 1 to be able to measure the tooltip size correctly when it's not ready yet. | ||||||
let scale = new Animated.Value(1); | ||||||
let scale = 1; | ||||||
let shouldShowBelow = false; | ||||||
let horizontalShift = 0; | ||||||
let horizontalShiftPointer = 0; | ||||||
|
@@ -269,6 +276,21 @@ const createTooltipStyleUtils: StyleUtilGenerator<GetTooltipStylesStyleUtil> = ( | |||||
}, | ||||||
}; | ||||||
}, | ||||||
|
||||||
// Utility function to create and manage scale animations with React Native Reanimated | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's use JSDoc syntax, this way when you hover over functions you get intellisense 😄
Suggested change
|
||||||
|
||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
getTooltipAnimatedStyles: (props: TooltipAnimationProps) => { | ||||||
const tooltipHorizontalPadding = spacing.ph2.paddingHorizontal * 2; | ||||||
const tooltipWidth = props.tooltipContentWidth && props.tooltipContentWidth + tooltipHorizontalPadding + 1; | ||||||
const isTooltipSizeReady = tooltipWidth !== undefined && props.tooltipWrapperHeight !== undefined; | ||||||
let scale = 1; | ||||||
if (isTooltipSizeReady) { | ||||||
scale = props.currentSize.value; | ||||||
} | ||||||
return { | ||||||
transform: [{scale}], | ||||||
}; | ||||||
}, | ||||||
}); | ||||||
|
||||||
export default createTooltipStyleUtils; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line should be removed, right? And the logic inside getTooltipStyles adjusted so that animationStyles aren't returned.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will correct it, I didn't want to change utils that are already written, but I can safely remove it is not used anywhere