How to Fix Android CameraX Tap-to-Focus Not Working (Even When Focus Succeeds)
The Problem: CameraX Focus Succeeds, But Nothing Changes
Implementing a manual tap-to-focus feature in Android using Jetpack CameraX is a common requirement. However, developers often run into a frustrating issue: the startFocusAndMetering API returns a successful result (isFocusSuccessful = true), the logcat outputs the correct coordinates, yet visually, the camera does not refocus at all.
If you are experiencing this issue, especially within a Jetpack Compose environment using an AndroidView wrapper for PreviewView, you are not alone. Let's dive into why this happens and how to resolve it.
Why CameraX "Succeeds" but Doesn't Visually Focus
When CameraX reports that a focus action was successful, it means the hardware lens successfully moved to the position requested by the metering point. If you don't see any visual change, it is usually due to one of the following reasons:
- Continuous Auto-Focus (CAF) Overrides It: By default, CameraX uses continuous auto-focus. Once your manual focus duration (e.g., 3 seconds) expires, the camera immediately reverts to CAF, making it look like nothing happened.
- Incorrect Coordinate Mapping: If your
PreviewViewis scaled, cropped, or not fully laid out when the coordinates are captured, themeteringPointFactorymight map the tap to an incorrect region (like the very edge of the sensor), resulting in no perceived focus change on your subject. - Lack of Visual Feedback: Human eyes struggle to detect minute focus adjustments without a visual cue (like a focusing ring/circle).
- Missing Focus Flags: By default,
FocusMeteringActionattempts to adjust Auto Focus (AF), Auto Exposure (AE), and Auto White Balance (AWB). Sometimes, combining these causes unexpected exposure shifts rather than a sharp focus change.
Step-by-Step Solutions to Fix Tap-to-Focus
1. Specify the Focus Flag Explicitly
Ensure you are explicitly targeting the Auto Focus (AF) system. While the builder defaults to AF, AE, and AWB, isolating your action to focus can sometimes yield more predictable results on budget devices.
val action = FocusMeteringAction.Builder(meteringPoint, FocusMeteringAction.FLAG_AF)
.setAutoCancelDuration(5, TimeUnit.SECONDS)
.build()2. Adjust or Disable Auto-Cancel
If your auto-cancel duration is too short (like 3 seconds), the camera will quickly snap back to continuous auto-focus. Try increasing this duration, or disable it entirely to lock the focus until the user taps elsewhere or the camera state changes:
// To keep the focus locked on the tapped point indefinitely:
val action = FocusMeteringAction.Builder(meteringPoint)
.disableAutoCancel()
.build()3. Handle Coordinates Correctly in Jetpack Compose
When wrapping PreviewView in Compose's AndroidView, layout coordinates can sometimes become offset. Ensure you are using the coordinates relative to the PreviewView itself, not the screen root. Using motionEvent.x and motionEvent.y on the PreviewView's touch listener is correct, but make sure the view's scale type is set properly:
previewView.scaleType = PreviewView.ScaleType.FILL_CENTER4. Add a Visual Focus Ring (The Missing Piece)
Often, the camera is focusing, but because modern smartphone cameras have a deep depth of field, the visual change is subtle. Adding a temporary UI circle where the user tapped provides instant feedback and makes the focus action feel responsive.
Here is an optimized implementation of the tap-to-focus listener, including a placeholder for triggering a UI animation:
previewView.setOnTouchListener { view, motionEvent ->
when (motionEvent.action) {
MotionEvent.ACTION_UP -> {
view.performClick()
// 1. Create the metering point
val factory = previewView.meteringPointFactory
val meteringPoint = factory.createPoint(motionEvent.x, motionEvent.y)
// 2. Build the action (focus only, lock for 5 seconds)
val action = FocusMeteringAction.Builder(meteringPoint, FocusMeteringAction.FLAG_AF)
.setAutoCancelDuration(5, TimeUnit.SECONDS)
.build()
// 3. Trigger your UI Focus Ring animation here using motionEvent.x and y
// triggerFocusRingAnimation(motionEvent.x, motionEvent.y)
// 4. Execute focus request
val result = camera.cameraControl.startFocusAndMetering(action)
result.addListener(
{
try {
val isFocusSuccessful = result.get().isFocusSuccessful
if (isFocusSuccessful) {
Log.d("CameraXApp", "Focus succeeded at: (${motionEvent.x}, ${motionEvent.y})")
} else {
Log.d("CameraXApp", "Focus failed to lock.")
}
} catch (e: Exception) {
Log.e("CameraXApp", "Focus action interrupted", e)
}
},
ContextCompat.getMainExecutor(view.context)
)
}
}
true
}How to Verify if Focus is Actually Changing
If you still suspect the hardware is not reacting, try this simple test:
- Point your camera at an object extremely close to the lens (macro distance) so the background is blurry.
- Tap a distant object in the background.
- If the background becomes sharp and the foreground becomes blurry, your tap-to-focus is working perfectly!