reactjsreact-nativelayoutmeasure

React Native measureInWindow always return 0 for all properties


I have the following hook:

export default function useOverlay(action: Action, cutoutSize: number) {

    const anchor = useRef<View>(null)
    const { width, height } = Dimensions.get("window");

    const [overlay, setOverlay] = useReducer(
        mergeObjReducer<ScanOverlayProps>,
        {
            overlayWidth: width,
            overlayHeight: height,
            cutoutOffsetX: 32,
            cutoutOffsetY: 0,
            cutoutSize,
            overlayColor: "#151515",
            cutoutVisible: true
        }
    )

    useLayoutEffect(() => {

        anchor.current?.measureInWindow((x, y, w, h) => {
            console.log("LAYOUT", x, y, w, h) //ERROR all values are always 0.
            setOverlay({ cutoutOffsetY: y })
        });
    }, [])

    useEffect(() => {

        const isScan = (action === "scan");

        setOverlay({
            cutoutVisible: (isScan && overlay.cutoutOffsetY !== 0),
            overlayColor: isScan ? "#151515d1" : "#151515"
        })
    }, [action, overlay.cutoutOffsetY])


    return { anchor, overlay }
}

Used in the following component like so:

export type Action = "scan" | "qr";

export default function Actions() {

    const isActive = (name: Action) => action === name;

    const { scanner, data } = useCodeScanner()
    const [action, setAction] = useState<Action>("qr");
    const { anchor, overlay } = useOverlay(action, 240);
    const active = useCameraActive(action, 50);
    useActionBrightness(action);
    useScanAction(data);


    return (
        <View style={styles.container}>
            {active && <CameraView
                // onError={handleCameraError}
                isActive
                codeScanner={scanner}
                style={styles.camera}
            />}
            <Overlay {...overlay} />

            <View style={styles.svgContent}>
                <View ref={anchor} style={styles.actionContent}>
                    {isActive("qr") && <DynamicQRCode size={240} data={{ actionType: "person" }} />}
                </View>
            </View>
        </View>
    )
}

Yet, whenever I log the value of any of the measure callback parameters, I always get 0. WHY? I have tried everything, delays, interactionsManager, passing a onlayout function with a ref etc. Nothing gives me the actual screen position. How do I fix this? I have been going mad for the past 4 hours....

FYI (not sure if relevant though), this screen is located in a TopTabsNavigator from react native navigation.

Let me know if any more information would be useful.


Solution

  • Update Overlay Hook as:

    import { useRef, useReducer, useEffect, useState, useCallback } from "react";
    import { Dimensions, View, LayoutChangeEvent, ViewStyle } from "react-native";
    import { useFocusEffect } from "@react-navigation/native";
    
    export default function useOverlay(action: Action, cutoutSize: number) {
      const anchor = useRef<View>(null);
      const layoutReady = useRef(false);
      const [screenFocused, setScreenFocused] = useState(false);
    
      const { width, height } = Dimensions.get("window");
    
      const [overlay, setOverlay] = useReducer(mergeObjReducer<ScanOverlayProps>, {
        overlayWidth: width,
        overlayHeight: height,
        cutoutOffsetX: 32,
        cutoutOffsetY: 0,
        cutoutSize,
        overlayColor: "#151515",
        cutoutVisible: true,
      });
    
      const onLayout = useCallback((e: LayoutChangeEvent) => {
        layoutReady.current = true;
        tryMeasure();
      }, []);
    
      useFocusEffect(
        useCallback(() => {
          setScreenFocused(true);
          tryMeasure();
          return () => setScreenFocused(false);
        }, [])
      );
    
      const tryMeasure = () => {
        if (layoutReady.current && screenFocused) {
          anchor.current?.measureInWindow((x, y, w, h) => {
            console.log("MEASURED", x, y, w, h);
            if (y !== 0) {
              setOverlay({ cutoutOffsetY: y });
            }
          });
        }
      };
    
      useEffect(() => {
        const isScan = action === "scan";
        setOverlay({
          cutoutVisible: isScan && overlay.cutoutOffsetY !== 0,
          overlayColor: isScan ? "#151515d1" : "#151515",
        });
      }, [action, overlay.cutoutOffsetY]);
    
      return { anchor, overlay, onLayout };
    }
    

    And also Actions component:

    export default function Actions() {
      const [action, setAction] = useState<Action>("qr");
      const { scanner, data } = useCodeScanner();
      const { anchor, overlay, onLayout } = useOverlay(action, 240);
      const active = useCameraActive(action, 50);
      useActionBrightness(action);
      useScanAction(data);
    
      const isActive = (name: Action) => action === name;
    
      return (
        <View style={styles.container}>
          {active && (
            <CameraView
              isActive
              codeScanner={scanner}
              style={styles.camera}
            />
          )}
          <Overlay {...overlay} />
    
          <View style={styles.svgContent}>
            <View ref={anchor} onLayout={onLayout} style={styles.actionContent}>
              {isActive("qr") && (
                <DynamicQRCode size={240} data={{ actionType: "person" }} />
              )}
            </View>
          </View>
        </View>
      );
    }