Kshitij Aggarwal
Posted on May 18, 2020
CameraX is probably one of the most requested API's that most developers dealing with the Android camera API setup are looking forward to. What the team developing the CameraX API is doing is no easy feat and this also shows in the development path of the API with pretty massive changes happening throughout the Alpha development stage. (Release notes & history)
This blog post is first of the series on CameraX where I try and explore the API (1.0.0-beta03 at the time of writing this article) in a more in-depth manner at an implementation level with a real-use setup (read 'fragments') since most of the CameraX explorations I have found have been prior to the 1.0.0-alpha7 release, which by now have been changed significantly.
Implementation Steps
The following are the implementation steps I followed to get a simple camera preview using CameraX
- Include the following gradle dependencies in your project level
build.gradle
dependencies {
// CameraX core library
def camerax_version = '1.0.0-beta03'
implementation "androidx.camera:camera-core:$camerax_version"
// CameraX Camera2 extensions
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-alpha10'
implementation 'androidx.fragment:fragment:1.2.4'
}
- Add the
PreviewView
component in the fragment layout XML
<androidx.camera.view.PreviewView
android:id="@+id/view_finder"
android:layout_width="match_parent"
android:layout_height="match_parent" />
- Add the Camera permissions to the
AndroidManifest.xml
and implement the permissions provider in your fragment
<uses-permission android:name="android.permission.CAMERA" />
- Add the following method to add the camera preview use-case to the fragment
private fun bindCameraUseCases() {
// Get screen metrics used to setup camera for full screen resolution
val metrics = DisplayMetrics().also { viewFinder.display.getRealMetrics(it) }
Log.d(TAG, "Screen metrics: ${metrics.widthPixels} x ${metrics.heightPixels}")
val screenAspectRatio = aspectRatio(metrics.widthPixels, metrics.heightPixels)
Log.d(TAG, "Preview aspect ratio: $screenAspectRatio")
val rotation = viewFinder.display.rotation
// Bind the CameraProvider to the LifeCycleOwner
val cameraSelector = CameraSelector.Builder().requireLensFacing(lensFacing).build()
val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
cameraProviderFuture.addListener(Runnable {
// CameraProvider
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
// Preview
preview = Preview.Builder()
// We request aspect ratio but no resolution
.setTargetAspectRatio(screenAspectRatio)
// Set initial target rotation
.setTargetRotation(rotation)
.build()
// Must unbind the use-cases before rebinding them
cameraProvider.unbindAll()
try {
// A variable number of use-cases can be passed here -
// camera provides access to CameraControl & CameraInfo
camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview)
// Attach the viewfinder's surface provider to preview use case
preview?.setSurfaceProvider(viewFinder.createSurfaceProvider(camera?.cameraInfo))
} catch (exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}, ContextCompat.getMainExecutor(requireContext()))
}
- Add the following helper method to find the correct aspect ratio of the device and set it to CameraX
private fun aspectRatio(width: Int, height: Int): Int {
val previewRatio = max(width, height).toDouble() / min(width, height)
if (abs(previewRatio - RATIO_4_3_VALUE) <= abs(previewRatio - RATIO_16_9_VALUE)) {
return AspectRatio.RATIO_4_3
}
return AspectRatio.RATIO_16_9
}
- Start the camera
private fun startCamera() {
viewFinder.post {
bindCameraUseCases()
}
}
That's all folks. You just need to handle the camera permissions in the usual manner and the above code will give you the basic camera preview setup. Now lets dig a little deeper and see what are we actually doing here.
Explanation
-
Find the device aspect ratio and rotation values
We will use the
DisplayMetrics
API to find these values and use them later in both setting up CameraX as well as in future scenarios send this to MLKit.
val metrics = DisplayMetrics().also { viewFinder.display.getRealMetrics(it) }
val screenAspectRatio = aspectRatio(metrics.widthPixels, metrics.heightPixels)
val rotation = viewFinder.display.rotation
-
Select which camera to use and create a
ListenableFuture
of theProcessCameraProvider
val cameraSelector = CameraSelector.Builder().requireLensFacing(lensFacing).build()
val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
A ListenableFuture
in short allows you to register callbacks to be executed once the asynchronous computation is complete, or if the computation is already complete, immediately. You can find more details in the official documentation
Callback for this cameraProviderFuture
contains the logic for when the ProcessCameraProvider
is available to prepare it to be initialized by binding it to the lifecycle and required use cases
- Prepare the camera preview use case Here we prepare the camera preview use case using the aspect ratio and rotation values calculated earlier.
preview = Preview.Builder()
// We request aspect ratio but no resolution
.setTargetAspectRatio(screenAspectRatio)
// Set initial target rotation
.setTargetRotation(rotation)
.build()
Use-cases provide a very clear abstraction between the three main things we generally use camera for - preview the camera feed, capture from the camera feed and analyse the captured frames. CameraX provide these three use-cases as separate classes which we can extend as per our requirement. You can find more details about the use-cases in the official documentation and we will explore them further in future articles in this series.
-
Bind the
ProcessCameraProvider
to the lifecycle along with the use-cases First we have to unbind all the existing use-cases from the camera provider. If not it will throw runtime exception
cameraProvider.unbindAll()
Binding the ProcessCameraProvider
to the lifecycle is a must as this allows the CameraX API to automatically manage the camera resources bases on the provided lifecycle, reducing a lot of boilerplate code needed earlier with Camera2
API. We also attach our use-case here, Preview
use-case in our sample, for the CameraX API to show the camera preview. We now take this post-binding camera instance and set it to the PreviewView
object from the layout xml. This is where the preview will be rendered on the UI
try {
// A variable number of use-cases can be passed here -
// camera provides access to CameraControl & CameraInfo
camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview)
// Attach the viewfinder's surface provider to preview use case
preview?.setSurfaceProvider(viewFinder.createSurfaceProvider(camera?.cameraInfo))
} catch (exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
-
Start the camera
Calling the 'bind' code in
post
on theviewFinder
ensures that the code is called on the UI thread, which is mandatory for initiating the CameraX API. Not doing this will give a run-time exception.
viewFinder.post {
bindCameraUseCases()
}
And now we have a fully functional camera preview app using CameraX. You can find the complete source code here: https://github.com/funkyidol/CameraXSample.
Hope this helps & give me a follow to receive notifications for future articles in this series.
Posted on May 18, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.