In this codelab, you'll learn how to create a camera app that uses CameraX to show a viewfinder, take photos, and analyze an image stream from the camera.

To achieve this, we will introduce the concept of use cases in CameraX, which you can use for a variety of camera operations, from displaying a viewfinder to analyzing frames in real time.

Prerequisites

What you'll do

What you'll need

  1. Using the Android Studio menu, start a new project and select Empty Activity when prompted.

  1. Next, name the app "CameraX App". Make sure that the language is set to Kotlin, the minimum API level is 21 (which is the minimum required for CameraX) and that you use AndroidX artifacts.

Add the Gradle dependencies

  1. Open the build.gradle(Module: app) file and add the CameraX dependencies to our app Gradle file, inside the dependencies section:
def camerax_version = "1.0.0-beta03"
// CameraX core library using camera2 implementation
implementation "androidx.camera:camera-camera2:$camerax_version"
// CameraX Lifecycle Library
implementation "androidx.camera:camera-lifecycle:$camerax_version"
// CameraX View class
implementation "androidx.camera:camera-view:1.0.0-alpha10"
  1. CameraX needs some methods that are part of Java 8, so we need to set our compile options accordingly. At the end of the android block, right after buildTypes, add the following:
compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}

When prompted, click Sync Now, and we will be ready to use CameraX in our app.

Create the viewfinder layout

Let's replace the default layout with

  1. Open the activity_main layout file and replace it with this code.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
   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">

   <Button
       android:id="@+id/camera_capture_button"
       android:layout_width="100dp"
       android:layout_height="100dp"
       android:layout_marginBottom="50dp"
       android:scaleType="fitCenter"
       android:text="Take Photo"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintBottom_toBottomOf="parent"
       android:elevation="2dp" />

   <androidx.camera.view.PreviewView
       android:id="@+id/viewFinder"
       android:layout_width="match_parent"
       android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

Set up MainActivity.kt

  1. Replace the code in MainActivity.kt, with this. It includes import statements, variables you will instantiate, functions you will implement and constants.

onCreate() has already been implemented for you to check for camera permissions, start the camera, set the onClickListener() for the photo button, and implement the outputDirectory and cameraExecutor. Even though the onCreate() is implemented for you, the camera will not work yet until you implement the methods in the file.

package com.example.cameraxapp

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.Manifest
import android.content.pm.PackageManager
import android.net.Uri
import android.util.Log
import android.widget.Button
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import java.util.concurrent.Executors
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import kotlinx.android.synthetic.main.activity_main.*
import java.io.File
import java.nio.ByteBuffer
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.ExecutorService
typealias LumaListener = (luma: Double) -> Unit

class MainActivity : AppCompatActivity() {
   private var preview: Preview? = null
   private var imageCapture: ImageCapture? = null
   private var imageAnalyzer: ImageAnalysis? = null
   private var camera: Camera? = null

   private lateinit var outputDirectory: File
   private lateinit var cameraExecutor: ExecutorService

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

       // Request camera permissions
       if (allPermissionsGranted()) {
           startCamera()
       } else {
           ActivityCompat.requestPermissions(
               this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
       }

       // Setup the listener for take photo button
       camera_capture_button.setOnClickListener { takePhoto() }

       outputDirectory = getOutputDirectory()

       cameraExecutor = Executors.newSingleThreadExecutor()
   }

   private fun startCamera() {
       // TODO
   }

   private fun takePhoto() {
       // TODO
   }

   private fun allPermissionsGranted() = false

   fun getOutputDirectory(): File {
       val mediaDir = externalMediaDirs.firstOrNull()?.let {
           File(it, resources.getString(R.string.app_name)).apply { mkdirs() } }
       return if (mediaDir != null && mediaDir.exists())
           mediaDir else filesDir
   }

   companion object {
       private const val TAG = "CameraXBasic"
       private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
       private const val REQUEST_CODE_PERMISSIONS = 10
       private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
   }
}
  1. Run the code, it should look like this:

Before the app opens the camera, it needs permission from the user to do so. In this step, you'll implement camera permissions.

  1. Open AndroidManifest.xml and add these lines before the application tag.
<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />

Adding android.hardware.camera.any makes sure that the device has a camera. Specifying .any means that it can be a front camera or a back camera.

  1. Copy this code into MainActivity.kt.
    The bullet points below will break down the code you just copied.
override fun onRequestPermissionsResult(
   requestCode: Int, permissions: Array<String>, grantResults:
   IntArray) {
   if (requestCode == REQUEST_CODE_PERMISSIONS) {
       if (allPermissionsGranted()) {
           startCamera()
       } else {
           Toast.makeText(this,
               "Permissions not granted by the user.",
               Toast.LENGTH_SHORT).show()
           finish()
       }
   }
}
if (requestCode == REQUEST_CODE_PERMISSIONS) {

}
if (allPermissionsGranted()) {
   startCamera()
}
else {
   Toast.makeText(this,
       "Permissions not granted by the user.",
       Toast.LENGTH_SHORT).show()
   finish()
}
  1. Replace the allPermissionsGranted() method with this:
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {       
   ContextCompat.checkSelfPermission(
   baseContext, it) == PackageManager.PERMISSION_GRANTED
}
  1. Run the app.

It should now ask permission to use the camera:

In a camera application, the viewfinder is used to let the user preview the photo they will be taking. You can implement a viewfinder using the CameraX Preview class.

To use Preview, you'll first need to define a configuration, which then gets used to create an instance of the use case. The resulting instance is what you need to bind to the CameraX lifecycle.

  1. Copy this code into the startCamera() function.

The bullet points below will break down the code you just copied.

private fun startCamera() {
   val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

   cameraProviderFuture.addListener(Runnable {
       // Used to bind the lifecycle of cameras to the lifecycle owner
       val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

       // Preview
       preview = Preview.Builder()
           .build()

       // Select back camera
       val cameraSelector = CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build()

       try {
           // Unbind use cases before rebinding
           cameraProvider.unbindAll()

           // Bind use cases to camera
           camera = cameraProvider.bindToLifecycle(
               this, cameraSelector, preview)
           preview?.setSurfaceProvider(viewFinder.createSurfaceProvider(camera?.cameraInfo))
       } catch(exc: Exception) {
           Log.e(TAG, "Use case binding failed", exc)
       }

   }, ContextCompat.getMainExecutor(this))
}
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener(Runnable {}, ContextCompat.getMainExecutor(this))
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
preview = Preview.Builder().build()
val cameraSelector = CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build()
try {
   cameraProvider.unbindAll()
   camera = cameraProvider.bindToLifecycle(
       this, cameraSelector, preview)
preview?.setSurfaceProvider(viewFinder.createSurfaceProvider(camera?.cameraInfo))
} 
catch(exc: Exception) {
      Log.e(TAG, "Use case binding failed", exc)
}
  1. Run the app, you should see a camera preview!

Other use cases work in a very similar way compared to Preview. First, you must define a configuration object which is used to instantiate the actual use case object. To capture photos, you'll need to implement the takePhoto() method, which is called when the capture button is pressed .

Copy this code into the takePhoto() method.

The bullet points below will break down the code you just copied.

private fun takePhoto() {
   // Get a stable reference of the modifiable image capture use case
   val imageCapture = imageCapture ?: return

   // Create timestamped output file to hold the image
   val photoFile = File(
       outputDirectory,
       SimpleDateFormat(FILENAME_FORMAT, Locale.US
       ).format(System.currentTimeMillis()) + ".jpg")

   // Create output options object which contains file + metadata
   val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()

   // Setup image capture listener which is triggered after photo has
   // been taken
   imageCapture.takePicture(
       outputOptions, ContextCompat.getMainExecutor(this), object : ImageCapture.OnImageSavedCallback {
           override fun onError(exc: ImageCaptureException) {
               Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
           }

           override fun onImageSaved(output: ImageCapture.OutputFileResults) {
               val savedUri = Uri.fromFile(photoFile)
               val msg = "Photo capture succeeded: $savedUri"
               Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
               Log.d(TAG, msg)
           }
       })
}
val imageCapture = imageCapture ?: return
val photoFile = File(
   outputDirectory,
   SimpleDateFormat(FILENAME_FORMAT, Locale.US
   ).format(System.currentTimeMillis()) + ".jpg")
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
imageCapture.takePicture(
   outputOptions, ContextCompat.getMainExecutor(this), object : ImageCapture.OnImageSavedCallback {} 
)
override fun onError(exc: ImageCaptureException) {
   Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
}
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
   val savedUri = Uri.fromFile(photoFile)
   val msg = "Photo capture succeeded: $savedUri"
   Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
   Log.d(TAG, msg)
}
  1. Go to the startCamera() method and copy this code under the code for preview.
imageCapture = ImageCapture.Builder()
   .build()

This shows where in the method to paste the code:

private fun startCamera() {
       ... 

       preview = Preview.Builder()
           .build()

       // Paste image capture code here!


       val cameraSelector = CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build()

       ... 
}
  1. Finally, update the call to bindToLifecycle() in the try block to include the new use case:
camera = cameraProvider.bindToLifecycle(
   this, cameraSelector, preview, imageCapture)
  1. Rerun the app and press Take Photo.
    You will see a toast presented on the screen and a message in the logs.

View the photo

  1. Check the log statements, you will also see a log announcing that the photo capture succeeded.
2020-04-24 15:13:26.146 11981-11981/com.example.cameraxapp D/CameraXBasic: Photo capture succeeded: file:///storage/emulated/0/Android/media/com.example.cameraxapp/CameraXApp/2020-04-24-15-13-25-746.jpg
  1. Copy the file where the photo is stored omitting the file:// prefix.
/storage/emulated/0/Android/media/com.example.cameraxapp/CameraXApp/2020-04-24-15-13-25-746.jpg
  1. In the Android Studio terminal run these commands:
adb shell
cp [INSERT THE FILE FROM STEP 2 HERE] /sdcard/Download/photo.jpg
  1. Run this ADB command then exit the shell:
adb pull /sdcard/Download/photo.jpg
  1. You can review the photo saved in the file named photo.jpg in your current folder.

If you are looking for a simple camera app, you are done. It's really that simple! If you are looking to implement an image analyzer, read on!

A great way to make your camera app more interesting is using the ImageAnalysis feature. It allows you to define a custom class implementing the ImageAnalysis.Analyzer interface, which will be called with incoming camera frames. You won't have to worry about managing the camera session state or even disposing of images; binding to our app's desired lifecycle is sufficient, like with other lifecycle-aware components.

  1. Add this analyzer in as an inner class in MainActivity.kt.
    This analyzer logs the average luminosity of the image. To create an analyzer, you need to override the analyze function in a class that implements the ImageAnalysis.Analyzer interface.
private class LuminosityAnalyzer(private val listener: LumaListener) : ImageAnalysis.Analyzer {

   private fun ByteBuffer.toByteArray(): ByteArray {
       rewind()    // Rewind the buffer to zero
       val data = ByteArray(remaining())
       get(data)   // Copy the buffer into a byte array
       return data // Return the byte array
   }

   override fun analyze(image: ImageProxy) {

       val buffer = image.planes[0].buffer
       val data = buffer.toByteArray()
       val pixels = data.map { it.toInt() and 0xFF }
       val luma = pixels.average()

       listener(luma)

       image.close()
   }
}

With our class implementing the ImageAnalysis.Analyzer interface, all we need to do is instantiate an instance of LuminosityAnalyzer in the ImageAnalysis like all other use cases and update the `startCamera()` function once again, before the call to CameraX.bindToLifecycle():

  1. In the startCamera() method, add this code under the imageCapture() code.
imageAnalyzer = ImageAnalysis.Builder()
   .build()
   .also {
       it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
           Log.d(TAG, "Average luminosity: $luma")
       })
   }

This shows where in the method to paste the code:

private fun startCamera() {
       ... 

       imageCapture = ImageCapture.Builder()
   .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
   .build()

       // Paste image analyzer code here!


       val cameraSelector = CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build()

       ... 
}
  1. Update the bindToLifecycle() call on the cameraProvider to include the imageAnalyzer.
camera = cameraProvider.bindToLifecycle(
   this, cameraSelector, preview, imageCapture, imageAnalyzer)
  1. Run the app now! It will produce a message similar to this in logcat approximately every second.
D/CameraXApp: Average luminosity: ...

You've successfully implemented the following into a new Android app from scratch:

If you are interested in reading more about CameraX and the things that you can do with it, checkout the documentation or clone the official sample.