diff --git a/packages/docs-gesture-handler/src/examples/GestureStateFlowExample/ChartItem.tsx b/packages/docs-gesture-handler/src/examples/GestureStateFlowExample/ChartItem.tsx index 9a47906a61..fbdfc4a199 100644 --- a/packages/docs-gesture-handler/src/examples/GestureStateFlowExample/ChartItem.tsx +++ b/packages/docs-gesture-handler/src/examples/GestureStateFlowExample/ChartItem.tsx @@ -1,6 +1,6 @@ import { Grid } from '@mui/material'; -import React, { LegacyRef, useEffect } from 'react'; -import { StyleProp, StyleSheet, View, Text } from 'react-native'; +import { useEffect, useLayoutEffect, useRef } from 'react'; +import { StyleProp, StyleSheet, Text, View, ViewStyle } from 'react-native'; import ChartManager, { Item, WAVE_DELAY_MS } from './ChartManager'; import Animated, { useAnimatedStyle, @@ -8,23 +8,32 @@ import Animated, { withSpring, } from 'react-native-reanimated'; +export type Coordinate = { + x: number; + y: number; +}; + interface ChartItemProps { item: Item; chartManager: ChartManager; - innerRef?: LegacyRef; - style?: StyleProp; + updateCoordinates?: (id: number, coordinate: Coordinate) => void; + style?: StyleProp; } +const getCenter = (side: number, size: number) => side + size / 2; + export default function ChartItem({ item, chartManager, - innerRef, + updateCoordinates, style, }: ChartItemProps) { + const ref = useRef(null); + const progress = useSharedValue(0); useEffect(() => { - if (item.id != ChartManager.EMPTY_SPACE_ID) { + if (item.id !== ChartManager.EMPTY_SPACE_ID) { const listenerId = chartManager.addListener(item.id, (isActive) => { progress.value = withSpring(isActive ? 1 : 0, { duration: 2 * WAVE_DELAY_MS, @@ -35,7 +44,7 @@ export default function ChartItem({ chartManager.removeListener(item.id, listenerId); }; } - }, [chartManager]); + }, [chartManager, item.id, progress]); const animatedStyle = useAnimatedStyle(() => { return { @@ -56,8 +65,23 @@ export default function ChartItem({ }; }); + useLayoutEffect(() => { + const box = ( + ref.current as unknown as HTMLElement + )?.getBoundingClientRect?.(); + + if (!box) { + return; // no-op on undefined view ref + } + + updateCoordinates(item.id, { + x: getCenter(box.left, box.width), + y: getCenter(box.top, box.height), + }); + }, [item, updateCoordinates]); + return ( - + + ref={ref}> {item.label} diff --git a/packages/docs-gesture-handler/src/examples/GestureStateFlowExample/ChartManager.ts b/packages/docs-gesture-handler/src/examples/GestureStateFlowExample/ChartManager.ts index 5c9e791b4e..49e8e9905d 100644 --- a/packages/docs-gesture-handler/src/examples/GestureStateFlowExample/ChartManager.ts +++ b/packages/docs-gesture-handler/src/examples/GestureStateFlowExample/ChartManager.ts @@ -1,4 +1,3 @@ -import { useMemo } from 'react'; import { TapGesture, PanGesture, @@ -92,7 +91,7 @@ export default class ChartManager { private _connections: ChartConnection[] = []; private _layout: number[][]; private _listeners: Map void>> = - useMemo(() => new Map(), []); + new Map(); public static EMPTY_SPACE_ID = 0; @@ -120,7 +119,7 @@ export default class ChartManager { itemId: number, listener: (isActive: boolean) => void ): number { - const listenerId = this._listeners.get(itemId)?.size - 1 ?? 0; + const listenerId = this._listeners.get(itemId)?.size - 1; // another map is used inside of _listeners to seamlessly remove listening functions from _listeners if (this._listeners.has(itemId)) { @@ -151,7 +150,7 @@ export default class ChartManager { label = stateToName.get(label); } - let highlightColor = labelColorMap.get(label) ?? Colors.YELLOW; + const highlightColor = labelColorMap.get(label) ?? Colors.YELLOW; const newItem = { id: newId, @@ -205,12 +204,12 @@ export default class ChartManager { undeterminedCallback(true); - const resetAllStates = (event: GestureStateChangeEvent) => { + const resetAllStates = (event: GestureStateChangeEvent) => { undeterminedCallback(true); - if (event.state == State.FAILED) { + if (event.state === State.FAILED) { failedCallback(true); } - if (event.state == State.CANCELLED) { + if (event.state === State.CANCELLED) { cancelledCallback(true); } setTimeout(() => { @@ -236,7 +235,7 @@ export default class ChartManager { .onEnd(() => { endCallback(true); }) - .onFinalize((event: GestureStateChangeEvent) => { + .onFinalize((event: GestureStateChangeEvent) => { resetAllStates(event); }); diff --git a/packages/docs-gesture-handler/src/examples/GestureStateFlowExample/FlowChart.tsx b/packages/docs-gesture-handler/src/examples/GestureStateFlowExample/FlowChart.tsx index 45b0c7ec27..f945559daf 100644 --- a/packages/docs-gesture-handler/src/examples/GestureStateFlowExample/FlowChart.tsx +++ b/packages/docs-gesture-handler/src/examples/GestureStateFlowExample/FlowChart.tsx @@ -1,66 +1,49 @@ -import 'react-native-gesture-handler'; -import React, { useEffect, useRef, useState } from 'react'; +import { useCallback, useMemo, useReducer, useRef } from 'react'; import { StyleSheet, View } from 'react-native'; import ChartManager from './ChartManager'; import { Grid } from '@mui/material'; -import ChartItem from './ChartItem'; +import ChartItem, { Coordinate } from './ChartItem'; import Arrow from './Arrow'; -type Coordinate = { - x: number; - y: number; -}; - type FlowChartProps = { chartManager: ChartManager; }; export default function FlowChart({ chartManager }: FlowChartProps) { - const itemsRef = useRef([]); - const itemsCoordsRef = useRef([]); - const rootRef = useRef(null); - - // there's a bug where arrows are not shown on the first render on production build - // i hate this but it forces a re-render after the component is mounted - // a man's gotta do what a man's gotta do - const [counter, setCounter] = useState(0); - useEffect(() => { - const timeout = setTimeout(() => { - setCounter(counter + 1); - }, 0); - return () => clearTimeout(timeout); - }, []); + const [, forceUpdate] = useReducer((x: number) => x + 1, 0); + const coordinates = useMemo>(() => new Map(), []); + const rootRef = useRef(null); - const getCenter = (side: number, size: number) => side + size / 2; + const updateCoordinates = useCallback( + (id: number, coordinate: Coordinate) => { + const htmlRootElement = rootRef.current as unknown as HTMLElement; + const root = htmlRootElement.getBoundingClientRect(); - itemsCoordsRef.current = itemsRef.current.map((element) => { - // during unloading or overresizing, item may reload itself, causing it to be undefined - if (!element) { - return { - x: 0, - y: 0, - } as Coordinate; - } + if (!root) { + return; + } - const box = element.getBoundingClientRect(); - const root = rootRef.current.getBoundingClientRect(); - return { - x: getCenter(box.left, box.width) - root.left, - y: getCenter(box.top, box.height) - root.top, - } as Coordinate; - }); + // Adjust to root relative positioning + coordinates.set(id, { + x: coordinate.x - root.left, + y: coordinate.y - root.top, + }); + forceUpdate(); + }, + [coordinates] + ); return ( - {chartManager.layout.map((row, index) => ( - + {chartManager.layout?.map((row) => ( + {row .map((itemId) => chartManager.items[itemId]) - .map((item, index) => ( + .map((item) => ( (itemsRef.current[item.id] = el)} + key={item.id} + updateCoordinates={updateCoordinates} item={item} chartManager={chartManager} /> @@ -72,21 +55,22 @@ export default function FlowChart({ chartManager }: FlowChartProps) { // we have all the connections layed out, // but the user may choose not to use some of the available items, if ( - !itemsCoordsRef.current[connection.from] || - !itemsCoordsRef.current[connection.to] + !coordinates.get(connection.from) || + !coordinates.get(connection.to) ) { return ; } + return ( ); diff --git a/packages/docs-gesture-handler/src/examples/GestureStateFlowExample/index.tsx b/packages/docs-gesture-handler/src/examples/GestureStateFlowExample/index.tsx index 902e59117e..4f3c42e3d1 100644 --- a/packages/docs-gesture-handler/src/examples/GestureStateFlowExample/index.tsx +++ b/packages/docs-gesture-handler/src/examples/GestureStateFlowExample/index.tsx @@ -1,5 +1,4 @@ -import 'react-native-gesture-handler'; -import React, { useEffect, useMemo, useRef } from 'react'; +import { useEffect, useMemo, useReducer } from 'react'; import { StyleSheet, View, useWindowDimensions, Text } from 'react-native'; import Animated, { useAnimatedStyle, @@ -19,49 +18,59 @@ import FlowChart from './FlowChart'; const MIN_DESKTOP_WIDTH = 1298; export default function App() { - const chartManager = useRef(new ChartManager()); + const [, forceUpdate] = useReducer((x: number) => x + 1, 0); + const chartManager = useMemo(() => new ChartManager(), []); const [panHandle, capturedPan, resetPan] = useMemo( - () => chartManager.current.newGesture(Gesture.Pan()), - [] + () => chartManager.newGesture(Gesture.Pan()), + [chartManager] ); const [pressHandle, capturedPress, resetLongPress] = useMemo( - () => chartManager.current.newGesture(Gesture.LongPress()), - [] + () => chartManager.newGesture(Gesture.LongPress()), + [chartManager] ); - useEffect(() => { - resetPan(); - resetLongPress(); - }, []); - - const panIds = panHandle.idObject; - const pressIds = pressHandle.idObject; - const dimensions = useWindowDimensions(); const isDesktopMode = dimensions.width > MIN_DESKTOP_WIDTH; - // prettier-ignore - const desktopLayout = [ - [panIds.undetermined, ChartManager.EMPTY_SPACE_ID, pressIds.undetermined, ChartManager.EMPTY_SPACE_ID], - [panIds.began, panIds.failed, pressIds.began, pressIds.failed], - [panIds.active, panIds.cancelled, pressIds.active, pressIds.cancelled], - [panIds.end, ChartManager.EMPTY_SPACE_ID, pressIds.end, ChartManager.EMPTY_SPACE_ID], - ]; + useEffect(() => { + // Timing issue, neither useEffect, useLayoutEffect or requestAnimationFrame work + const timeout = setTimeout(() => { + resetPan(); + resetLongPress(); + }, 300); - // prettier-ignore - const phoneLayout = [ - [panIds.undetermined], - [panIds.began, panIds.failed], - [panIds.active, panIds.cancelled], - [panIds.end, ChartManager.EMPTY_SPACE_ID], - ]; + return () => { + clearTimeout(timeout); + }; + }, [resetLongPress, resetPan]); - chartManager.current.layout = isDesktopMode ? desktopLayout : phoneLayout; + useEffect(() => { + const panIds = panHandle.idObject; + const pressIds = pressHandle.idObject; + + // prettier-ignore + const desktopLayout = [ + [panIds.undetermined, ChartManager.EMPTY_SPACE_ID, pressIds.undetermined, ChartManager.EMPTY_SPACE_ID], + [panIds.began, panIds.failed, pressIds.began, pressIds.failed], + [panIds.active, panIds.cancelled, pressIds.active, pressIds.cancelled], + [panIds.end, ChartManager.EMPTY_SPACE_ID, pressIds.end, ChartManager.EMPTY_SPACE_ID], + ]; + + // prettier-ignore + const phoneLayout = [ + [panIds.undetermined], + [panIds.began, panIds.failed], + [panIds.active, panIds.cancelled], + [panIds.end, ChartManager.EMPTY_SPACE_ID], + ]; + + chartManager.layout = isDesktopMode ? desktopLayout : phoneLayout; + forceUpdate(); + }, [chartManager, isDesktopMode, panHandle, pressHandle]); const pressed = useSharedValue(false); - const offset = useSharedValue(0); const scale = useSharedValue(1); @@ -110,7 +119,7 @@ export default function App() { Gesture.LongPress() )} - +