Android Camera Preview App: Camera vs. Camera2

Building Android camera apps is much more complicated than building iOS camera apps due to the different vendors and hardware specs. Since from API level 21, the old Camera class was deprecated and a brand-new Camera2 class was born. In this post, I want to use the least code to create Android camera preview apps with Camera and Camera2 respectively and compare the usage difference between the two sets of APIs.

Building Android Camera Preview App with Least Code

Create a new project with Empty Activity template.

Declare the camera-related use permissions in AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.dynamsoft.camera">

    <uses-permission android:name="android.permission.CAMERA" />

    <uses-feature android:name="android.hardware.camera" />
    <uses-feature android:name="android.hardware.camera.autofocus" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic\_launcher"
        android:label="@string/app\_name"
        android:roundIcon="@mipmap/ic\_launcher\_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity" android:screenOrientation="portrait">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

If you want to make the app displayed as full screen, modify values/styles.xml:

<resources>
    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
        <item name="android:windowFullscreen">true</item>
    </style>

</resources>

I want to make the code as simple as possible, so there is no XML UI layout file needed. Both TextureView and SurfaceView can be rendered as the camera preview. Here I use TextureView as the content view.

The code is pretty simple according to the TextureView API Documentation:

public class MainActivity extends AppCompatActivity implements TextureView.SurfaceTextureListener, ActivityCompat.OnRequestPermissionsResultCallback {

    private Camera mCamera;
    private TextureView mTextureView;

    @Override
    protected void onResume() {
        super.onResume();

        if (mTextureView.isAvailable()) {
            openCamera(mTextureView.getSurfaceTexture());
        } else {
            mTextureView.setSurfaceTextureListener(this);
        }
    }

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mTextureView = new TextureView(this);
        mTextureView.setSurfaceTextureListener(this);
        setContentView(mTextureView);
    }
}

Register TextureView.SurfaceTextureListener to monitor the surface events:

public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
    openCamera(surface);
}

public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
}

public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
    if (mCamera != null) {
        mCamera.stopPreview();
        mCamera.release();
    }

    return true;
}

public void onSurfaceTextureUpdated(SurfaceTexture surface) {
}

Once the surface is available, we can open a camera, set some parameters and start the camera preview:

private static final int REQUEST\_CAMERA\_PERMISSION = 1;
private static final SparseIntArray ORIENTATIONS = new SparseIntArray();
static {
    ORIENTATIONS.append(Surface.ROTATION\_0, 90);
    ORIENTATIONS.append(Surface.ROTATION\_90, 0);
    ORIENTATIONS.append(Surface.ROTATION\_180, 270);
    ORIENTATIONS.append(Surface.ROTATION\_270, 180);
}

private void openCamera(SurfaceTexture surface) {
    if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
            != PackageManager.PERMISSION\_GRANTED) {
        requestCameraPermission();
        return;
    }

    mCamera = Camera.open(0);
    try {
        mCamera.setPreviewTexture(surface);
        int rotation = getWindowManager().getDefaultDisplay()
                .getRotation();
        mCamera.setDisplayOrientation(ORIENTATIONS.get(rotation));
        Camera.Parameters params =  mCamera.getParameters();
        params.setFocusMode(Camera.Parameters.FOCUS\_MODE\_CONTINUOUS\_VIDEO);
        mCamera.setParameters(params);
        mCamera.startPreview();
    } catch (IOException ioe) {
    }
}

Build and run the app:

android camera preview distortion

Anything wrong? It seems the preview image is distorted when rotating my smartphone. To fix the issue, we can use AutoFitTextureView instead. However, the code provided by Google does not support full screen. An answer from StackOverflow can perfectly solve the issue:

import android.content.Context;
import android.util.AttributeSet;
import android.view.TextureView;

/**
 * A {@link TextureView} that can be adjusted to a specified aspect ratio.
 */
public class AutoFitTextureView extends TextureView {

    private int mRatioWidth = 0;
    private int mRatioHeight = 0;

    public AutoFitTextureView(Context context) {
        this(context, null);
    }

    public AutoFitTextureView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public AutoFitTextureView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    /**
     * Sets the aspect ratio for this view. The size of the view will be measured based on the ratio
     * calculated from the parameters. Note that the actual sizes of parameters don't matter, that
     * is, calling setAspectRatio(2, 3) and setAspectRatio(4, 6) make the same result.
     *
     * @param width  Relative horizontal size
     * @param height Relative vertical size
     */
    public void setAspectRatio(int width, int height) {
        if (width < 0 || height < 0) {
            throw new IllegalArgumentException("Size cannot be negative.");
        }
        mRatioWidth = width;
        mRatioHeight = height;
        requestLayout();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        if (0 == mRatioWidth || 0 == mRatioHeight) {
            setMeasuredDimension(width, height);
        } else {
            if (width < height \* mRatioWidth / mRatioHeight) {
                setMeasuredDimension(height \* mRatioWidth / mRatioHeight, height);
            } else {
                setMeasuredDimension(width, width \* mRatioHeight / mRatioWidth);
            }
        }
    }

}

So far, I’ve finished the MainActivity.java, which has approximately 118 lines of code. The Android camera preview app can run perfectly.

## Android camera preview

Next, I will replace Camera class with Camera2 class based on Google’s sample android-Camera2Basic.

Open the camera with CameraManager:

private final CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {

    @Override
    public void onOpened(@NonNull CameraDevice cameraDevice) {
        mCameraOpenCloseLock.release();
        mCameraDevice = cameraDevice;
        createCameraPreviewSession();
    }

    @Override
    public void onDisconnected(@NonNull CameraDevice cameraDevice) {
        mCameraOpenCloseLock.release();
        cameraDevice.close();
        mCameraDevice = null;
    }

    @Override
    public void onError(@NonNull CameraDevice cameraDevice, int error) {
        mCameraOpenCloseLock.release();
        cameraDevice.close();
        mCameraDevice = null;
        Activity activity = MainActivity.this;
        if (null != activity) {
            activity.finish();
        }
    }

};

private void openCamera(int width, int height) {
    if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
            != PackageManager.PERMISSION\_GRANTED) {
        requestCameraPermission();
        return;
    }

    setUpCameraOutputs(width, height);
    CameraManager manager = (CameraManager)this.getSystemService(Context.CAMERA\_SERVICE);
    try {
        if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
            throw new RuntimeException("Time out waiting to lock camera opening.");
        }
        manager.openCamera(mCameraId, mStateCallback, null);
    } catch (CameraAccessException e) {
        e.printStackTrace();
    } catch (InterruptedException e) {
        throw new RuntimeException("Interrupted while trying to lock camera opening.", e);
    }
}

Get the camera id and calculate the aspect ratio of TextureView. The calculation code is complex:

private void setUpCameraOutputs(int width, int height) {
    Activity activity = this;
    CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA\_SERVICE);
    try {
        for (String cameraId : manager.getCameraIdList()) {
            CameraCharacteristics characteristics
                    = manager.getCameraCharacteristics(cameraId);

            Integer facing = characteristics.get(CameraCharacteristics.LENS\_FACING);
            if (facing != null && facing == CameraCharacteristics.LENS\_FACING\_FRONT) {
                continue;
            }

            mCameraId = cameraId;

            StreamConfigurationMap map = characteristics.get(
                    CameraCharacteristics.SCALER\_STREAM\_CONFIGURATION\_MAP);
            if (map == null) {
                continue;
            }

            Size largest = Collections.max(
                    Arrays.asList(map.getOutputSizes(ImageFormat.JPEG)),
                    new CompareSizesByArea());

            int displayRotation = activity.getWindowManager().getDefaultDisplay().getRotation();
            mSensorOrientation = characteristics.get(CameraCharacteristics.SENSOR\_ORIENTATION);
            boolean swappedDimensions = false;
            switch (displayRotation) {
                case Surface.ROTATION\_0:
                case Surface.ROTATION\_180:
                    if (mSensorOrientation == 90 || mSensorOrientation == 270) {
                        swappedDimensions = true;
                    }
                    break;
                case Surface.ROTATION\_90:
                case Surface.ROTATION\_270:
                    if (mSensorOrientation == 0 || mSensorOrientation == 180) {
                        swappedDimensions = true;
                    }
                    break;
            }

            Point displaySize = new Point();
            activity.getWindowManager().getDefaultDisplay().getSize(displaySize);
            int rotatedPreviewWidth = width;
            int rotatedPreviewHeight = height;
            int maxPreviewWidth = displaySize.x;
            int maxPreviewHeight = displaySize.y;

            if (swappedDimensions) {
                rotatedPreviewWidth = height;
                rotatedPreviewHeight = width;
                maxPreviewWidth = displaySize.y;
                maxPreviewHeight = displaySize.x;
            }

            if (maxPreviewWidth > MAX\_PREVIEW\_WIDTH) {
                maxPreviewWidth = MAX\_PREVIEW\_WIDTH;
            }

            if (maxPreviewHeight > MAX\_PREVIEW\_HEIGHT) {
                maxPreviewHeight = MAX\_PREVIEW\_HEIGHT;
            }

            mPreviewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class),
                    rotatedPreviewWidth, rotatedPreviewHeight, maxPreviewWidth,
                    maxPreviewHeight, largest);

            int orientation = getResources().getConfiguration().orientation;
            if (orientation == Configuration.ORIENTATION\_LANDSCAPE) {
                mTextureView.setAspectRatio(
                        mPreviewSize.getWidth(), mPreviewSize.getHeight());
            } else {
                mTextureView.setAspectRatio(
                        mPreviewSize.getHeight(), mPreviewSize.getWidth());
            }

            return;
        }
    } catch (CameraAccessException e) {
        e.printStackTrace();
    } catch (NullPointerException e) {
    }
}

When the camera is opened, create a capture session to start the preview:

private void createCameraPreviewSession() {
    try {
        SurfaceTexture texture = mTextureView.getSurfaceTexture();
        assert texture != null;
        Surface surface = new Surface(texture);

        mPreviewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE\_PREVIEW);
        mPreviewRequestBuilder.addTarget(surface);
        mCameraDevice.createCaptureSession(Arrays.asList(surface),
                new CameraCaptureSession.StateCallback() {

                    @Override
                    public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
                        if (null == mCameraDevice) {
                            return;
                        }

                        mCaptureSession = cameraCaptureSession;
                        try {
                            mPreviewRequestBuilder.set(CaptureRequest.CONTROL\_AF\_MODE,
                                    CaptureRequest.CONTROL\_AF\_MODE\_CONTINUOUS\_PICTURE);

                            mPreviewRequest = mPreviewRequestBuilder.build();
                            mCaptureSession.setRepeatingRequest(mPreviewRequest,
                                    null, null);
                        } catch (CameraAccessException e) {
                            e.printStackTrace();
                        }
                    }

                    @Override
                    public void onConfigureFailed(
                            @NonNull CameraCaptureSession cameraCaptureSession) {
                    }
                }, null
        );
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}

Using Camera2, the total lines of code increased to 337.

Should We Migrate from Camera to Camera2?

It is obvious Camera2 is not compatible with Camera and much more complicated. If you do not need too many camera functionalities besides Android camera preview, the Camera class is enough. If you want to empower your app with advanced camera features, such as shutter speed control, it is worth migrating APIs from Camera to Camera2.

Source Code

https://github.com/yushulx/android-camera-preview