Building Android Camera Apps: Camera vs. Camera2
Developing Android camera apps can be more complex compared to iOS due to varying hardware specifications and vendor implementations. Since API level 21, the older Camera API has been deprecated in favor of the more powerful and flexible Camera2 API. In this post, we’ll explore how to create Android camera preview apps using both Camera and Camera2 APIs with minimal code, and compare the differences between these two approaches.
Building an Android Camera Preview App with Minimal Code
To get started, create a new project using the Empty Activity template.
First, declare the necessary camera permissions in your 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>
To make the app display in full screen, modify the values/styles.xml file:
<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>
Implementing Camera Preview with the Camera API
To keep the code simple, we won’t use an XML layout file. Instead, we’ll directly render the camera preview using a TextureView as the content view.
Here’s how you can implement a basic camera preview using the Camera API:
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);
}
}
Next, register the TextureView.SurfaceTextureListener
to monitor 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) {
}
When the surface is available, you can open the camera, set the necessary 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:
Addressing Preview Distortion Issues
Upon running the app, you may notice that the preview image becomes distorted when rotating the device. To fix this, you can use the AutoFitTextureView from Google’s Camera2 sample. However, this solution doesn’t support full-screen mode by default. Instead, you can use a modified version of AutoFitTextureView
that properly handles aspect ratio for full-screen previews:
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);
}
}
}
}
Implementing Camera Preview with the Camera2 API
Now, let’s replace the Camera API with the more advanced Camera2 API, using Google’s camera2basic sample as a reference.
Here’s how you can open the camera using 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);
}
}
Setting up the camera and calculating the aspect ratio for the preview can be 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) {
}
}
Once 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();
}
}
With Camera2, the total lines of code increase significantly, reflecting the complexity of this API.
Should You Migrate from Camera to Camera2?
The Camera2 API offers a wealth of advanced features, such as manual focus, exposure, and more precise controls over camera parameters, making it ideal for apps requiring complex camera functionalities. However, it is much more complicated and verbose than the older Camera API.
If your app only requires basic camera features, the Camera API might be sufficient. But if you need advanced control or plan to leverage modern camera features, it’s worth considering a migration to Camera2.