Skip to main content

CameraX

CameraX is one of the libraries that Android developers can use to add camera functionality to their apps. For most use cases, CameraX is more recommended than other options.

There are 2 general ways to integrate CameraX into your app:

  1. Using CameraController or
  2. Using CameraProvider.

Using CameraX with CameraController​

CameraController is one of the ways of integrating CameraX into your app. When using CameraController you should be aware of some of its characteristics:

  • CameraController handles camera initialization for you. You only need to bind the controller to your app's LifecycleOwner.
  • CameraController is required to be used with PreviewView class to display the camera preview.
  • By default, CameraController listens to PreviewView's touch events; enabling tap-to-focus and pinch-to-zoom features.
  • By default, CameraController enables 3 use cases:
    • Preview (required for displaying camera preview),
    • ImageCapture (for taking picture), and
    • ImageAnalysis (for analyzing images from camera).
  • The VideoCapture use case is disabled by default because it might conflict with other use cases, especially on lower end devices.
  • You can customize the enabled features easily by using setEnabledUseCases().
  • CameraController listens to device motion sensor and set the target rotation for the use cases.

CameraController Simple Example​

Here's a simple tutorial to integrate CameraController into your application.

First you need to add CAMERA permission to your AndroidManifest.xml file. You can also specify the hardware feature requirement by adding <uses-feature> tag:

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />

</manifest>

Next, you need to add camerax dependencies to your app's build.gradle.kts file:

app/build.gradle.kts
dependencies {
// Add these dependencies:
val camerax_version = "1.4.0-beta01"
implementation("androidx.camera:camera-core:${camerax_version}")
implementation("androidx.camera:camera-camera2:${camerax_version}")
implementation("androidx.camera:camera-view:${camerax_version}")
}

Then, you need to add PreviewView to your activity_main.xml resource file. This is where the camera preview will be displayed. We also added a button that will take a picture when clicked:

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">

<androidx.camera.view.PreviewView
android:id="@+id/preview_view"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<Button
android:id="@+id/button_capture"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="Capture"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

Finally, inside our MainActivity.kt file, there are several things we need to do. The first thing we need to do is to request CAMERA runtime permission. After the permission is granted, we can safely bind our CameraController (we're using the concrete LifecycleCameraController class here) instance to our Activity's lifecycle and assign it to our layout's PreviewView. Finally, we're using takePicture() method to take a picture and save it into our app's internal storage.

Here's how our MainActivity.kt will look like in the end:

MainActivity.kt
package com.hanmajid.androidnotebook

import android.app.Activity
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.ImageProxy
import androidx.camera.view.LifecycleCameraController
import androidx.camera.view.PreviewView
import androidx.core.content.ContextCompat
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors

class MainActivity : ComponentActivity() {
private lateinit var cameraExecutor: ExecutorService
private lateinit var cameraController: LifecycleCameraController
private lateinit var requestPermissionsLauncher: ActivityResultLauncher<Array<String>>

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

// Setup permissions request launcher.
requestPermissionsLauncher =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {
if (allPermissionsGranted()) {
startCamera()
} else {
Toast.makeText(
this,
"Permissions not granted by the user.",
Toast.LENGTH_SHORT
).show()
finish()
}
}

// Setup capture button on click listener.
findViewById<Button>(R.id.button_capture).setOnClickListener {
takePicture()
}

// Request camera permissions
if (allPermissionsGranted()) {
startCamera()
} else {
requestPermissionsLauncher.launch(REQUIRED_PERMISSIONS)
}

cameraExecutor = Executors.newSingleThreadExecutor()
}

private fun startCamera() {
cameraController = LifecycleCameraController(baseContext).apply {
bindToLifecycle(this@MainActivity)
val previewView = findViewById<PreviewView>(R.id.preview_view)
previewView.controller = this
}
}

private fun takePicture() {
cameraController.takePicture(
cameraExecutor,
object : ImageCapture.OnImageCapturedCallback() {
override fun onCaptureStarted() {
super.onCaptureStarted()
Log.i(TAG, "onCaptureStarted")
}

override fun onCaptureProcessProgressed(progress: Int) {
super.onCaptureProcessProgressed(progress)
Log.i(TAG, "onCaptureProcessProgressed: $progress")
}

override fun onError(exception: ImageCaptureException) {
super.onError(exception)
Log.i(TAG, "onError: $exception")
}

override fun onCaptureSuccess(image: ImageProxy) {
super.onCaptureSuccess(image)
Log.i(TAG, "onCaptureSuccess: $image")
saveImage(
this@MainActivity,
image.toBitmap(),
"picture.jpeg",
)
image.close()
}
}
)
}

/**
* Save the image [bitmap] to internal storage.
*/
private fun saveImage(
activity: Activity,
bitmap: Bitmap,
filename: String,
) {
activity.openFileOutput(filename, Context.MODE_PRIVATE)
.use { outputStream ->
bitmap.compress(Bitmap.CompressFormat.PNG, 0, outputStream)
}
}

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

companion object {
private val TAG = "MainActivity"
private val REQUIRED_PERMISSIONS = arrayOf(
android.Manifest.permission.CAMERA,
)
}
}

Managing Camera Torch/Flashlight​

Using CameraController class and CameraInfo interface, we can manage our camera's torch/flashlight unit.

With CameraInfo, we can check whether our camera has flashlight unit and get a LiveData of the camera's torch state. And with CameraController, we can enable/disable the torch programmatically.

From the simple example that we've built on the previous section, we can add some code to demonstrate this:

class MainActivity : ComponentActivity() {
// Step 1: Add this variable to keep track whether the camera's torch is currently enabled or not.
private var isTorchEnabled = false

private fun startCamera() {
cameraController = LifecycleCameraController(baseContext).apply {
bindToLifecycle(this@MainActivity)
val previewView = findViewById<PreviewView>(R.id.preview_view)
previewView.controller = this
}

// Step 2: Use `torchState` method to listen to the torch state and update the variable.
cameraController.torchState.observe(this) { torchState ->
isTorchEnabled = torchState == TorchState.ON
}
}

// Step 3: Create a method to enable/disable torch camera.
// Make sure to check first whether the camera has a flashlight unit.
private fun toggleTorch() {
if (cameraController.cameraInfo?.hasFlashUnit() == true) {
cameraController.enableTorch(!isTorchEnabled)
}
}
}

That's the gist of it. Now, you only need to call the toggleTorch() method somewhere within your application to trigger the torch state change.

Managing Camera Zoom​

Using CameraController class and CameraInfo interface, we can manage our camera's zoom.

With CameraInfo, we can get a LiveData of the camera's zoom state. And with CameraController, we can set the zoom value programmatically.

From the simple example that we've built on the previous section, we can add some code to demonstrate this:

import com.google.android.material.slider.Slider

class MainActivity : ComponentActivity() {
private fun startCamera() {
cameraController = LifecycleCameraController(baseContext).apply {
bindToLifecycle(this@MainActivity)
val previewView = findViewById<PreviewView>(R.id.preview_view)
previewView.controller = this
}

// Step 1: Assuming that we have a Slider in our layout (slider_zoom),
// we can use `zoomState` method to listen to the zoom state and update the Slider value.
cameraController.zoomState.observe(this) { zoomState ->
findViewById<Slider>(R.id.slider_zoom).value = zoomState.linearZoom
}
}

// Step 2: Create a listener that updates the camera's linear zoom whenever our Slider value changes.
private fun handleChangeSliderZoom() {
findViewById<Slider>(R.id.slider_zoom).addOnChangeListener { _, value, _ ->
cameraController.setLinearZoom(value)
}
}
}

Managing Tap-To-Focus​

Checking Whether Tap-To-Focus is Enabled​

To check whether our camera currently enables tap-to-focus feature, we can use isTapToFocusEnabled() method:

import androidx.camera.view.CameraController

// val cameraController: CameraController = ...
val isEnabled: Boolean = cameraController.isTapToFocusEnabled

Enabling/Disabling Tap-To-Focus​

To enable/disable our camera's tap-to-focus feature, we can use setTapToFocusEnabled() method:

import androidx.camera.view.CameraController

// val cameraController: CameraController = ...
// Disable tap-to-focus
cameraController.isTapToFocusEnabled = false

Getting Tap-To-Focus State​

We can retrieve a LiveData of the camera's tap-to-focus state by using getTapToFocusState():

import android.util.Log
import androidx.camera.view.CameraController
import androidx.lifecycle.LifecycleOwner

cameraController.tapToFocusState.observe(lifecycleOwner) { tapToFocusState ->
Log.i("TAG", tapToFocusState.toString())
}

These are the possible tap-to-focus-state:

Tap-To-Focus StateDescription
TAP_TO_FOCUS_NOT_STARTEDNo tap-to-focus action has been started by the end user.
TAP_TO_FOCUS_STARTEDA tap-to-focus action has started but not completed. The app also gets notified with this state if a new action happens before the previous one could finish.
TAP_TO_FOCUS_FOCUSEDThe previous tap-to-focus action was completed successfully and the camera is focused.
TAP_TO_FOCUS_NOT_FOCUSEDThe previous tap-to-focus action was completed successfully but the camera is still unfocused, similar to the CONTROL_AF_STATE_NOT_FOCUSED_LOCKED state. The end user might be able to get a better result by trying again with different camera distances and/or lighting.
TAP_TO_FOCUS_FAILEDThe previous tap-to-focus action was failed to complete. This is usually due to device limitations.

References​