Search code examples
reactjsreact-nativeautofocusexpo-camera

How to implement tap on focus in react natie using expo camera?


I am using expo camera and I have wrapped it inside TapGestureHandler so I can detect tap event. Here is the code:

<TapGestureHandler onHandlerStateChange={onSingleTapEvent}>
  <View>
    <Camera
      ref={cameraRef}
      type={cameraType}
      ratio={ratio}
      onCameraReady={onCameraReady}
      autoFocus={Camera.Constants.AutoFocus.on}
    ></Camera>
  </View>
</TapGestureHandler>;

My onSingleTapEvent:

 const onSingleTapEvent = (event) => {
    if (event.nativeEvent.state === State.ACTIVE) {
      console.log("Single tap event: ", event.nativeEvent);
    }
  };

When I tap on the screen I see the following console output:

    Single tap event:  Object {
  "absoluteX": 210.3333282470703,
  "absoluteY": 527.3333129882812,
  "handlerTag": 3,
  "numberOfPointers": 1,
  "oldState": 2,
  "state": 4,
  "x": 210.3333282470703,
  "y": 446.3333435058594,
}

My question is, how do I implement tap on focus? As far as I understand I need to play with focusDepth property of expo camera, but I don't know how to set it? Any ideas, bogs or pseudo code would be grant!


Solution

  • Creating a useAutofocus-Hook:

    A nice way to solve this is by creating a custom useAutofocus-Hook.

        import { useEffect, useState } from 'react';
        import {
          type GestureStateChangeEvent,
          type TapGestureHandlerEventPayload,
        } from 'react-native-gesture-handler';
        
        export const useAutofocus = () => {
          const [isRefreshing, setIsRefreshing] = useState<boolean>(false);
        
          useEffect(() => {
            if (isRefreshing) {
              setIsRefreshing(false);
            }
          }, [isRefreshing]);
        
          const onTap = (_event: GestureStateChangeEvent<TapGestureHandlerEventPayload>): void => {
            setIsRefreshing(true);
          };
        
          return { isRefreshing, onTap };
        };
    

    With this hook you can conveniently react to tap gestures and use the mechanism that Louis21 described in his answer to perform a refocus on tap.

    Example Implementation using GestureDetector

        import { Camera, AutoFocus } from 'expo-camera';
        import React from 'react';
        import { Gesture, GestureDetector } from 'react-native-gesture-handler';
        import { StyleSheet, View } from 'react-native';
        import { useAutofocus } from '@/hooks/useAutofocus';
        
        export default function CameraModalScreen() {
          const { isRefreshing, onTap } = useAutofocus();
        
          const tap = Gesture.Tap().onBegin(onTap);
        
          return (
            <GestureDetector gesture={tap}>
              <View style={styles.container}>
                <Camera style={styles.camera} autoFocus={isRefreshing ? AutoFocus.off : AutoFocus.on} />
              </View>
            </GestureDetector>
          );
        }
        
        const styles = StyleSheet.create({
          container: {
            flex: 1,
            justifyContent: 'center',
          },
          camera: {
            flex: 1,
          },
          contentContainer: {
            flex: 1,
            marginBottom: 64,
          },
        });
    

    Providing visual Feedback to user

    I also really like Louis solution to providing visual feedback for the tap. If you want to implement this with my solution you could expand the Hook like this

    import { useEffect, useState } from 'react';
    import {
      type GestureStateChangeEvent,
      type TapGestureHandlerEventPayload,
    } from 'react-native-gesture-handler';
    
    type FocusSquare = {
      visible: boolean;
      x: number;
      y: number;
    };
    
    export const useAutofocus = () => {
      const [isRefreshing, setIsRefreshing] = useState<boolean>(false);
      const [focusSquare, setFocusSquare] = useState<FocusSquare>({ visible: false, x: 0, y: 0 });
    
      useEffect(() => {
        if (isRefreshing) {
          setIsRefreshing(false);
        }
      }, [isRefreshing]);
    
      const onTap = (event: GestureStateChangeEvent<TapGestureHandlerEventPayload>): void => {
        const { x, y } = event;
        setIsRefreshing(true);
        setFocusSquare({ visible: true, x, y });
    
        // Hide the square after 400 millliseconds.
        setTimeout(() => {
          setFocusSquare((prevState) => ({ ...prevState, visible: false }));
        }, 400);
      };
    
      return { isRefreshing, focusSquare, onTap };
    };
    

    Inside of the component it would look really similar to Louis23's implementation

    import { Camera, AutoFocus } from 'expo-camera';
    import React from 'react';
    import { Gesture, GestureDetector } from 'react-native-gesture-handler';
    import { StyleSheet, View } from 'react-native';
    import { useAutofocus } from '@/hooks/useAutofocus';
    
    export default function CameraModalScreen() {
      const { isRefreshing, focusSquare, onTap } = useAutofocus();
      console.log(focusSquare);
      const tap = Gesture.Tap().onBegin(onTap);
    
      return (
        <GestureDetector gesture={tap}>
          <View style={styles.container}>
            <Camera style={styles.camera} autoFocus={isRefreshing ? AutoFocus.off : AutoFocus.on} />
            {focusSquare.visible && (
              <View
                style={[styles.focusSquare, { top: focusSquare.y - 25, left: focusSquare.x - 25 }]}
              />
            )}
          </View>
        </GestureDetector>
      );
    }
    
    const styles = StyleSheet.create({
      container: {
        flex: 1,
        justifyContent: 'center',
      },
      camera: {
        flex: 1,
      },
      contentContainer: {
        flex: 1,
        marginBottom: 64,
      },
      focusSquare: {
        position: 'absolute',
        width: 50,
        height: 50,
        borderWidth: 2,
        borderColor: 'white',
      },
    });