An Introduction to CameraX

An Introduction to CameraX

CameraX is a new Jetpack library introduced at Google IO 2019 which was built to help make camera development easier. It provides an easy to use API environment which works across most Android devices with backwards compatibility to Android 5.0 (API Level 21).

Today, we are going to talk about how CameraX solves some of the problems developers were facing with the old camera API and take a look at how we can create our own camera app using CameraX.

How it helps

Now the question remains how CameraX is different from the other camera APIs and how it can help us develop better camera apps. Here are some of the greatest improvements and benefits CameraX provides us with.

Ease of use:

CameraX provides several predefined use cases like a preview, image capture and image analysis which work on almost every device on the market. This allows us developers to focus on the tasks we need to get done instead of spending our time writing basic functionality and managing requirements for different devices.

Consistency across devices:

Managing the consistency across different devices is hard and there is a lot to account for including the aspect ratio, rotation and orientation. CameraX takes care of that basic configuration and greatly reduces our test burden as developers.

Add ons:

CameraX also enables us developers to use the same camera features that the pre-installed camera app provides, with little code requirements. This is possible by providing optional add ons that add effects like Portrait, HDR, Night, and Beauty within our applications.

Project:

By now we should know why CameraX is useful and where it can improve our development experience. Now let’s take a look at how we can develop a simple camera application which allows us to take photos, enable the flash and switch lenses.

So, without wasting any further time, let’s get started.

Importing the dependencies

Before we can start creating our UI we first need to import the needed dependencies for our project. We can do so by adding the following lines of code to our build.gradle (Module:app) file.

//Material Design
implementation 'com.google.android.material:material:1.1.0-alpha05'

// CameraX
def camerax_version = "1.0.0-alpha01"
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"

Creating the UI

Now that we have successfully set up our project we can continue by creating our main layout which will include our preview view and three buttons to control our app.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity"
        android:orientation="vertical">

    <TextureView
            android:id="@+id/view_finder"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
            android:id="@+id/fab_camera"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:fabSize="normal"
            android:src="@drawable/ic_camera"
            android:layout_alignParentBottom="true"
            android:layout_margin="32dp"
            android:layout_centerHorizontal="true"
            app:backgroundTint="@android:color/white"/>

    <com.google.android.material.floatingactionbutton.FloatingActionButton
            android:id="@+id/fab_flash"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:fabSize="normal"
            android:src="@drawable/ic_flash"
            android:layout_alignParentBottom="true"
            android:layout_margin="32dp"
            app:backgroundTint="@android:color/white"/>

    <com.google.android.material.floatingactionbutton.FloatingActionButton
            android:id="@+id/fab_switch_camera"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:fabSize="normal"
            android:src="@drawable/ic_switch_camera"
            android:layout_alignParentBottom="true"
            android:layout_margin="32dp"
            android:layout_alignParentRight="true"
            app:backgroundTint="@android:color/white"/>

</RelativeLayout>

Note: The Textureview comes with the CameraX library and is used to display a preview of the camera in our application.

Requesting the required permissions

Before we can start implementing the preview we first need to request the required permissions.

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

We also need to make sure that the user has really enabled the permissions by checking them in realtime and request them if he hasn’t.

val permissions = arrayOf(android.Manifest.permission.CAMERA, android.Manifest.permission.WRITE_EXTERNAL_STORAGE, android.Manifest.permission.READ_EXTERNAL_STORAGE)

private fun hasNoPermissions(): Boolean{
    return ContextCompat.checkSelfPermission(this,
        Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(this,
        Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(this,
        Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED
}

fun requestPermission(){
    ActivityCompat.requestPermissions(this, permissions,0)
}

Configuring the preview

Now that we have created our layout and requested the needed permissions we can start implementing the camera preview. For that, we need to create a preview config and add a preview listener onto it which updates the UI everytime the output changes.

First let’s create the preview config in our bindCamera() function.

private fun bindCamera(){
 val previewConfig = PreviewConfig.Builder()
     .setLensFacing(lensFacing)
     .build()
}

After that we use our config to create a preview object and get the view that will display the preview in the UI.

val preview = Preview(previewConfig)

// The view that displays the preview
val textureView: TextureView = findViewById(R.id.view_finder)

Now we just need to add an OnPreviewOutputUpdateListener on our preview and update the view with the preview output we receive.

// Handles the output data of the camera
preview.setOnPreviewOutputUpdateListener { previewOutput ->
    // Displays the camera image in our preview view
    textureView.surfaceTexture = previewOutput.surfaceTexture
}

Taking an image

Next up let’s look at how we can take images using CameraX. For that, we need to create an image capture config which will define the lens and flash mode we will use for our image and capture the image using the takePhoto() function.

// Image capture config which controls the Flash and Lens
val imageCaptureConfig = ImageCaptureConfig.Builder()
    .setTargetRotation(windowManager.defaultDisplay.rotation)
    .setLensFacing(lensFacing)
    .setFlashMode(FlashMode.ON)
    .build()

imageCapture = ImageCapture(imageCaptureConfig)

Here we set the flash mode, rotation and lense the image should be taken with.

After that, we define an onClickListener on our camer fab button which will taken an image and save it into the local storage.

private val filename = "test.png"
private val sd = Environment.getExternalStorageDirectory()
private val dest = File(sd, filename)

// Takes an images and saves it in the local storage
fab_camera.setOnClickListener {
    imageCapture?.takePicture(dest,
        object : ImageCapture.OnImageSavedListener {
            override fun onError(error: ImageCapture.UseCaseError,
                                 message: String, exc: Throwable?) {
                Log.e("Image", error.toString())
            }
            override fun onImageSaved(file: File) {
                Log.v("Image", "Successfully saved image")
            }
        })
}

The takePicture() function takes two parameters: The destination the picture should be saved in and an onImageSavedListener which defines and onError and onImageSaved event.

Switching flash state

Now that we are able to take images let’s look at how we can change the flash of our camera. For that, we just need to check the current flash state and change it to the opposite.

// Changes the flash mode when the button is clicked
fab_flash.setOnClickListener {
    val flashMode = imageCapture?.flashMode
    if(flashMode == FlashMode.ON) imageCapture?.flashMode = FlashMode.OFF
    else imageCapture?.flashMode = FlashMode.ON
}

Here we check the flashstate by getting the flashMode parameter from our imageCapture configuration. After that we change it to the opposite state.

Switching lenses

Now let’s continue by adding the lens switching functionality into our app. For that, we need to save our current lense state and change it if the flash button is clicked.

This is easier said than done because the lens can’t just be switched while the camera is bound to the lifecycle (We will bind the camera to the lifecycle in the next step). That’s why we first need to unbind the camera from the lifecycle and then change the config and bind it again.

CameraX.unbindAll()

After that we can change the lense state and bind the camera again.

private var lensFacing = CameraX.LensFacing.BACK

// Changes the lens direction if the button is clicked
fab_switch_camera.setOnClickListener {
    lensFacing = if (CameraX.LensFacing.FRONT == lensFacing) {
        CameraX.LensFacing.BACK
    } else {
        CameraX.LensFacing.FRONT
    }
    bindCamera()
}

Bind the camera to the lifecycle

Lastly, we need to bind the camera to the lifecycle of our app to run it. We can do so by calling the bindToLifecycle() function and simply pass our activity as a lifecycle and our configurations.

CameraX.bindToLifecycle(this as LifecycleOwner, imageCapture, preview)

Note: It will give you an error that MainActivity.kt is not a lifecycle if your appcompat dependency is not version 1.1.0 or higher.

Complete Source Code for the MainActivity.kt

Here you can find the complete source code of the MainActivity.kt.

The full code is also available on my Github.

package com.example.camerax

import android.Manifest
import android.content.pm.PackageManager
import android.os.Bundle
import android.os.Environment
import android.util.Log
import android.view.TextureView
import androidx.camera.core.*
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import kotlinx.android.synthetic.main.activity_main.*
import java.io.File

val permissions = arrayOf(android.Manifest.permission.CAMERA, android.Manifest.permission.WRITE_EXTERNAL_STORAGE, android.Manifest.permission.READ_EXTERNAL_STORAGE)

class MainActivity : AppCompatActivity() {

    private val filename = "test.png"
    private val sd = Environment.getExternalStorageDirectory()
    private val dest = File(sd, filename)
    private var lensFacing = CameraX.LensFacing.BACK
    private var imageCapture: ImageCapture? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        bindCamera()

        // Takes an images and saves it in the local storage
        fab_camera.setOnClickListener {
            imageCapture?.takePicture(dest,
                object : ImageCapture.OnImageSavedListener {
                    override fun onError(error: ImageCapture.UseCaseError,
                                         message: String, exc: Throwable?) {
                        Log.e("Image", error.toString())
                    }
                    override fun onImageSaved(file: File) {
                        Log.v("Image", "Successfully saved image")
                    }
                })
        }

        // Changes the flash mode when the button is clicked
        fab_flash.setOnClickListener {
            val flashMode = imageCapture?.flashMode
            if(flashMode == FlashMode.ON) imageCapture?.flashMode = FlashMode.OFF
            else imageCapture?.flashMode = FlashMode.ON
        }

        // Changes the lens direction if the button is clicked
        fab_switch_camera.setOnClickListener {
            lensFacing = if (CameraX.LensFacing.FRONT == lensFacing) {
                CameraX.LensFacing.BACK
            } else {
                CameraX.LensFacing.FRONT
            }
            bindCamera()
        }
    }

    /**
     * Check if the app has all permissions
     */
    private fun hasNoPermissions(): Boolean{
        return ContextCompat.checkSelfPermission(this,
            Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(this,
            Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(this,
            Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED
    }

    /**
     * Request all permissions
     */
    fun requestPermission(){
        ActivityCompat.requestPermissions(this, permissions,0)
    }

    /**
     * Bind the Camera to the lifecycle
     */
    private fun bindCamera(){
        CameraX.unbindAll()

        // Preview config for the camera
        val previewConfig = PreviewConfig.Builder()
            .setLensFacing(lensFacing)
            .build()

        val preview = Preview(previewConfig)

        // The view that displays the preview
        val textureView: TextureView = findViewById(R.id.view_finder)

        // Handles the output data of the camera
        preview.setOnPreviewOutputUpdateListener { previewOutput ->
            // Displays the camera image in our preview view
            textureView.surfaceTexture = previewOutput.surfaceTexture
        }


        // Image capture config which controls the Flash and Lens
        val imageCaptureConfig = ImageCaptureConfig.Builder()
            .setTargetRotation(windowManager.defaultDisplay.rotation)
            .setLensFacing(lensFacing)
            .setFlashMode(FlashMode.ON)
            .build()

        imageCapture = ImageCapture(imageCaptureConfig)

        // Bind the camera to the lifecycle
        CameraX.bindToLifecycle(this as LifecycleOwner, imageCapture, preview)
    }

    override fun onStart() {
        super.onStart()

        // Check and request permissions
        if (hasNoPermissions()) {
            requestPermission()
        }
    }
}

Looking up the image

After taking an image you need to go into the local storage of your Android device to look at it. I’m just including it for the people who aren’t confident navigating in the local Android storage.

  1. Open the files app on your Android device
  2. Go to the local register
  3. Search for test.png

Closing Notes

You made it all the way until the end! Hope that this article helped you understand the basics of Android the CameraX library and how you can use it in your projects.

If you have found this useful, please consider recommending and sharing it with other fellow developers.

If you have any questions or feedback, let me know in the comments down below.

Read these next: