天天看點

Android Camera2 預覽功能實作

1. 概述

最近在做一些關于人臉識别的項目,需要用到 Android 相機的預覽功能。網上查閱相關資料後,發現 Android 5.0 及以後的版本中,原有的 Camera API 已經被 Camera2 API 所取代。

全新的 Camera2 在 Camera 的基礎上進行了改造,大幅提升了 Android 系統的拍照功能。它通過以下幾個類與方法來實作相機預覽時的工作過程:

•CameraManager :攝像頭管理器,主要用于檢測系統攝像頭、打開系統攝像頭等;

•CameraDevice : 用于描述系統攝像頭,可用于關閉相機、建立相機會話、發送拍照請求等;

•CameraCharacteristics :用于描述攝像頭所支援的各種特性;

•CameraCaptureSession :當程式需要預覽、拍照時,都需要先通過 CameraCaptureSession 來實作。該會話通過調用方法 setRepeatingRequest() 實作預覽;

•CameraRequest :代表一次捕獲請求,用于描述捕獲圖檔的各種參數設定;

•CameraRequest.Builder :負責生成 CameraRequest 對象。

2. 相機預覽

下面通過源碼來講解如何使用 Camera2 來實作相機的預覽功能。

2.1 相機權限設定

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

2.2 App 布局

•activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
 xmlns:tools="http://schemas.android.com/tools"
 android:id="@+id/container"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 android:background="#000"
 tools:context=".MainActivity">
</FrameLayout>
•fragment_camera.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
 xmlns:tools="http://schemas.android.com/tools"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 tools:context=".CameraFragment">
 <com.lightweh.camera2preview.AutoFitTextureView
 android:id="@+id/textureView"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:layout_centerVertical="true"
 android:layout_centerHorizontal="true" />
</RelativeLayout>           

2.3 相機自定義View

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);
 }
 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(width, width * mRatioHeight / mRatioWidth);
 } else {
 setMeasuredDimension(height * mRatioWidth / mRatioHeight, height);
 }
 }
 }
}           

2.4 動态申請相機權限

public class MainActivity extends AppCompatActivity {
 private static final int REQUEST_PERMISSION = 1;
 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_main);
 if (hasPermission()) {
 if (null == savedInstanceState) {
 setFragment();
 }
 } else {
 requestPermission();
 }
 }
 @Override
 public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
 if (requestCode == REQUEST_PERMISSION) {
 if (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
 setFragment();
 } else {
 requestPermission();
 }
 } else {
 super.onRequestPermissionsResult(requestCode, permissions, grantResults);
 }
 }
 // 權限判斷,當系統版本大于23時,才有必要判斷是否擷取權限
 private boolean hasPermission() {
 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
 return checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED;
 } else {
 return true;
 }
 }
 // 請求相機權限
 private void requestPermission() {
 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
 if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
 Toast.makeText(MainActivity.this, "Camera permission are required for this demo", Toast.LENGTH_LONG).show();
 }
 requestPermissions(new String[]{Manifest.permission.CAMERA}, REQUEST_PERMISSION);
 }
 }
 // 啟動相機Fragment
 private void setFragment() {
 getSupportFragmentManager()
 .beginTransaction()
 .replace(R.id.container, CameraFragment.newInstance())
 .commitNowAllowingStateLoss();
 }
}           

2.5 開啟相機預覽

首先,在onResume()中,我們需要開啟一個 HandlerThread,然後利用該線程的 Looper 對象建構一個 Handler 用于相機回調。

@Override
public void onResume() {
 super.onResume();
 startBackgroundThread();

 // When the screen is turned off and turned back on, the SurfaceTexture is 
 // already available, and "onSurfaceTextureAvailable" will not be called. In 
 // that case, we can open a camera and start preview from here (otherwise, we 
 // wait until the surface is ready in the SurfaceTextureListener).
 if (mTextureView.isAvailable()) {
 openCamera(mTextureView.getWidth(), mTextureView.getHeight());
 } else {
 mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
 }
}
private void startBackgroundThread() {
 mBackgroundThread = new HandlerThread("CameraBackground");
 mBackgroundThread.start();
 mBackgroundHandler = new Handler(mBackgroundThread.getLooper());
}
同時,在 onPause() 中有對應的 HandlerThread 關閉方法。

當螢幕關閉後重新開啟,SurfaceTexture 已經就緒,此時不會觸發 onSurfaceTextureAvailable 回調。是以,我們判斷 mTextureView 如果可用,則直接打開相機,否則等待 SurfaceTexture 回調就緒後再開啟相機。

private void openCamera(int width, int height) {
 if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.CAMERA)
 != PackageManager.PERMISSION_GRANTED) {
 return;
 }
 setUpCameraOutputs(width, height);
 configureTransform(width, height);
 Activity activity = getActivity();
 CameraManager manager = (CameraManager) activity.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, mBackgroundHandler);
 } catch (CameraAccessException e) {
 e.printStackTrace();
 } catch (InterruptedException e) {
 throw new RuntimeException("Interrupted while trying to lock camera opening.", e);
 }
}
開啟相機時,我們首先判斷是否具備相機權限,然後調用 setUpCameraOutputs 函數對相機參數進行設定(包括指定攝像頭、相機預覽方向以及預覽尺寸的設定等),接下來調用 configureTransform 函數對預覽圖檔的大小和方向進行調整,最後擷取 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 = getActivity();
 if (null != activity) {
 activity.finish();
 }
 }
};           

相機開啟時還會指定相機的狀态變化回調函數 mStateCallback,如果相機成功開啟,則開始建立相機預覽會話。

private void createCameraPreviewSession() {
 try {
 // 擷取 texture 執行個體
 SurfaceTexture texture = mTextureView.getSurfaceTexture();
 assert texture != null;
 // 設定 TextureView 緩沖區大小
 texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
 // 擷取 Surface 顯示預覽資料
 Surface surface = new Surface(texture);
 // 建構适合相機預覽的請求
 mPreviewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
 // 設定 surface 作為預覽資料的顯示界面
 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, mBackgroundHandler);
  } catch (CameraAccessException e) {
  e.printStackTrace();
  }
  }
  @Override
  public void onConfigureFailed(
  @NonNull CameraCaptureSession cameraCaptureSession) {
  showToast("Failed");
  }
 }, null
 );
 } catch (CameraAccessException e) {
 e.printStackTrace();
 }
}           

以上便是 Camera2 API 實作相機預覽的主要過程。

歡迎加入Android開發技術交流QQ群;701740775

本群提供Android進階開發資料、進階UI、性能優化、架構師課程、NDK、混合式開發(ReactNative+Weex)等相關資料和解答

不懂得問題都可以在本群提出來 還會有職業生涯規劃以及面試指導

進群修改群備注:開發年限-地區-經驗

友善架構師解答問題

繼續閱讀