Add useHandleRef as a lighter alternative for useAnimatedRef (#6500)

This commit is contained in:
dan
2024-11-18 22:21:47 +00:00
committed by GitHub
parent 4f0f9eb413
commit 7b6c182723
7 changed files with 75 additions and 54 deletions
+39
View File
@@ -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 <View ref={ref} />, 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<any>)
} else {
return null
}
}
+7 -11
View File
@@ -1,12 +1,6 @@
import React, {memo} from 'react' import React, {memo} from 'react'
import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native' import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native'
import Animated, { import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated'
measure,
MeasuredDimensions,
runOnJS,
runOnUI,
useAnimatedRef,
} from 'react-native-reanimated'
import {AppBskyActorDefs, ModerationDecision} from '@atproto/api' import {AppBskyActorDefs, ModerationDecision} from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
@@ -14,6 +8,7 @@ import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native' import {useNavigation} from '@react-navigation/native'
import {BACK_HITSLOP} from '#/lib/constants' import {BACK_HITSLOP} from '#/lib/constants'
import {measureHandle, useHandleRef} from '#/lib/hooks/useHandleRef'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {NavigationProp} from '#/lib/routes/types' import {NavigationProp} from '#/lib/routes/types'
import {isIOS} from '#/platform/detection' import {isIOS} from '#/platform/detection'
@@ -49,7 +44,7 @@ let ProfileHeaderShell = ({
const {openLightbox} = useLightboxControls() const {openLightbox} = useLightboxControls()
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
const {isDesktop} = useWebMediaQueries() const {isDesktop} = useWebMediaQueries()
const aviRef = useAnimatedRef() const aviRef = useHandleRef()
const onPressBack = React.useCallback(() => { const onPressBack = React.useCallback(() => {
if (navigation.canGoBack()) { if (navigation.canGoBack()) {
@@ -86,9 +81,10 @@ let ProfileHeaderShell = ({
const modui = moderation.ui('avatar') const modui = moderation.ui('avatar')
const avatar = profile.avatar const avatar = profile.avatar
if (avatar && !(modui.blur && modui.noOverride)) { if (avatar && !(modui.blur && modui.noOverride)) {
const aviHandle = aviRef.current
runOnUI(() => { runOnUI(() => {
'worklet' 'worklet'
const rect = measure(aviRef) const rect = measureHandle(aviHandle)
runOnJS(_openLightbox)(avatar, rect) runOnJS(_openLightbox)(avatar, rect)
})() })()
} }
@@ -170,14 +166,14 @@ let ProfileHeaderShell = ({
styles.avi, styles.avi,
profile.associated?.labeler && styles.aviLabeler, profile.associated?.labeler && styles.aviLabeler,
]}> ]}>
<Animated.View ref={aviRef} collapsable={false}> <View ref={aviRef} collapsable={false}>
<UserAvatar <UserAvatar
type={profile.associated?.labeler ? 'labeler' : 'user'} type={profile.associated?.labeler ? 'labeler' : 'user'}
size={90} size={90}
avatar={profile.avatar} avatar={profile.avatar}
moderation={moderation.ui('avatar')} moderation={moderation.ui('avatar')}
/> />
</Animated.View> </View>
</View> </View>
</TouchableWithoutFeedback> </TouchableWithoutFeedback>
</GrowableAvatar> </GrowableAvatar>
+7 -11
View File
@@ -1,18 +1,13 @@
import React from 'react' import React from 'react'
import {Pressable, StyleSheet, View} from 'react-native' import {Pressable, StyleSheet, View} from 'react-native'
import Animated, { import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated'
measure,
MeasuredDimensions,
runOnJS,
runOnUI,
useAnimatedRef,
} from 'react-native-reanimated'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native' import {useNavigation} from '@react-navigation/native'
import {BACK_HITSLOP} from '#/lib/constants' import {BACK_HITSLOP} from '#/lib/constants'
import {measureHandle, useHandleRef} from '#/lib/hooks/useHandleRef'
import {usePalette} from '#/lib/hooks/usePalette' import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
@@ -60,7 +55,7 @@ export function ProfileSubpageHeader({
const {openLightbox} = useLightboxControls() const {openLightbox} = useLightboxControls()
const pal = usePalette('default') const pal = usePalette('default')
const canGoBack = navigation.canGoBack() const canGoBack = navigation.canGoBack()
const aviRef = useAnimatedRef() const aviRef = useHandleRef()
const onPressBack = React.useCallback(() => { const onPressBack = React.useCallback(() => {
if (navigation.canGoBack()) { if (navigation.canGoBack()) {
@@ -101,9 +96,10 @@ export function ProfileSubpageHeader({
if ( if (
avatar // TODO && !(view.moderation.avatar.blur && view.moderation.avatar.noOverride) avatar // TODO && !(view.moderation.avatar.blur && view.moderation.avatar.noOverride)
) { ) {
const aviHandle = aviRef.current
runOnUI(() => { runOnUI(() => {
'worklet' 'worklet'
const rect = measure(aviRef) const rect = measureHandle(aviHandle)
runOnJS(_openLightbox)(avatar, rect) runOnJS(_openLightbox)(avatar, rect)
})() })()
} }
@@ -155,7 +151,7 @@ export function ProfileSubpageHeader({
paddingBottom: 6, paddingBottom: 6,
paddingHorizontal: isMobile ? 12 : 14, paddingHorizontal: isMobile ? 12 : 14,
}}> }}>
<Animated.View ref={aviRef} collapsable={false}> <View ref={aviRef} collapsable={false}>
<Pressable <Pressable
testID="headerAviButton" testID="headerAviButton"
onPress={onPressAvi} onPress={onPressAvi}
@@ -169,7 +165,7 @@ export function ProfileSubpageHeader({
<UserAvatar type={avatarType} size={58} avatar={avatar} /> <UserAvatar type={avatarType} size={58} avatar={avatar} />
)} )}
</Pressable> </Pressable>
</Animated.View> </View>
<View style={{flex: 1}}> <View style={{flex: 1}}>
{isLoading ? ( {isLoading ? (
<LoadingPlaceholder <LoadingPlaceholder
+5 -8
View File
@@ -1,11 +1,11 @@
import React, {useRef} from 'react' import React, {useRef} from 'react'
import {DimensionValue, Pressable, View} from 'react-native' import {DimensionValue, Pressable, View} from 'react-native'
import Animated, {AnimatedRef, useAnimatedRef} from 'react-native-reanimated'
import {Image} from 'expo-image' import {Image} from 'expo-image'
import {AppBskyEmbedImages} from '@atproto/api' import {AppBskyEmbedImages} from '@atproto/api'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {HandleRef, useHandleRef} from '#/lib/hooks/useHandleRef'
import type {Dimensions} from '#/lib/media/types' import type {Dimensions} from '#/lib/media/types'
import {isNative} from '#/platform/detection' import {isNative} from '#/platform/detection'
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
@@ -68,17 +68,14 @@ export function AutoSizedImage({
image: AppBskyEmbedImages.ViewImage image: AppBskyEmbedImages.ViewImage
crop?: 'none' | 'square' | 'constrained' crop?: 'none' | 'square' | 'constrained'
hideBadge?: boolean hideBadge?: boolean
onPress?: ( onPress?: (containerRef: HandleRef, fetchedDims: Dimensions | null) => void
containerRef: AnimatedRef<React.Component<{}, {}, any>>,
fetchedDims: Dimensions | null,
) => void
onLongPress?: () => void onLongPress?: () => void
onPressIn?: () => void onPressIn?: () => void
}) { }) {
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const largeAlt = useLargeAltBadgeEnabled() const largeAlt = useLargeAltBadgeEnabled()
const containerRef = useAnimatedRef() const containerRef = useHandleRef()
const fetchedDimsRef = useRef<{width: number; height: number} | null>(null) const fetchedDimsRef = useRef<{width: number; height: number} | null>(null)
let aspectRatio: number | undefined let aspectRatio: number | undefined
@@ -109,7 +106,7 @@ export function AutoSizedImage({
const hasAlt = !!image.alt const hasAlt = !!image.alt
const contents = ( const contents = (
<Animated.View ref={containerRef} collapsable={false} style={{flex: 1}}> <View ref={containerRef} collapsable={false} style={{flex: 1}}>
<Image <Image
style={[a.w_full, a.h_full]} style={[a.w_full, a.h_full]}
source={image.thumb} source={image.thumb}
@@ -188,7 +185,7 @@ export function AutoSizedImage({
)} )}
</View> </View>
) : null} ) : null}
</Animated.View> </View>
) )
if (cropDisabled) { if (cropDisabled) {
+5 -8
View File
@@ -1,11 +1,11 @@
import React from 'react' import React from 'react'
import {Pressable, StyleProp, View, ViewStyle} from 'react-native' import {Pressable, StyleProp, View, ViewStyle} from 'react-native'
import Animated, {AnimatedRef} from 'react-native-reanimated'
import {Image, ImageStyle} from 'expo-image' import {Image, ImageStyle} from 'expo-image'
import {AppBskyEmbedImages} from '@atproto/api' import {AppBskyEmbedImages} from '@atproto/api'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {HandleRef} from '#/lib/hooks/useHandleRef'
import {Dimensions} from '#/lib/media/types' import {Dimensions} from '#/lib/media/types'
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
import {PostEmbedViewContext} from '#/view/com/util/post-embeds/types' import {PostEmbedViewContext} from '#/view/com/util/post-embeds/types'
@@ -20,7 +20,7 @@ interface Props {
index: number index: number
onPress?: ( onPress?: (
index: number, index: number,
containerRefs: AnimatedRef<React.Component<{}, {}, any>>[], containerRefs: HandleRef[],
fetchedDims: (Dimensions | null)[], fetchedDims: (Dimensions | null)[],
) => void ) => void
onLongPress?: EventFunction onLongPress?: EventFunction
@@ -28,7 +28,7 @@ interface Props {
imageStyle?: StyleProp<ImageStyle> imageStyle?: StyleProp<ImageStyle>
viewContext?: PostEmbedViewContext viewContext?: PostEmbedViewContext
insetBorderStyle?: StyleProp<ViewStyle> insetBorderStyle?: StyleProp<ViewStyle>
containerRefs: AnimatedRef<React.Component<{}, {}, any>>[] containerRefs: HandleRef[]
thumbDimsRef: React.MutableRefObject<(Dimensions | null)[]> thumbDimsRef: React.MutableRefObject<(Dimensions | null)[]>
} }
@@ -52,10 +52,7 @@ export function GalleryItem({
const hideBadges = const hideBadges =
viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia
return ( return (
<Animated.View <View style={a.flex_1} ref={containerRefs[index]} collapsable={false}>
style={a.flex_1}
ref={containerRefs[index]}
collapsable={false}>
<Pressable <Pressable
onPress={ onPress={
onPress onPress
@@ -118,6 +115,6 @@ export function GalleryItem({
</Text> </Text>
</View> </View>
) : null} ) : null}
</Animated.View> </View>
) )
} }
+7 -7
View File
@@ -1,8 +1,8 @@
import React from 'react' import React from 'react'
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {AnimatedRef, useAnimatedRef} from 'react-native-reanimated'
import {AppBskyEmbedImages} from '@atproto/api' import {AppBskyEmbedImages} from '@atproto/api'
import {HandleRef, useHandleRef} from '#/lib/hooks/useHandleRef'
import {PostEmbedViewContext} from '#/view/com/util/post-embeds/types' import {PostEmbedViewContext} from '#/view/com/util/post-embeds/types'
import {atoms as a, useBreakpoints} from '#/alf' import {atoms as a, useBreakpoints} from '#/alf'
import {Dimensions} from '../../lightbox/ImageViewing/@types' import {Dimensions} from '../../lightbox/ImageViewing/@types'
@@ -12,7 +12,7 @@ interface ImageLayoutGridProps {
images: AppBskyEmbedImages.ViewImage[] images: AppBskyEmbedImages.ViewImage[]
onPress?: ( onPress?: (
index: number, index: number,
containerRefs: AnimatedRef<React.Component<{}, {}, any>>[], containerRefs: HandleRef[],
fetchedDims: (Dimensions | null)[], fetchedDims: (Dimensions | null)[],
) => void ) => void
onLongPress?: (index: number) => void onLongPress?: (index: number) => void
@@ -43,7 +43,7 @@ interface ImageLayoutGridInnerProps {
images: AppBskyEmbedImages.ViewImage[] images: AppBskyEmbedImages.ViewImage[]
onPress?: ( onPress?: (
index: number, index: number,
containerRefs: AnimatedRef<React.Component<{}, {}, any>>[], containerRefs: HandleRef[],
fetchedDims: (Dimensions | null)[], fetchedDims: (Dimensions | null)[],
) => void ) => void
onLongPress?: (index: number) => void onLongPress?: (index: number) => void
@@ -56,10 +56,10 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
const gap = props.gap const gap = props.gap
const count = props.images.length const count = props.images.length
const containerRef1 = useAnimatedRef() const containerRef1 = useHandleRef()
const containerRef2 = useAnimatedRef() const containerRef2 = useHandleRef()
const containerRef3 = useAnimatedRef() const containerRef3 = useHandleRef()
const containerRef4 = useAnimatedRef() const containerRef4 = useHandleRef()
const thumbDimsRef = React.useRef<(Dimensions | null)[]>([]) const thumbDimsRef = React.useRef<(Dimensions | null)[]>([])
switch (count) { switch (count) {
+5 -9
View File
@@ -6,13 +6,7 @@ import {
View, View,
ViewStyle, ViewStyle,
} from 'react-native' } from 'react-native'
import { import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated'
AnimatedRef,
measure,
MeasuredDimensions,
runOnJS,
runOnUI,
} from 'react-native-reanimated'
import {Image} from 'expo-image' import {Image} from 'expo-image'
import { import {
AppBskyEmbedExternal, AppBskyEmbedExternal,
@@ -27,6 +21,7 @@ import {
ModerationDecision, ModerationDecision,
} from '@atproto/api' } from '@atproto/api'
import {HandleRef, measureHandle} from '#/lib/hooks/useHandleRef'
import {usePalette} from '#/lib/hooks/usePalette' import {usePalette} from '#/lib/hooks/usePalette'
import {useLightboxControls} from '#/state/lightbox' import {useLightboxControls} from '#/state/lightbox'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
@@ -163,12 +158,13 @@ export function PostEmbeds({
} }
const onPress = ( const onPress = (
index: number, index: number,
refs: AnimatedRef<React.Component<{}, {}, any>>[], refs: HandleRef[],
fetchedDims: (Dimensions | null)[], fetchedDims: (Dimensions | null)[],
) => { ) => {
const handles = refs.map(r => r.current)
runOnUI(() => { runOnUI(() => {
'worklet' 'worklet'
const rects = refs.map(ref => (ref ? measure(ref) : null)) const rects = handles.map(measureHandle)
runOnJS(_openLightbox)(index, rects, fetchedDims) runOnJS(_openLightbox)(index, rects, fetchedDims)
})() })()
} }