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:
- Using
CameraController
or - 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'sLifecycleOwner
.CameraController
is required to be used withPreviewView
class to display the camera preview.- By default,
CameraController
listens toPreviewView
'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), andImageAnalysis
(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:
<?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:
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:
<?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:
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 State | Description |
---|---|
TAP_TO_FOCUS_NOT_STARTED | No tap-to-focus action has been started by the end user. |
TAP_TO_FOCUS_STARTED | A 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_FOCUSED | The previous tap-to-focus action was completed successfully and the camera is focused. |
TAP_TO_FOCUS_NOT_FOCUSED | The 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_FAILED | The previous tap-to-focus action was failed to complete. This is usually due to device limitations. |