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
- Basic Android development experience.
What you'll do
- Learn how to add the CameraX dependencies.
- Learn how to display the camera preview in an activity. (Preview use case)
- Build an app that can take a photo and save it to storage. (ImageCapture use case)
- Learn how to analyze frames from the camera in real time. (ImageAnalysis use case)
What you'll need
- An Android device. Android Studio's emulator works too. We recommend using an AVD that is based on Android 11 or higher.
- Minimum supported API level is 21.
- Android Studio 3.6 or above.
- Using the Android Studio menu, start a new project and select Empty Activity when prompted.
- 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
- 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-rc01"
// 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-alpha19"
- 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 afterbuildTypes
, add the following:
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
- If it is not already added, add this plugin at the top.
apply plugin: 'kotlin-android-extensions'
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 this code:
- 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
- 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.
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.nio.ByteBuffer
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.ExecutorService
typealias LumaListener = (luma: Double) -> Unit
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() {}
private fun startCamera() {}
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)
}
}
Before the app opens the camera, it needs permission from the user to do so. In this step, you'll implement camera permissions.
- Open
AndroidManifest.xml
and add these lines before theapplication
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.
- 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()
}
}
}
- Check if the request code is correct; ignore it otherwise.
if (requestCode == REQUEST_CODE_PERMISSIONS) {
}
- If the permissions are granted, call
startCamera()
.
if (allPermissionsGranted()) {
startCamera()
}
- If permissions are not granted, present a toast to notify the user that the permissions were not granted.
else {
Toast.makeText(this,
"Permissions not granted by the user.",
Toast.LENGTH_SHORT).show()
finish()
}
- 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 bind to the CameraX lifecycle.
- 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
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(viewFinder.surfaceProvider)
}
// 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)
} 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 aRunnable
as one argument. We will fill it in later. AddContextCompat
.getMainExecutor()
as the second argument. This returns anExecutor
that runs on the main thread.
cameraProviderFuture.addListener(Runnable {}, ContextCompat.getMainExecutor(this))
- In the
Runnable
, add aProcessCameraProvider
. This is used to bind the lifecycle of your camera to theLifecycleOwner
within the application's process.
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
- Initialize your
Preview
object, call build on it, get a surface provider from viewfinder, and then set it on the preview.
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(viewFinder.surfaceProvider)
}
- Create a
CameraSelector
object and selectDEFAULT_BACK_CAMERA
.
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
- Create a
try
block. Inside that block, make sure nothing is bound to yourcameraProvider
, and then bind yourcameraSelector
and preview object to thecameraProvider
.
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
this, cameraSelector, preview)
}
- There are a few ways this code could fail, like if the app is no longer in focus. Wrap this code in a
catch
block to log if there's a failure.
catch(exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
- Run the app. You should see a camera preview!
Other use cases work in a very similar way as Preview
. First, you define a configuration object that is used to instantiate the actual use case object. To capture photos, you'll implement the takePhoto()
method, which is called when the Take photo 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 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. This will be null If you tap the photo button before image capture is set up. Without thereturn
statement, the app would crash if it wasnull
.
val imageCapture = imageCapture ?: return
- Next, create a file to hold the image. Add in 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. This object is 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 yourphotoFile
.
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
- Call
takePicture()
on theimageCapture
object. Pass inoutputOptions
, the executor, and a callback for when the image is saved. You'll fill out the callback next.
imageCapture.takePicture(
outputOptions, ContextCompat.getMainExecutor(this), object : ImageCapture.OnImageSavedCallback {}
)
- In the case that the image capture fails or saving the image capture fails, add in an error case to log that it failed.
override fun onError(exc: ImageCaptureException) {
Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
}
- If the capture doesn't fail, the photo was taken successfully! Save the photo to the file you created earlier, present a toast to let the user know it was successful, and print a log statement.
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)
}
- 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 thetry
block to include the new use case:
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture)
The method will look like this at this point:
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.surfaceProvider)
}
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))
}
- 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
- Check the log statements. You will 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
- 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
- In the Android Studio terminal, run these commands:
adb shell
cp [INSERT THE FILE FROM STEP 2 HERE] /sdcard/Download/photo.jpg
- Run this ADB command, and then exit the shell:
adb pull /sdcard/Download/photo.jpg
- 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 that implements the ImageAnalysis.Analyzer
interface, and which will be called with incoming camera frames. You won't have to manage the camera session state or even dispose of images; binding to our app's desired lifecycle is sufficient, like with other lifecycle-aware components.
- Add this analyzer in as an inner class in
MainActivity.kt
. The analyzer logs the average luminosity of the image. To create an analyzer, you override theanalyze
function in a class that implements theImageAnalysis.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,
similar to other use cases, and update the startCamera()
function once again, before the call to CameraX.bindToLifecycle()
:
- In the
startCamera()
method, add this code under theimageCapture()
code.
val imageAnalyzer = ImageAnalysis.Builder()
.build()
.also {
it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
Log.d(TAG, "Average luminosity: $luma")
})
}
- Update the
bindToLifecycle()
call on thecameraProvider
to include theimageAnalyzer
.
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture, imageAnalyzer)
The full method will now look like this:
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.surfaceProvider)
}
imageCapture = ImageCapture.Builder()
.build()
val imageAnalyzer = ImageAnalysis.Builder()
.build()
.also {
it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
Log.d(TAG, "Average luminosity: $luma")
})
}
// 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, imageAnalyzer)
} catch(exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}, ContextCompat.getMainExecutor(this))
}
- 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:
- Included CameraX dependencies into your project.
- Displayed a camera viewfinder (using
Preview
use case) - Implemented photo capture, saving images to storage (using ImageCapture use case)
- Implemented analysis of frames from the camera in real time (using ImageAnalysis use case)
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.