Android Jetpack CameraX Example

This post is about Android Jetpack CameraX API and how to implement it in an android application with a simple basic example.

CameraX

  • CameraX is a Jetpack support library, makes it easier to add camera capabilities to your app.
  • It has backward compatibility down to Android API 21.
  • It uses a simpler, use case-based approach that is lifecycle-aware.
  • It resolves device compatibility issues for you so that you don’t have to include device-specific code in your code base.
  • It also reduces the amount of code you need to write when adding camera capabilities to your app.

CameraX: Use Cases

We use CameraX to interface with a device’s camera through an abstraction called a use case. The following use cases are available:

  • Preview: accepts a surface for displaying a preview, such as a PreviewView.
  • Image analysis: analyse an image.
  • Image capture: captures and saves an image.

Creating new project

1 . Create a new project by going to File  New Android Project, select Empty Activity , provide app name, select language to kotlin and then finally click on finish.

2. Open the build.gradle(Module: app) file and add the below CameraX dependencies inside the dependencies section:

build.gradle(Module: app)

dependencies {
def camerax_version = "1.0.0-beta07"
// 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-alpha14"
}

3 . 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
}

4 .Open AndroidManifest.xml and add the below permissions 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.

5. Adding Camera View (PreviewView) and Capture Button in XML.

activity_main.xml

<?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="Capture 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>

Implement Preview use case

6 . To implement the CameraX preview use case, we will implement the startCamera() method, which is called after allowing camera permission from user.

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

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
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(viewFinder.createSurfaceProvider())
}

// Select back camera as a default
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

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

// Bind preview use case to camera
cameraProvider.bindToLifecycle(
this, cameraSelector, preview
)

} catch (exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}

}, ContextCompat.getMainExecutor(this))
}
  • Create an instance of the ProcessCameraProvider. This is used to bind the lifecycle of cameras to the lifecycle owner. This eliminates the task of opening and closing the camera since CameraX is lifecycle-aware.
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
  • Add a listener to the cameraProviderFuture. Add a Runnable as one argument and ContextCompat.getMainExecutor() as the second argument ( returns an Executor that runs on the main thread).
cameraProviderFuture.addListener(Runnable {}, ContextCompat.getMainExecutor(this))
  • In the Runnable, add a ProcessCameraProvider. This is used to bind the lifecycle of your camera to the LifecycleOwner within the application’s process.
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
  • Initialize the Preview object, get a surface provider from viewfinder, and then set it on the preview.
// Preview
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(viewFinder.createSurfaceProvider())
}
// Select back camera as a default
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
  • Inside try block, make sure nothing is bound to your cameraProvider, and then bind your cameraSelector and preview object to the cameraProvider. Add catch block to log if there’s a failure.
try {
// Unbind use cases from app lifecycle
cameraProvider.unbindAll()

// Bind use cases to camera
cameraProvider.bindToLifecycle(
this, cameraSelector, preview
)

} catch (exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
  • Run the app. You should see a camera preview!

Implement ImageCapture use case

7 . To capture and save photos, we will implement the takePhoto() method, which is called when the CAPTURE PHOTO button is pressed .

// Set up the listener for capture photo button
camera_capture_button.setOnClickListener { takePhoto() }

private fun takePhoto() {

val imageCapture = imageCapture ?: return

// Create time-stamped 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()

// Set up 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)
}
})
}
  • First, get a reference to the ImageCapture use case. If the use case is null, exit out of the function. 
val imageCapture = imageCapture ?: return
  • Create a file to hold the image. Add a time stamp so the file name will be unique.
val photoFile = File(
outputDirectory,
SimpleDateFormat(
FILENAME_FORMAT, Locale.US
).format(System.currentTimeMillis()) + ".jpg"
)
  • Create an OutputFileOptions object, where you can specify things about how you want your output to be. You want the output saved in the file we just created, so add your photoFile.
// Create output options object which contains file + metadata
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
  • Call takePicture() on the imageCapture object. Pass in outputOptions, the executor, and a callback for when the image is saved. 
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)
}
})
  • onError() : call when image capture fails or saving the image capture fails.
  • onImageSaved() : call when the photo was taken successfully. Saves the photo to the file you created earlier, present a toast to let the user know it was successful, and print a log statement.
  • Go to the startCamera() method and copy this code under the code for preview.
imageCapture = ImageCapture.Builder()
.build()
  • Finally, update the call to bindToLifecycle() in the try block to include the new use case:
// Add imageCapture use case to camera
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture
)
  • Rerun the app and press Capture Photo. You will see a toast presented on the screen and a message in the logs.

8 . Open MainActivity.kt and add the below code.

MainActivity.kt

package com.c1ctech.androidcameraxdemo

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.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 kotlinx.android.synthetic.main.activity_main.*
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.ExecutorService

class MainActivity : AppCompatActivity() {
private var imageCapture: ImageCapture? = 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
)
}

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

outputDirectory = getOutputDirectory()

cameraExecutor = Executors.newSingleThreadExecutor()
}

private fun takePhoto() {

// Get a stable reference of the modifiable image capture use case
val imageCapture = imageCapture ?: return

// Create time-stamped 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()

// Set up 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)
}
})
}

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()

// creating Preview instance
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(viewFinder.createSurfaceProvider())
}

// creating ImageCapture instance
imageCapture = ImageCapture.Builder()
.build()

// Select back camera as a default
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

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

// Bind use cases to camera
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture
)

} catch (exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}

}, ContextCompat.getMainExecutor(this))
}

private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(
baseContext, it
) == PackageManager.PERMISSION_GRANTED
}

private 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
}

override fun onDestroy() {
super.onDestroy()
cameraExecutor.shutdown()
}

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)
}

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()
}
}
}
}

Leave a Reply