Blog Infos
Author
Published
Topics
, , , ,
Published

Writing customised camera experiences on Android has traditionally been very difficult. For one thing, photography itself is difficult: as anyone who has used a DSLR camera will know, there are lots of esoteric settings to get right. On Android we have the additional problem that different camera hardware from diverse manufacturers can respond in unpredictable ways, meaning that a camera app that works for, say, Samsung might not work correctly on a Pixel. So writing the code is difficult, and testing the code is also difficult.

That’s where the CameraX library comes in. It handles not just the complex photography bits but also the low-level device-specific bits as well. So now you can write simpler code, and you can write it just once, confident it’ll work on all supported devices.

The CameraX use cases

The CameraX library has 4 specific purposes, which it calls use cases:

  • Preview: like a viewfinder, this is a live preview of what the camera is pointing at. This use case is mandatory.
  • Image capture, for taking photos
  • Video capture, for capturing moving images and sound
  • Image analysis, which allows us to do clever things with the live camera output, such as QR code scanning or offloading to AI models

So, let’s build a camera app! We’re going to add the preview use case, selfie/rear camera selection, zoom, and image capture.

A full working version of this step-by-step guide is here: https://github.com/tdcolvin/CameraXWorkshop.

1. Add the CameraX library

First off, let’s include the latest CameraX library. These are the additions you need for your Gradle config files (assuming you’re using version catalogues):

implementation(libs.androidx.camera.core)
implementation(libs.androidx.camera.lifecycle)
implementation(libs.androidx.camera.view)
implementation(libs.androidx.camera.camera2)
implementation(libs.androidx.camera.video)
implementation(libs.androidx.camera.extensions)
[versions]
cameraX = "1.4.0"
[libraries]
androidx-camera-core = { group = "androidx.camera", name = "camera-core", version.ref = "cameraX" }
androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "cameraX" }
androidx-camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "cameraX" }
androidx-camera-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "cameraX" }
androidx-camera-video = { group = "androidx.camera", name = "camera-video", version.ref = "cameraX" }
androidx-camera-extensions = { group = "androidx.camera", name = "camera-extensions", version.ref = "cameraX" }
2. Add a PreviewView to your UI

A PreviewView shows what the camera is looking at, like a viewfinder. Unfortunately it’s an Android Views widget rather than a Composable, so we’re going to have to wrap it in an AndroidView Composable:

@Composable
fun CameraPreview(
modifier: Modifier = Modifier
) {
AndroidView(modifier = modifier,
factory = { context ->
PreviewView(context)
}
)
}

Now try running your app.

Hmm, blank screen.

That’s because a PreviewView is essentially just a canvas to draw things onto. It takes a preview use case to work out what pixels to draw:

The CameraX Preview use case draws pixels onto a PreviewView

So…

3. Adding a Preview use case

It’s easy to create a Preview use case. The class you need is androidx.camera.core.Preview which you’ll probably have to mention by its full name (because you may already have an import called Preview for the Compose @Preview annotation). We get an instance of Preview using a builder pattern, which we remember:

@Composable
fun CameraPreview( ... ) {
val previewUseCase = remember { androidx.camera.core.Preview.Builder().build() }
...
}

Then we need to tell the Preview to paint onto the canvas (“surface”) provided by the PreviewView. We’ll do that in the AndroidView’s factory method, when we create the PreviewView widget:

AndroidView(
...
factory = { context ->
PreviewView(context).also {
previewUseCase.surfaceProvider = it.surfaceProvider
}
}
)

Now let’s try running it again. Anything? No, not yet…

That’s because we haven’t bound the use case to a running camera.

4. Binding it all together

So, we’ve got something that could pick up the pixels from a camera, and something that would display those pixels. What we’re missing is a link to an actual camera.

That’s what a CameraProvider is. It’s used to bind use cases to a camera, and to do so in a lifecycle-aware manner.

CameraProvider joins together the use cases and a physical camera, in a lifecycle-aware manner

Let’s start with how to get an instance of a CameraProvider, which is an interface. The concrete class we want is ProcessCameraProvider, which we can get using a Context:

@Composable
fun CameraPreview( … ) {
val localContext = LocalContext.current
LaunchedEffect(Unit) {
cameraProvider = ProcessCameraProvider.awaitInstance(localContext)
}
}

Once we have that, we can bind a Preview use case to a camera and to a lifecycle. Through the magic of Kotlin we can put a function inside our Composable, which we can call whenever we need to rebind the camera:

@Composable
fun CameraPreview( … ) {
fun rebindCameraProvider() {
val cameraSelector = CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_FRONT)
.build()
cameraProvider?.bindToLifecycle(
localLifecycleOwner,
cameraSelector,
previewUseCase
)
}
}

You can see here how we’ve bound to the selfie camera (CameraSelector.LENS_FACING_FRONT) — we could equally have used CameraSelector.LENS_FACING_BACK for the rear camera.

That rebindCameraProvider() function needs to be called once only when both the PreviewView and ProcessCameraProvider are available. Our Composable starts creating both of those at the same time. The PreviewView almost arrives quickest, but we don’t want a potential race condition problem, so let’s make sure we call rebindCameraProvider() in both cases. The code is set up to only do something when both of those elements are in place, so we’re not in any danger of binding twice.

So that’s:

LaunchedEffect(Unit) {
cameraProvider = ProcessCameraProvider.awaitInstance(localContext)
rebindCameraProvider()
}

and:

AndroidView(modifier = modifier,
factory = { context ->
PreviewView(context).also {
previewUseCase.surfaceProvider = it.surfaceProvider
rebindCameraProvider()
}
}
)

Now run your app again. Tadaaa — we finally have a working camera preview! And you get to see my face:

PreviewView + Preview + CameraProvider all working nicely!

Let’s make this even better by allowing the user to switch cameras from front to rear and vice versa.

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Kobweb:Creating websites in Kotlin leveraging Compose HTML

Kobweb is a Kotlin web framework that aims to make web development enjoyable by building on top of Compose HTML and drawing inspiration from Jetpack Compose.
Watch Video

Kobweb:Creating websites in Kotlin leveraging Compose HTML

David Herman
Ex-Googler, author of Kobweb

Kobweb:Creating websites in Kotlin leveraging Compose HTML

David Herman
Ex-Googler, author o ...

Kobweb:Creating websites in Kotlin leveraging Compose HTML

David Herman
Ex-Googler, author of Kob ...

Jobs

No results found.

5. Switching cameras

We’ll add a parameter to our Composable to take the camera facing direction. Since the choice of camera is specified as part of the CameraProvider’s binding, we’re going to need to rebind when it changes.

That gives us a situation in which rebindCameraProvider() could call bindToLifecycle() whilst already bound. That’s illegal, and will throw an Exception. To handle this, we also need to add a call to unbind everything previously bound. That’s cameraProvider.unbindAll():

@Composable
fun CameraPreview(
lensFacing: Int = CameraSelector.LENS_FACING_BACK,
) {
fun rebindCameraProvider() {
cameraProvider.unbindAll()
val cameraSelector = CameraSelector.Builder()
.requireLensFacing(lensFacing)
.build()
}
LaunchedEffect(lensFacing) {
rebindCameraProvider()
}
}

There we go — now we can see out of both sides of the phone, and you get to see out my window:

Switching between front and rear cameras now works!

6. Zooming

The CameraProvider.bindToLifecycle() function actually returns us an instance of Camera which can be used to query information about, and configure, the camera. It has a property cameraControl which returns a CameraControl instance; we can use that to, err, control the camera. Which includes zooming!

(It’s worth also exploring the other things you can do with CameraControl — it encompasses quite a lot of good functionality).

So let’s capture the CameraControl when we bind:

var cameraControl by remember { mutableStateOf<CameraControl?>(null) }
fun rebindCameraProvider() {
val camera = cameraProvider.bindToLifecycle( … )
cameraControl = camera.cameraControl
}

And now let’s add the zoom level as a parameter to our Composable, and when it changes, perform the zoom operation:

@Composable
fun CameraPreview(
zoomLevel: Float,
...
) {
...
LaunchedEffect(zoomLevel) {
cameraControl?.setLinearZoom(zoomLevel)
}
}

Note that zooming isn’t instantaneous, in many cases we have to wait for something mechanical to happen. For that reason, the setLinearZoom() function above returns a ListenableFuture instance which you can use determine whether the operation has completed.

Warning: you can’t perform another zoom operation until the last one has finished. Trying to do so will throw an Exception. So you need to be a little careful here, particularly if you’re going to implement zoom based on a pinch gesture or slider (which can fire off many change events very quicky).

Now you get to see me waaay too close:

7. Taking a picture: accepting and binding an ImageCapture use case

To capture an image from the camera in full resolution, we need to use an ImageCapture use case. This is best off defined outside the CameraPreview composable, because we’ll need to use it in our button’s click event.

So our Composable now needs to take an ImageCapture parameter, and we need to reference it in the bind:

@Composable
fun CameraPreview(
...
imageCaptureUseCase: ImageCapture
) {
...
fun rebindCameraProvider() {
cameraProvider?.let { cameraProvider ->
val camera = cameraProvider.bindToLifecycle(
localContext as LifecycleOwner,
cameraSelector,
previewUseCase, imageCaptureUseCase
)
}
}
}

Now we just need to create that ImageCapture use case in the outside code.

8: Creating and using the ImageCapture use case

The ImageCapture use case is created in the same way as the Preview use case was:

val imageCaptureUseCase = remember { ImageCapture.Builder().build() }
view raw MainActivity.kt hosted with ❤ by GitHub

To use it, let’s add a “Take Photo” button:

Button(onClick = {
val outputFileOptions = ImageCapture.OutputFileOptions.Builder(File(localContext.externalCacheDir, "image.jpg"))
.build()
val callback = object: ImageCapture.OnImageSavedCallback {
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
outputFileResults.savedUri?.shareAsImage(localContext)
}
override fun onError(exception: ImageCaptureException) {
}
}
imageCaptureUseCase.takePicture(outputFileOptions, ContextCompat.getMainExecutor(localContext), callback)
}) {
Text("Take Photo")
}
view raw MainActivity.kt hosted with ❤ by GitHub

That’s quite a lot of code, but basically it’s all centred around the imageCaptureUseCase.takePicture() function call. To get to that point, we’ve had to:

  • Create an object (ImageCapture.OutputFileOptions) which tells it where to save the file it’s going to create
  • Create a callback for when the operation is complete, so we can see if it was successful or not. Specifically we create an ImageCapture.OnImageSavedCallback object which contains 2 functions; onImageSaved() and onError().

And now you can take a photo and share it!

ImageCapture use case in action, taking a photo and sharing it

You can check my Bluesky account for this original shared image!

To wrap it up

So, that’s it for now. We saw how to:

  • Include CameraX in a project
  • Create a PreviewView, filled in using a Preview use case
  • Bind the Preview use case to a camera to make it all work
  • Change camera lenses and rebind as needed
  • Obtain a CameraControl instance and use it for zooming
  • Create an ImageCapture use case and take photos.

A full working version of all the above code can be found in my CameraXWorkshop repository.

I hope it was useful; please do leave me a comment here, or find me on LinkedIn or Bluesky if you have any questions:

. . .

Need to build an app that uses camera features? Or any app at all, whether Android, iOS or web? I’m a consultant Android developer and the cofounder of Apptaura, the app development experts. Please get in touch if I can help with your latest project.

This article is previously published on proandroiddev.com.

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
Menu