From 7b6c18272385494145676e42cbe922bde7ceae6b Mon Sep 17 00:00:00 2001 From: dan Date: Mon, 18 Nov 2024 22:21:47 +0000 Subject: [PATCH] Add useHandleRef as a lighter alternative for useAnimatedRef (#6500) --- src/lib/hooks/useHandleRef.ts | 39 +++++++++++++++++++ src/screens/Profile/Header/Shell.tsx | 18 ++++----- src/view/com/profile/ProfileSubpageHeader.tsx | 18 ++++----- src/view/com/util/images/AutoSizedImage.tsx | 13 +++---- src/view/com/util/images/Gallery.tsx | 13 +++---- src/view/com/util/images/ImageLayoutGrid.tsx | 14 +++---- src/view/com/util/post-embeds/index.tsx | 14 +++---- 7 files changed, 75 insertions(+), 54 deletions(-) create mode 100644 src/lib/hooks/useHandleRef.ts diff --git a/src/lib/hooks/useHandleRef.ts b/src/lib/hooks/useHandleRef.ts new file mode 100644 index 0000000000..167ba270b6 --- /dev/null +++ b/src/lib/hooks/useHandleRef.ts @@ -0,0 +1,39 @@ +import {useState} from 'react' +import {AnimatedRef, measure, MeasuredDimensions} from 'react-native-reanimated' + +export type HandleRef = { + (node: any): void + current: null | number +} + +// This is a lighterweight alternative to `useAnimatedRef()` for imperative UI thread actions. +// Render it like , then pass `ref.current` to `measureHandle()` and such. +export function useHandleRef(): HandleRef { + return useState(() => { + const ref = (node: any) => { + if (node) { + ref.current = + node._nativeTag ?? + node.__nativeTag ?? + node.canonical?.nativeTag ?? + null + } else { + ref.current = null + } + } + ref.current = null + return ref + })[0] as HandleRef +} + +// When using this version, you need to read ref.current on the JS thread, and pass it to UI. +export function measureHandle( + current: number | null, +): MeasuredDimensions | null { + 'worklet' + if (current !== null) { + return measure((() => current) as AnimatedRef) + } else { + return null + } +} diff --git a/src/screens/Profile/Header/Shell.tsx b/src/screens/Profile/Header/Shell.tsx index 1a1e7d4a24..573d38145d 100644 --- a/src/screens/Profile/Header/Shell.tsx +++ b/src/screens/Profile/Header/Shell.tsx @@ -1,12 +1,6 @@ import React, {memo} from 'react' import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native' -import Animated, { - measure, - MeasuredDimensions, - runOnJS, - runOnUI, - useAnimatedRef, -} from 'react-native-reanimated' +import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated' import {AppBskyActorDefs, ModerationDecision} from '@atproto/api' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {msg} from '@lingui/macro' @@ -14,6 +8,7 @@ import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' import {BACK_HITSLOP} from '#/lib/constants' +import {measureHandle, useHandleRef} from '#/lib/hooks/useHandleRef' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {NavigationProp} from '#/lib/routes/types' import {isIOS} from '#/platform/detection' @@ -49,7 +44,7 @@ let ProfileHeaderShell = ({ const {openLightbox} = useLightboxControls() const navigation = useNavigation() const {isDesktop} = useWebMediaQueries() - const aviRef = useAnimatedRef() + const aviRef = useHandleRef() const onPressBack = React.useCallback(() => { if (navigation.canGoBack()) { @@ -86,9 +81,10 @@ let ProfileHeaderShell = ({ const modui = moderation.ui('avatar') const avatar = profile.avatar if (avatar && !(modui.blur && modui.noOverride)) { + const aviHandle = aviRef.current runOnUI(() => { 'worklet' - const rect = measure(aviRef) + const rect = measureHandle(aviHandle) runOnJS(_openLightbox)(avatar, rect) })() } @@ -170,14 +166,14 @@ let ProfileHeaderShell = ({ styles.avi, profile.associated?.labeler && styles.aviLabeler, ]}> - + - + diff --git a/src/view/com/profile/ProfileSubpageHeader.tsx b/src/view/com/profile/ProfileSubpageHeader.tsx index d73b322f2e..0e25fe5e61 100644 --- a/src/view/com/profile/ProfileSubpageHeader.tsx +++ b/src/view/com/profile/ProfileSubpageHeader.tsx @@ -1,18 +1,13 @@ import React from 'react' import {Pressable, StyleSheet, View} from 'react-native' -import Animated, { - measure, - MeasuredDimensions, - runOnJS, - runOnUI, - useAnimatedRef, -} from 'react-native-reanimated' +import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' import {BACK_HITSLOP} from '#/lib/constants' +import {measureHandle, useHandleRef} from '#/lib/hooks/useHandleRef' import {usePalette} from '#/lib/hooks/usePalette' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {makeProfileLink} from '#/lib/routes/links' @@ -60,7 +55,7 @@ export function ProfileSubpageHeader({ const {openLightbox} = useLightboxControls() const pal = usePalette('default') const canGoBack = navigation.canGoBack() - const aviRef = useAnimatedRef() + const aviRef = useHandleRef() const onPressBack = React.useCallback(() => { if (navigation.canGoBack()) { @@ -101,9 +96,10 @@ export function ProfileSubpageHeader({ if ( avatar // TODO && !(view.moderation.avatar.blur && view.moderation.avatar.noOverride) ) { + const aviHandle = aviRef.current runOnUI(() => { 'worklet' - const rect = measure(aviRef) + const rect = measureHandle(aviHandle) runOnJS(_openLightbox)(avatar, rect) })() } @@ -155,7 +151,7 @@ export function ProfileSubpageHeader({ paddingBottom: 6, paddingHorizontal: isMobile ? 12 : 14, }}> - + )} - + {isLoading ? ( >, - fetchedDims: Dimensions | null, - ) => void + onPress?: (containerRef: HandleRef, fetchedDims: Dimensions | null) => void onLongPress?: () => void onPressIn?: () => void }) { const t = useTheme() const {_} = useLingui() const largeAlt = useLargeAltBadgeEnabled() - const containerRef = useAnimatedRef() + const containerRef = useHandleRef() const fetchedDimsRef = useRef<{width: number; height: number} | null>(null) let aspectRatio: number | undefined @@ -109,7 +106,7 @@ export function AutoSizedImage({ const hasAlt = !!image.alt const contents = ( - + ) : null} - + ) if (cropDisabled) { diff --git a/src/view/com/util/images/Gallery.tsx b/src/view/com/util/images/Gallery.tsx index 9d0817bd2f..cc3eda68d2 100644 --- a/src/view/com/util/images/Gallery.tsx +++ b/src/view/com/util/images/Gallery.tsx @@ -1,11 +1,11 @@ import React from 'react' import {Pressable, StyleProp, View, ViewStyle} from 'react-native' -import Animated, {AnimatedRef} from 'react-native-reanimated' import {Image, ImageStyle} from 'expo-image' import {AppBskyEmbedImages} from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {HandleRef} from '#/lib/hooks/useHandleRef' import {Dimensions} from '#/lib/media/types' import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' import {PostEmbedViewContext} from '#/view/com/util/post-embeds/types' @@ -20,7 +20,7 @@ interface Props { index: number onPress?: ( index: number, - containerRefs: AnimatedRef>[], + containerRefs: HandleRef[], fetchedDims: (Dimensions | null)[], ) => void onLongPress?: EventFunction @@ -28,7 +28,7 @@ interface Props { imageStyle?: StyleProp viewContext?: PostEmbedViewContext insetBorderStyle?: StyleProp - containerRefs: AnimatedRef>[] + containerRefs: HandleRef[] thumbDimsRef: React.MutableRefObject<(Dimensions | null)[]> } @@ -52,10 +52,7 @@ export function GalleryItem({ const hideBadges = viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia return ( - + ) : null} - + ) } diff --git a/src/view/com/util/images/ImageLayoutGrid.tsx b/src/view/com/util/images/ImageLayoutGrid.tsx index dcc330dace..16ea9d453f 100644 --- a/src/view/com/util/images/ImageLayoutGrid.tsx +++ b/src/view/com/util/images/ImageLayoutGrid.tsx @@ -1,8 +1,8 @@ import React from 'react' import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' -import {AnimatedRef, useAnimatedRef} from 'react-native-reanimated' import {AppBskyEmbedImages} from '@atproto/api' +import {HandleRef, useHandleRef} from '#/lib/hooks/useHandleRef' import {PostEmbedViewContext} from '#/view/com/util/post-embeds/types' import {atoms as a, useBreakpoints} from '#/alf' import {Dimensions} from '../../lightbox/ImageViewing/@types' @@ -12,7 +12,7 @@ interface ImageLayoutGridProps { images: AppBskyEmbedImages.ViewImage[] onPress?: ( index: number, - containerRefs: AnimatedRef>[], + containerRefs: HandleRef[], fetchedDims: (Dimensions | null)[], ) => void onLongPress?: (index: number) => void @@ -43,7 +43,7 @@ interface ImageLayoutGridInnerProps { images: AppBskyEmbedImages.ViewImage[] onPress?: ( index: number, - containerRefs: AnimatedRef>[], + containerRefs: HandleRef[], fetchedDims: (Dimensions | null)[], ) => void onLongPress?: (index: number) => void @@ -56,10 +56,10 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) { const gap = props.gap const count = props.images.length - const containerRef1 = useAnimatedRef() - const containerRef2 = useAnimatedRef() - const containerRef3 = useAnimatedRef() - const containerRef4 = useAnimatedRef() + const containerRef1 = useHandleRef() + const containerRef2 = useHandleRef() + const containerRef3 = useHandleRef() + const containerRef4 = useHandleRef() const thumbDimsRef = React.useRef<(Dimensions | null)[]>([]) switch (count) { diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx index 1351a2cbc3..9dc43da8e7 100644 --- a/src/view/com/util/post-embeds/index.tsx +++ b/src/view/com/util/post-embeds/index.tsx @@ -6,13 +6,7 @@ import { View, ViewStyle, } from 'react-native' -import { - AnimatedRef, - measure, - MeasuredDimensions, - runOnJS, - runOnUI, -} from 'react-native-reanimated' +import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated' import {Image} from 'expo-image' import { AppBskyEmbedExternal, @@ -27,6 +21,7 @@ import { ModerationDecision, } from '@atproto/api' +import {HandleRef, measureHandle} from '#/lib/hooks/useHandleRef' import {usePalette} from '#/lib/hooks/usePalette' import {useLightboxControls} from '#/state/lightbox' import {useModerationOpts} from '#/state/preferences/moderation-opts' @@ -163,12 +158,13 @@ export function PostEmbeds({ } const onPress = ( index: number, - refs: AnimatedRef>[], + refs: HandleRef[], fetchedDims: (Dimensions | null)[], ) => { + const handles = refs.map(r => r.current) runOnUI(() => { 'worklet' - const rects = refs.map(ref => (ref ? measure(ref) : null)) + const rects = handles.map(measureHandle) runOnJS(_openLightbox)(index, rects, fetchedDims) })() }