Petnow LogoPetnow

Customization

How to layer your own RN UI on top of CameraView to build guide, button, and result screens.

Overview

<CameraView> renders the camera preview and the SDK detection overlay (nose/face markers, progress ring) natively. On top of it, you build the app UI — such as guide messages, buttons, progress, and result screens — yourself by overlaying ordinary RN views with absolute positioning. The UI is driven by the onDetectionStatus / onDetectionProgress / onDetectionFinished events and the camera commands.

The detection markers (nose/face) are drawn by the SDK itself. In addition, the detection rect (nose/face normalized box) is exposed to JS via onDetectionResult, so you can also build custom guides in RN that follow the marker positions (example below).

Basic Pattern

Lay <CameraView> over the screen and overlay an overlay container on top of it. Use pointerEvents="box-none" so that touches pass through to the camera area as well.

import { View, StyleSheet } from 'react-native';

<View style={{ flex: 1 }}>
  <CameraView
    camera={camera}
    species="DOG"
    purpose="PET_PROFILE_REGISTRATION"
    captureSessionId={captureSessionId}
    style={StyleSheet.absoluteFill}
    onDetectionStatus={setStatus}
    onDetectionProgress={setProgress}
    onDetectionFinished={onFinished}
  />

  {/* App UI overlay */}
  <SafeAreaView style={StyleSheet.absoluteFill} pointerEvents="box-none">
    {/* ...guide / buttons / progress... */}
  </SafeAreaView>
</View>

Example: Top Buttons (Switch / Close)

<View style={styles.topRow} pointerEvents="box-none">
  <Pressable onPress={() => camera.switchCamera()}>
    <Text style={styles.btn}>Switch</Text>
  </Pressable>
  <Pressable onPress={onClose}>
    <Text style={styles.btn}>Close</Text>
  </Pressable>
</View>

Example: Status-Based Guide Messages

Receive onDetectionStatus as state and render user guidance text.

const [status, setStatus] = useState<DetectionStatus | null>(null);

function guideMessage(s: DetectionStatus | null): string {
  if (!s) return 'Preparing camera...';
  switch (s.type) {
    case 'noObject':   return 'Center your pet on the screen';
    case 'processing': return 'Recognizing...';
    case 'detected':   return 'Great! Hold still';
    case 'finished':   return 'Capture complete!';
    case 'failed':     return `Try again: ${s.reason}`;
  }
}

<Text style={styles.guideCapsule}>{guideMessage(status)}</Text>

Example: Progress

const [progress, setProgress] = useState(0); // onDetectionProgress (0~100)

<View style={styles.progressTrack}>
  <View style={[styles.progressFill, { width: `${progress}%` }]} />
</View>

Example: Result Screen and Retake

When you receive completion via onDetectionFinished, show the result/upload UI, and restart with camera.startDetection() on "Retake".

const [result, setResult] = useState<CameraResult | null>(null);

const onFinished = useCallback((r: CameraResult) => {
  setResult(r);
  upload(r); // upload the file:// URIs to the app server
}, []);

const retake = useCallback(() => {
  setResult(null);
  setStatus(null);
  setProgress(0);
  camera.startDetection();
}, [camera]);

{result && (
  <View style={styles.resultSheet}>
    <Text>Capture complete — {result.fingerprintImages.length} fingerprint images</Text>
    <Pressable onPress={retake}><Text>Retake</Text></Pressable>
  </View>
)}

Example: Detection Rect Tracking Overlay

With onDetectionResult, you receive the nose/face box (normalized 0–1, top-left origin), and you can draw a custom marker that follows that position yourself. Obtain the view size with onLayout to convert the normalized coordinates to pixels.

import { useState } from 'react';
import { View, StyleSheet } from 'react-native';
import type { DetectionResult } from '@petnow/react-native-camera-ui';

const [rect, setRect] = useState<DetectionResult['nose']>(null);
const [size, setSize] = useState({ w: 0, h: 0 });

<View
  style={{ flex: 1 }}
  onLayout={(e) =>
    setSize({ w: e.nativeEvent.layout.width, h: e.nativeEvent.layout.height })
  }
>
  <CameraView
    camera={camera}
    species="DOG"
    purpose="PET_PROFILE_REGISTRATION"
    captureSessionId={captureSessionId}
    style={StyleSheet.absoluteFill}
    onDetectionResult={(r: DetectionResult) => setRect(r.nose ?? r.face)}
  />
  {rect && (
    <View
      pointerEvents="none"
      style={{
        position: 'absolute',
        left: rect.x * size.w,
        top: rect.y * size.h,
        width: rect.width * size.w,
        height: rect.height * size.h,
        borderWidth: 2,
        borderColor: '#FF592C',
        borderRadius: 8,
      }}
    />
  )}
</View>
  • nose/face are each { x, y, width, height } (normalized 0–1) or null. They can be null on frames with no detection, so guard for it.
  • Since it is drawn separately from the SDK's built-in markers, you can omit this overlay if the built-in markers are sufficient.

Tips

  • Use pointerEvents="box-none" on the overlay container and areas that don't need taps, and keep default behavior only on interactive elements like buttons.
  • If you need to pause (e.g., showing a sheet/popup), you can call camera.pauseDetection() and then resume with camera.resumeDetection() while preserving progress.
  • Feel free to change the guide text and failure-reason mapping to match your app's tone.

Next Steps

On this page