In the world of modern Android development, Jetpack Compose is transforming how we design UIs — and CameraX is simplifying how we work with the camera. One powerful but often overlooked feature you can add to camera apps is “Tap to Focus.”
When paired with CameraX’s MeteringPointFactory and the flexibility of Compose UI elements, implementing this behavior becomes intuitive and interactive.
In this blog, we’ll guide you step-by-step through:
-
Setting up CameraX with Compose
-
Capturing tap gestures on a preview
-
Converting tap coordinates into focus points
-
Using
CameraControl
andMeteringPointFactory
to initiate focus -
Adding visual feedback (focus ring) using Compose
Let’s start mastering camera transformations and interactions with Jetpack Compose.
Why Tap to Focus?
The ability to tap on a preview and adjust the camera’s focus is an essential part of modern camera apps. Whether you’re building a photography app, barcode scanner, or even an AR experience, manual focus control improves both usability and professional feel.
Prerequisites
Make sure your Android project is set up with:
-
Kotlin 1.8+
-
Jetpack Compose 1.5.0+
-
CameraX 1.3.0+
-
Min SDK 21
dependencies {
implementation("androidx.camera:camera-core:1.3.0")
implementation("androidx.camera:camera-camera2:1.3.0")
implementation("androidx.camera:camera-lifecycle:1.3.0")
implementation("androidx.camera:camera-view:1.3.0")
implementation("androidx.compose.ui:ui:1.5.0")
implementation("androidx.compose.material:material:1.5.0")
implementation("androidx.activity:activity-compose:1.7.2")
}
Step 1: Permissions and Camera Preview Setup
Add Permissions to AndroidManifest.xml: xml Copy Edit
Camera Permission Request Composable:
@Composable
fun RequestCameraPermission(onPermissionGranted: () -> Unit) {
val context = LocalContext.current
val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission()
) { granted ->
if (granted) onPermissionGranted()
}
LaunchedEffect(Unit) {
launcher.launch(Manifest.permission.CAMERA)
}
}
Step 2: Create a Camera Preview Using AndroidView
CameraX’s preview is not natively compatible with Compose, so we embed PreviewView
using AndroidView
.
@Composable
fun CameraPreview(
modifier: Modifier = Modifier,
onPreviewReady: (PreviewView) -> Unit
) {
AndroidView(
factory = { context ->
val previewView = PreviewView(context).apply {
scaleType = PreviewView.ScaleType.FILL_CENTER
}
onPreviewReady(previewView)
previewView
},
modifier = modifier
)
}
Step 3: Initialize CameraX with Focus
fun startCamera(
context: Context,
lifecycleOwner: LifecycleOwner,
previewView: PreviewView,
onCameraControlReady: (CameraControl, MeteringPointFactory) -> Unit
) {
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
val preview = Preview.Builder().build().apply {
setSurfaceProvider(previewView.surfaceProvider)
}
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
val camera = cameraProvider.bindToLifecycle(
lifecycleOwner,
cameraSelector,
preview
)
val cameraControl = camera.cameraControl
val meteringPointFactory = previewView.meteringPointFactory
onCameraControlReady(cameraControl, meteringPointFactory)
}, ContextCompat.getMainExecutor(context))
}
Step 4: Detect Tap Coordinates and Focus
Jetpack Compose lets us capture gestures via Modifier.pointerInput
. Combine this with CameraControl
and MeteringPointFactory
to translate the screen tap into a camera focus action.
fun focusOnTap(
x: Float,
y: Float,
previewView: PreviewView,
cameraControl: CameraControl
) {
val meteringPoint = previewView.meteringPointFactory.createPoint(x, y)
val action = FocusMeteringAction.Builder(meteringPoint)
.setAutoCancelDuration(3, TimeUnit.SECONDS)
.build()
cameraControl.startFocusAndMetering(action)
}
Step 5: Compose Integration with Tap and Focus Ring
We now wire up everything in Compose and add a focus ring animation for visual feedback.
@Composable
fun TapToFocusCamera() {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
var previewViewRef by remember { mutableStateOf(null) }
var cameraControl by remember { mutableStateOf(null) }
var meteringFactory by remember { mutableStateOf(null) }
var focusRingOffset by remember { mutableStateOf(null) }
Box(modifier = Modifier.fillMaxSize()) {
CameraPreview(modifier = Modifier.fillMaxSize()) { previewView ->
previewViewRef = previewView
startCamera(
context,
lifecycleOwner,
previewView
) { control, factory ->
cameraControl = control
meteringFactory = factory
}
}
Box(modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectTapGestures { offset ->
focusRingOffset = offset
val x = offset.x
val y = offset.y
previewViewRef?.let { previewView ->
cameraControl?.let { control ->
focusOnTap(x, y, previewView, control)
}
}
// Remove ring after delay
CoroutineScope(Dispatchers.Main).launch {
delay(1000)
focusRingOffset = null
}
}
}
)
focusRingOffset?.let { offset ->
FocusRing(offset = offset)
}
}
}
Step 6: Create the Focus Ring Composable
Add a simple animated ring to visualize where the user tapped.
@Composable
fun FocusRing(offset: Offset) {
val size = 100.dp
Box(
modifier = Modifier
.offset {
IntOffset(
(offset.x - size.toPx() / 2).toInt(),
(offset.y - size.toPx() / 2).toInt()
)
}
.size(size)
.border(2.dp, Color.Yellow, CircleShape)
.background(Color.Transparent, CircleShape)
)
}
Use Cases of Tap to Focus
-
Photography & Camera Apps: Give users manual control over subject focus.
-
Barcode/QR Scanners: Focus accurately on codes in low-light or cluttered environments.
-
AR & Object Recognition Apps: Help machine learning models concentrate on the correct region.
-
Video Streaming: Maintain sharp focus on faces or objects while broadcasting.
Tips & Best Practices
-
Debounce Tap Events
Add a cooldown or debounce to prevent multiple focus calls rapidly. -
Use AutoCancel
CameraX focus metering supports auto-cancel—use it to return to auto mode. -
Add Feedback
Always provide visual feedback (like the ring) to show that focus occurred. -
Edge Cases
Handle taps near the edge or corners carefully. Some devices can’t focus at corners due to sensor limits. -
Permission Handling
Always check and handle camera permissions gracefully to avoid crashes
Conclusion
Jetpack Compose paired with CameraX unlocks exciting UI and camera functionality for Android developers. Adding tap-to-focus creates a smoother and more interactive user experience — and now, it’s easier than ever.
With Compose handling UI gestures and CameraX enabling easy control over the camera system, you’ve got a dynamic, efficient, and intuitive solution for building modern camera-based features