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:

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:

				
					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

				
					<uses-permission android:name="android.permission.CAMERA" />
				
			

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<PreviewView?>(null) }
    var cameraControl by remember { mutableStateOf<CameraControl?>(null) }
    var meteringFactory by remember { mutableStateOf<MeteringPointFactory?>(null) }
    var focusRingOffset by remember { mutableStateOf<Offset?>(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

Tips & Best Practices

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

Table of Contents