Android CameraX Overview

Filed Under: Android

In this tutorial, we’ll be discussing at length upon Android CameraX API. CameraX was one of the popular releases in Google IO 2019. Let’s see what it has in store for us.

What is Android CameraX?

Developing Camera Apps has always been hard. Getting a hold of the Camera APIs was never easy.

Hence, CameraX which is introduced in the Jetpack Support Library is a respite. It aims at making the development process easier.

Besides API complexity, while developing camera applications, we had to tackle a lot of scenarios such as:

  • OS Versions
  • Device Model specifications – Android Market is fragmented and has a vast variety of device configurations. Same Application can behave differently on different phones especially Samsung.

In this regards, CameraX strives to be more consistent across devices. CameraX basically uses Camera2 API under the hood but with a better implementation process. It is backward compatible until Android Lollipop (SDK 21).

CameraX provides the same consistency as the deprecated Camera 1 API by including the Camera 2 Legacy Layer.

How did Google do this?

Using an Automated CameraX test lab with a number of devices from various manufacturers.

CameraX has tackled the following issues as per Google IO 2019.

Android Camerax Fixes Google Source

Source: Google IO 2019

Android CameraX : Abstracting Camera2 API

Camera2 API provided us a lot of fine grain control over the sensors. More so, it required a lot of boilerplate code. It required communication with the HAL (Hardware Acceleration Layer) in order to access the hardware and drivers of the camera.

What CameraX essentially does is abstract all of this.

Hence CameraX provides ease of use, thanks to more readability and less boilerplate code.

Android CameraX : Use Cases

CameraX has come up with a use case-based approach to focus on the task you need to get done instead of spending time managing device-specific configurations.

The three core use cases are:

  • Preview – What you see. The Camera Feed.
  • Image Analysis – What you do. Processing the Camera Feed.
  • Image Capture – What you keep. Capturing the photo/video.

Android CameraX Extensions

Extensions basically allow us to use device-native camera features directly in our custom camera application with just a few lines of code.

Features like portrait mode, depth, Bokeh effect if supported by the device can be integrated into the use cases easily.

CameraX is lifecycle aware. So instead of starting and stopping the camera preview and other use cases in onResume() and onPause(), we can do that, using CameraX.bindToLifecycle().

Enough talk. Let’s deep dive into CameraX code now.

In the section below, we’ll be implementing two use cases – Preview and Image Capture. Image Analyzer would be covered in a separate tutorial.

CameraX Example Project Structure

Android Camerax Project Structure

Android Camerax Project Structure

CameraX Implementation Code

Let’s set up the build.gradle first:


implementation 'androidx.appcompat:appcompat:1.1.0-alpha03'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
def cameraxVersion = "1.0.0-alpha02"
implementation "androidx.camera:camera-core:${cameraxVersion}"
implementation "androidx.camera:camera-camera2:${cameraxVersion}"

We need to add the permissions for Camera and External Storage in the AndroidManifest.xml file.

The code for the activity_main.xml layout is given below:


<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextureView
        android:id="@+id/view_finder"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

    <ImageButton
        android:id="@+id/imgCapture"
        android:layout_width="72dp"
        android:layout_height="72dp"
        android:layout_margin="24dp"
        app:srcCompat="@android:drawable/ic_menu_camera"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

The code for the MainActivity.java class is given below.


package com.journaldev.androidcamerax;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.camera.core.CameraX;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCaptureConfig;
import androidx.camera.core.Preview;
import androidx.camera.core.PreviewConfig;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.LifecycleOwner;


import android.content.pm.PackageManager;
import android.graphics.Matrix;
import android.os.Bundle;
import android.os.Environment;
import android.util.Rational;
import android.util.Size;
import android.view.Surface;
import android.view.TextureView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;

import java.io.File;

public class MainActivity extends AppCompatActivity {


    private int REQUEST_CODE_PERMISSIONS = 101;
    private final String[] REQUIRED_PERMISSIONS = new String[]{"android.permission.CAMERA", "android.permission.WRITE_EXTERNAL_STORAGE"};
    TextureView textureView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        textureView = findViewById(R.id.view_finder);

        if(allPermissionsGranted()){
            startCamera(); //start camera if permission has been granted by user
        } else{
            ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS);
        }
    }

    private void startCamera() {

        CameraX.unbindAll();

        Rational aspectRatio = new Rational (textureView.getWidth(), textureView.getHeight());
        Size screen = new Size(textureView.getWidth(), textureView.getHeight()); //size of the screen


        PreviewConfig pConfig = new PreviewConfig.Builder().setTargetAspectRatio(aspectRatio).setTargetResolution(screen).build();
        Preview preview = new Preview(pConfig);

        preview.setOnPreviewOutputUpdateListener(
                new Preview.OnPreviewOutputUpdateListener() {
                    //to update the surface texture we  have to destroy it first then re-add it
                    @Override
                    public void onUpdated(Preview.PreviewOutput output){
                        ViewGroup parent = (ViewGroup) textureView.getParent();
                        parent.removeView(textureView);
                        parent.addView(textureView, 0);

                        textureView.setSurfaceTexture(output.getSurfaceTexture());
                        updateTransform();
                    }
                });


        ImageCaptureConfig imageCaptureConfig = new ImageCaptureConfig.Builder().setCaptureMode(ImageCapture.CaptureMode.MIN_LATENCY)
                .setTargetRotation(getWindowManager().getDefaultDisplay().getRotation()).build();
        final ImageCapture imgCap = new ImageCapture(imageCaptureConfig);

        findViewById(R.id.imgCapture).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                File file = new File(Environment.getExternalStorageDirectory() + "/" + System.currentTimeMillis() + ".png");
                imgCap.takePicture(file, new ImageCapture.OnImageSavedListener() {
                    @Override
                    public void onImageSaved(@NonNull File file) {
                        String msg = "Pic captured at " + file.getAbsolutePath();
                        Toast.makeText(getBaseContext(), msg,Toast.LENGTH_LONG).show();
                    }

                    @Override
                    public void onError(@NonNull ImageCapture.UseCaseError useCaseError, @NonNull String message, @Nullable Throwable cause) {
                        String msg = "Pic capture failed : " + message;
                        Toast.makeText(getBaseContext(), msg,Toast.LENGTH_LONG).show();
                        if(cause != null){
                            cause.printStackTrace();
                        }
                    }
                });
            }
        });

        //bind to lifecycle:
        CameraX.bindToLifecycle((LifecycleOwner)this, preview, imgCap);
    }

    private void updateTransform(){
        Matrix mx = new Matrix();
        float w = textureView.getMeasuredWidth();
        float h = textureView.getMeasuredHeight();

        float cX = w / 2f;
        float cY = h / 2f;

        int rotationDgr;
        int rotation = (int)textureView.getRotation();

        switch(rotation){
            case Surface.ROTATION_0:
                rotationDgr = 0;
                break;
            case Surface.ROTATION_90:
                rotationDgr = 90;
                break;
            case Surface.ROTATION_180:
                rotationDgr = 180;
                break;
            case Surface.ROTATION_270:
                rotationDgr = 270;
                break;
            default:
                return;
        }

        mx.postRotate((float)rotationDgr, cX, cY);
        textureView.setTransform(mx);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {

        if(requestCode == REQUEST_CODE_PERMISSIONS){
            if(allPermissionsGranted()){
                startCamera();
            } else{
                Toast.makeText(this, "Permissions not granted by the user.", Toast.LENGTH_SHORT).show();
                finish();
            }
        }
    }

    private boolean allPermissionsGranted(){

        for(String permission : REQUIRED_PERMISSIONS){
            if(ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED){
                return false;
            }
        }
        return true;
    }
}

PreviewConfig is where we configure the preview which is the live camera feed.

In the builder, we can set stuff like Aspect Ratios, Lens front or back, and target resolution.

The Preview is displayed on a TextureView.

ImageCaptureConfiguration.Builder() is configuration for image when captured. We set different configuration like MIN_LATENCY or MAX_QUALITY.

An output screenshot of the application in action is given below:

Android Camerax Output

Android Camerax Output

That brings an end to this tutorial. You can download the project from the link below or view the full source code from the Github Link provided underneath.

Comments

  1. Monique says:

    Hello! Thanks for the tutorial 🙂
    I followed all the steps of the tutorial. The camera opens normally, but the app is not saving the pic. Even if all the permissions granted, it is not allowing to save the pic. This is the error:

    2019-07-05 00:45:11.710 28373-28373/com.example.pegadamonique W/System.err: java.io.FileNotFoundException: /storage/emulated/0-1562298311316.png (Permission denied)
    2019-07-05 00:45:11.711 28373-28373/com.example.pegadamonique W/System.err: at java.io.FileOutputStream.open0(Native Method)
    2019-07-05 00:45:11.711 28373-28373/com.example.pegadamonique W/System.err: at java.io.FileOutputStream.open(FileOutputStream.java:308)
    2019-07-05 00:45:11.711 28373-28373/com.example.pegadamonique W/System.err: at java.io.FileOutputStream.(FileOutputStream.java:238)
    2019-07-05 00:45:11.711 28373-28373/com.example.pegadamonique W/System.err: at java.io.FileOutputStream.(FileOutputStream.java:180)
    2019-07-05 00:45:11.711 28373-28373/com.example.pegadamonique W/System.err: at androidx.camera.core.ImageSaver.run(ImageSaver.java:75)
    2019-07-05 00:45:11.711 28373-28373/com.example.pegadamonique W/System.err: at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
    2019-07-05 00:45:11.711 28373-28373/com.example.pegadamonique W/System.err: at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
    2019-07-05 00:45:11.711 28373-28373/com.example.pegadamonique W/System.err: at java.lang.Thread.run(Thread.java:764)

    1. Lugi says:

      He forgot to add the permissions on the AndroidManifest, this 2 lines and you will be ok

Leave a Reply

Your email address will not be published. Required fields are marked *

close
Generic selectors
Exact matches only
Search in title
Search in content
Search in posts
Search in pages