Scan Overlay
Render the thermal face-mesh scan glow with CircadifyOverlayView, drive it from onScanFace, and retheme it with CircadifyScanStyle.
The Android SDK ships CircadifyOverlayView — the thermal face-mesh scan glow that makes a Circadify scan recognizable. It paints a heat glow onto the live face mesh and adds a stationary gold guide oval, tracking the subject and pulsing hotter while measuring. The SDK stays headless: you own the layout, the preview, and the buttons, and drop the overlay on top.
The geometry that drives it — CircadifyScanFace — arrives on the onScanFace callback, so you can either mount the bundled overlay as-is or paint your own glow from the same projected anchors.
CircadifyOverlayView already renders the thermal/heat-glow face overlay for you. Mount it instead of hand-rolling an overlay from the raw onLandmarks callback. Only reach for a custom overlay when you genuinely need one — and then drive it from onScanFace, which gives you the same projected geometry the bundled view uses.
What the glow looks like
- The full 468-point / 880-triangle MediaPipe face mesh.
- A thermal ramp (gold → orange → red core) that sweeps region-by-region across the forehead and cheeks, computed in 3D so it conforms to the face.
- A stationary gold guide oval ("hold still inside the oval") shown under the glow. The glow itself only appears once a face is detected — no resting placeholder.
- A subtler, slower pulse while framing; a brighter, faster pulse during a measurement.
The heat-glow is rendered on Android with Canvas.drawVertices.
Adding the overlay
CircadifyOverlayView is a plain View. Put it on top of your CameraX PreviewView with identical bounds — the simplest way is a FrameLayout with both children set to MATCH_PARENT. Use PreviewView.ScaleType.FILL_CENTER so the preview matches the overlay's aspect-fill projection.
-
Stack the
PreviewViewand aCircadifyOverlayViewin aFrameLayout, overlay last (on top), bothMATCH_PARENT. -
Feed the overlay live geometry from the
onScanFacecallback — assignfacestraight tooverlay.scanFace(it isnullwhen no face is detected). -
Flag the measurement: set
overlay.isScanning = truewhilemeasureVitals()runs for the hotter, faster glow, and reset when it finishes.
import androidx.camera.view.PreviewView
import com.circadify.sdk.CircadifyCallbacks
import com.circadify.sdk.CircadifyConfig
import com.circadify.sdk.CircadifyOverlayView
val previewView = PreviewView(this).apply {
scaleType = PreviewView.ScaleType.FILL_CENTER
}
val overlay = CircadifyOverlayView(this)
// Overlay sits on top of the preview with identical bounds.
val cameraContainer = FrameLayout(this).apply {
addView(previewView, FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT))
addView(overlay, FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT))
}
val sdk = CircadifySDK(
context = this,
config = CircadifyConfig(
apiKey = "ck_test_your_key_here",
callbacks = CircadifyCallbacks(
// null when no face is detected — the glow hides.
onScanFace = { face -> overlay.scanFace = face },
),
),
)kotlinDrive the brighter glow around the measurement itself:
overlay.isScanning = true
try {
val result = sdk.measureVitals(
MeasurementOptions(lifecycleOwner = this, previewView = previewView),
)
// ...show vitals...
} finally {
overlay.isScanning = false
overlay.scanFace = null
}kotlinCircadifyOverlayView does not intercept touches, so it never steals taps from your UI. It animates itself while attached to the window, only self-redraws while a face is present (so it doesn't burn battery idle), and respects the system remove animations accessibility setting.
Theming
Retheme the overlay by assigning a CircadifyScanStyle to overlay.style. The default palette is a warm thermal ramp — gold → orange → red — that pulses across the face. Override any field to recolor or re-time the glow. Colors are ARGB ints (e.g. 0xFFF2B340.toInt()).
import com.circadify.sdk.CircadifyScanStyle
// A cool cyan ramp instead of the default thermal one.
overlay.style = CircadifyScanStyle(
coreColor = 0xFF1AB3FF.toInt(),
hotColor = 0xFF1F80FF.toInt(),
midColor = 0xFF1FB3FF.toInt(),
goldColor = 0xFF40D2F2.toInt(),
ovalColor = 0xFF40D2F2.toInt(),
)kotlin| Field | Type | Default | Description |
|---|---|---|---|
coreColor | Int (ARGB) | 0xFFFF331A | Red core — only fully present at peak scanning intensity. |
hotColor | Int (ARGB) | 0xFFFF401F | Hot orange. |
midColor | Int (ARGB) | 0xFFFF801F | Mid orange. |
goldColor | Int (ARGB) | 0xFFF2B340 | Gold — the resting/idle tone. |
ovalColor | Int (ARGB) | 0xFFF2B340 | Thin guide-oval outline drawn under the glow. |
scanRegionDurationSeconds | Double | 2.0 | Seconds each region stays lit while measuring (faster, hotter). Must be > 0. |
idleRegionDurationSeconds | Double | 3.5 | Seconds each region stays lit while idle/framing (slower, calmer). Must be > 0. |
scanPeak | Double | 1.0 | Peak glow intensity while measuring. Range 0.0..1.0. |
idlePeak | Double | 0.4 | Peak glow intensity while idle/framing. Range 0.0..1.0. |
Custom overlay
Prefer to draw your own overlay? onScanFace delivers a CircadifyScanFace with the same geometry the bundled view paints. All points are normalized [0,1] in the detector's upright, unmirrored image space (top-left origin) — the same space the measurement pipeline uses. Project them into view coordinates yourself: aspect-fill crop with imageAspect, then flip x when isMirrored (true for the front camera).
callbacks = CircadifyCallbacks(
onScanFace = { face ->
if (face == null) {
myOverlay.clear()
} else {
myOverlay.update(face) // project + draw your own glow / reticle
}
},
)kotlin| Field | Type | Description |
|---|---|---|
forehead | PointF | Glow anchor over the forehead (mean of the forehead ROI landmarks). |
leftCheek | PointF | Glow anchor over the subject's left cheek (image-left; mirrored for display). |
rightCheek | PointF | Glow anchor over the subject's right cheek. |
ovalCenter | PointF | Center of the face oval (padded bounding ellipse of all landmarks). |
ovalSize | SizeF | Size of the face oval, in normalized image units. |
imageAspect | Float | Aspect ratio (width / height) of the image the landmarks were detected in. |
isMirrored | Boolean | Whether the preview shows the image mirrored (true for the front camera). |
landmarks | List<Landmark> | All 468 face-mesh landmarks (normalized, upright, unmirrored) backing this geometry. |
Next Steps
- Configuration — callbacks (including
onScanFace) and capture options. - Camera & Permissions — request permission and attach the
PreviewView. - Methods — the rest of
CircadifySDK.