The least you can do with Camera2 API

For more insights visit Coders Bible — my new personal space, where I can help you become better developer!

After setting minSdkVersion of my latest project to 21, I wanted to provide those few users with great experience I encountered in Snapchat, Instagram or recently in Messenger, which was based on having embedded camera preview.

I dived deep into docs and talks about a new API Android 5.0 provided developers with, called Camera2, and after some frustrations I decided to share a recipe for a minimal application utilizing features of this class — simple “click-to-take-photo” app. Let’s start with basics.

Firstly we need a TextureView. This View class (with its underlying SurfaceTexture) is made for one purpose only — displaying content stream, which in our case is a stream from a camera process. Let’s also add a button which later will be responsible for taking photos.

FYI I also set <item>"android:windowTranslucentStatus">true</item> in styles.xml in order for my camera preview to have as much screen space as possible.

Next let’s start some preparation in onCreate(). Basically we need to request user to grant us access to camera (runtime permission). Then we’ll ask for system service called CAMERA_SERVICE. Finally we want to define default camera facing (front or back) and surfaceTextureListener. This last one is used to let us know when our TextureView and the core class laying beneath it (SurfaceTexture) are loaded. This is precisely when we’ll set up our camera and then open it.

For now, let’s forget about setting this listener to a textureView in a proper place. Surely you’re wondering about nuts and bolts of creating proper preview which are methods setUpCamera() and openCamera().

In setUpCamera() we need to go through a device’s list of cameras that cameraManager is providing and find the one with a LENS_FACING characteristic equal to our default cameraFacing. After establishing a correct camera we need to retrieve info about available preview sizes to scale our TextureView accordingly (StreamConfigurationMap is helpful here). Method getOutputSizes() returns an array of Size objects prepared for class SurfaceTexture. The zeroth element is the resolution we want — the highest available one.

Don’t forget to openCamera()! It may seem like a simple task — check for permission and then open it — but there are few things involved here. First of all we can’t overload main thread too much (hence backgroundHandler). Secondly we need some listener indicating when camera is opened and when it failed to do so (hence stateCallBack).

Creating background thread with looper doesn’t need lots of explanation (I’ll let you know later where to invoke a method below).

Let’s take a closer look at initializing stateCallback back in onCreate(). There are three states that camera can be in. Either it’s opened and everything is fine or it’s disconnected or it’s closed due to an error. In the first case we want to store our cameraDevice object inside activity’s field and call createPreviewSession(). In the remaining two we just do some cleaning.

Before we jump into the last step, which is implementing createPreviewSession(), I think we should take care of little things that we left behind:

  1. Setting surfaceTextureListener.
  2. Invoking openBackgroundThread().
  3. Avoiding memory leaks.

Ad 1 & 2.

Best place to set surfaceTextureListener as well as invoke openBackgroundThread() is onResume(). We also want to make sure that we don’t listen for surface texture events unnecessarily by checking whether textureView is already available.

Ad 3.

That one tends to be forgotten. Camera and background thread we created are potential memory leaks. Best place to clean them up is onStop().

Without further ado let me explain how createPreviewSession() works. I mentioned SurfaceTexture as the core class behind TextureView. We need to get our hands on it and properly set its bounds (with a small help of previewSize). Then we create a captureRequestBuilder with a default template and specify its target, which is Surface object based on SurfaceTexture acquired previously. Now everything fits together — we use cameraDevice to create capture session utilizing previewSurface , CameraCaptureSession.StateCallback (for acting upon camera being configured) and backgroundHandler. One thing to note here is a need to invoke setRepeatingRequest(). From docs:

With this method, the camera device will continually capture images using the settings in the provided CaptureRequest, at the maximum rate possible.

Yeah. Exactly what we want, isn’t it? The outcome of above code should be as follows:

And now for the fun part we implement the onClick method for our fab camera button. It’s simply a matter of creating directories for images and capturing bitmap from our preview textureView.

Let’s add WRITE_EXTERNAL_STORAGE permission first inside onCreate():

ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA, 
Manifest.permission.WRITE_EXTERNAL_STORAGE}, CAMERA_REQUEST_CODE);

From within this callback we can also invoke method used to create app’s image directories.

You’ll also find helpful a method for creating an image file.

Putting it all together this is our onClick:

In order to provide user with a standard photo taking experience we need to lock preview for a short time after photo has been taken (indicating capture moment via method capture()) and then unlock it (via calling setRepeatingRequest() one more time).

We’re passing null in capture() as well as setRepeatingRequest() because for this simple app we don’t care about output image’s metadata. Below is updated version of fab button onClick method:

Hope it was helpful.

EDIT:

I found issues with setting proper preview size on Nexus and Huawei devices. Texture view was too stretched either in height or width. If you stumble upon that yourself my advice is to use method below:

Essentially what it does is based on parameters width and height (in my case it was width and height of entire screen that I extracted using WindowManager) it calculates preferred ratio and using absolute value looks for the size from streamConfigurationMap
.getOutputSizes(SurfaceTexture.class)
that has ratio closest to preferred one.

Software Engineer & Business Technology Advisor