Smile, it’s CameraX! [preview and capture]

In my previous article about CameraX I covered the challenges we are facing by using oldest Camera APIs and what are the advantages of using this new API.

Use-case-driven approach is one of the most important advantages of using CameraX. UseCase is an abstract class and currently it supports 3 implementations, and each use case is fully independent. We could use only one of them or we could combine them.

Preview Use Case

Use a surface to display a a live feed of the camera.

For each use case first of all we will check the implementation steps and after that the assigned code for each step.

Step 1: add gradle dependencies

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
def camerax = "1.0.0-beta07"
implementation "androidx.camera:camera-camera2:${camerax}"
implementation "androidx.camera:camera-lifecycle:${camerax}"
implementation 'androidx.camera:camera-view:1.0.0-alpha11'
implementation 'androidx.camera:camera-extensions:1.0.0-alpha11'
view raw build.gradle hosted with ❤ by GitHub

Step 2: permission handling

// manifest
<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />
// fragment or activity
if (areAllPermissionsGranted()) {
startCamera()
} else {
ActivityCompat.requestPermissions(
this, PERMISSIONS, CAMERA_REQUEST_PERMISSION_CODE)
}
  • 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.
  • If we use android.hardware.camera without .any, it will not work if you have a device without a back camera, such as most Chromebooks.  The second line adds the permission to access that camera.

Step 3: add PreviewView in a layout

<FrameLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.camera.view.PreviewView
android:id="@+id/preview"
android:layout_width="match_parent"
android:layout_height="match_parent" >
</androidx.camera.view.PreviewView>
</FrameLayout>
view raw main_layout.xml hosted with ❤ by GitHub
  • Since beta release, PreviewView is the recommended way of implementing camera preview.
  • If you want to use your own surface (TextureView, for example), then you must implement your own surface provider as well as making sure that you handle sizing and orientation appropriately, which can be tricky.

What’s happening behind the scenes?

  • PreviewView is a custom view that displays the camera feed for CameraX’s preview use case. 
  • PreviewView also extends the FrameLayout which we all know that is a ViewGroup.
  • The default implementation is SurfaceView.
  • But if the camera device is running in backward compatibility mode, in code it means that devices have a supported camera hardware level CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,  the actual implementation mode will be TextureView.
val previewView = findViewById(R.id.preview)
// initialize the Preview object (current use case)
val preview = Preview.Builder().build()

For fine control when CameraX gets initialized, we can implement the CameraXConfig.Provider interface in our Application class.

Step 4: create an instance of the ProcessCameraProvider

val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
// used to bind the lifecycle of camera to the lifecycle owner
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
// add a listener to the cameraProviderFuture
val cameraExecutor = ContextCompat.getMainExecutor(this)
cameraProviderFuture.addListener(Runnable {}, cameraExecutor)
  • ProcessCameraProvider is a singleton which can be used to bind the lifecycle of camera to any LifecycleOwner within an application’s process.
  • Only a single process camera provider can exist within a process, and it can be retrieved with getInstance(Context).
  • It help us to not worry about opening and closing the camera since CameraX is lifecycle aware.
  • cameraExecutor is an Executor that runs on the main thread.
  • Add a listener to the cameraProviderFuture that contains 2 arguments: a Runnable and the executor.

Step 5: select Camera and bind it to the lifecycle

val backCamera = CameraSelector.LENS_FACING_BACK;
val frontCamera = CameraSelector.LENS_FACING_FRONT;
val cameraSelector = CameraSelector.Builder().requireLensFacing(backCamera).build()
cameraProviderFuture.addListener(Runnable {
cameraProvider.unbindAll();
camera = cameraProvider.bindToLifecycle(
this as LifecycleOwner,
cameraSelector,
preview
)
preview?.setSurfaceProvider(previewView.createSurfaceProvider(camera?.cameraInfo))
}, cameraExecutor)
  • This code will be added in the Runnable
  • Also it is recommended to create a try block and inside of it to add this code. This code can fail, for example if the app is no longer in focus.
  • Unbind use cases before rebinding.
  • It is recommended that we bind our use cases to the lifecycle via CameraX in a single call and using the same lifecycle owner.
  • By providing all the use cases at once, we give CameraX the opportunity to find a compromise such that all use cases can run.

Capture Use Case

It used to save high-quality images.

Step 1: create ImageCapture reference

val imageCapture = ImageCapture.Builder().build()
// SETUP CAPTURE MODE
// to optimize photo capture for quality
val captureMode = ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY
// to optimize photo capture for latency (default)
val captureMode = ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY
imageCapture = ImageCapture.Builder()
.setCaptureMode(captureMode)
.build()
// SETUP FLASH MODE
// flash will always be used when taking a picture
val flashMode = ImageCapture.FLASH_MODE_ON
// flash will never be used when taking a picture (default)
val flashMode = ImageCapture.FLASH_MODE_OFF
// flash will be used according to the camera system's determination
val flashMode = ImageCapture.FLASH_MODE_AUTO
imageCapture = ImageCapture.Builder()
.setFlashMode(flashMode)
.build()
// SETUP ASPECT RATIO
// 16:9 standard aspect ratio
val aspectRatio = AspectRatio.RATIO_16_9
// 4:3 standard aspect ratio (default)
val aspectRatio = AspectRatio.RATIO_4_3
imageCapture = ImageCapture.Builder()
.setTargetAspectRatio(aspectRatio)
.build()
// SETUP TARGET RESOLUTION
val metrics = DisplayMetrics().also { previewView.display.getRealMetrics(it) }
val screenSize = Size(metrics.widthPixels, metrics.heightPixels)
imageCapture = ImageCapture.Builder()
.setTargetResolution(screenSize)
.setTargetName("CameraConference")
.build()
  • If we want to have more control when an image is captured we could add to the builder different features like photo capture optimizations, flash mode, aspect ratio or target resolution.
  • Capture mode: if not set the default value is ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY
  • Flash mode: if not set the default value is  ImageCapture.FLASH_MODE_OFF
  • Aspect ratio: if not set, resolutions with aspect ratio 4:3 will be considered in higher priority.
  • setTargetResolution: if not set, the largest available resolution will be selected to use.  
  • setTargetName (debug purpose): if not set, the target name will default to a unique name automatically generated with the class canonical name and random UUID

Step 2: add orientation event listener

val orientationEventListener = object : OrientationEventListener(this as Context) {
override fun onOrientationChanged(orientation: Int) {
val rotation: Int = when (orientation) {
in 45..134 -> Surface.ROTATION_270
in 135..224 -> Surface.ROTATION_180
in 225..314 -> Surface.ROTATION_90
else -> Surface.ROTATION_0
}
// default => Display.getRotation()
imageCapture.targetRotation = rotation
}
}
orientationEventListener.enable()

Step 3: image file management

val file = File(
externalMediaDirs.first(),
"${System.currentTimeMillis()}.jpg"
)
val outputFileOptions = ImageCapture.OutputFileOptions.Builder(file).build()
  • ImageCapture.OutputFileOptions give us options for saving the captured image
  • The builder of this class is used to configure the save location that can be either:
    • a File (like in the code sample)
    • a  MediaStore or 
    • an OutputStream.

Step 4: call takePicture()

imageCapture.takePicture(outputFileOptions, cameraExecutor,
object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(outputFileResult: ImageCapture.OutputFileResults) {
// yey!!! 🙂
}
override fun onError(exception: ImageCaptureException) {
// ohhh!!! 😦
}
})
  • This method captures the image and saves it to a file provided as an argument (the first one)
  • The second argument represents the executor in which the callback methods will be run
  • The callback will be called only once for every invocation of this method and it contains the implementation of 2 methods
    • onImageSaved: called when an image has been successfully saved
    • onError: called when an error occurs while trying to save the image

Step 5: update the call to bind lifecycle

// bind the image capture use case to camera
camera = cameraProvider.bindToLifecycle(
this as LifecycleOwner,
cameraSelector,
preview,
imageCapture
)

The Image Capture use case is designed to capture high definition and high quality photos and provides auto white balance, auto exposure and auto focus (3A) functions, as well as simple manual camera control. The caller is responsible for deciding how to use the captured image:

Enjoy and feel free to leave a comment if something is not clear or if you have questions. And if you like it please share !

Thank you for reading! 🙌🙏😍✌

Follow me on: Twitter | Medium | Dev.to

Leave a comment