Android Camera 編程從入門到精通

一、前言

想通過一篇文章就讓我們精通 Android 的 Camera 那肯定是不可能的事情。但通過對 Android 中相機拍照的所有的方式的梳理和理解,包括直接調起相機拍照,Camera API 1 以及 Camera API 2 的分析與理解,為我們指明一條通往精通 Android Camera 的路還是有可能的。文章將先對 Android Camera 有一個全局的認知,然后再分析拍照的各個關鍵路徑及相關知識點。在實際開發過程中碰到問題再深入去了解 API 及其相關參數,應該就能解決我們在 Android Camera 編程中的大部分問題了。

二、相機基本使用以及 Camra API 1

這里主要涉及到的是如何直接調起系統相機拍照以及基于 Camra API 1 實現拍照。如下的思維導圖是一個基本的導讀。


Android 相機.jpg

1.權限及需求說明

要使用相機必須聲明 CAMERA 權限以及告訴系統你要使用這個功能。

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

上面這是最基本的,但如果你需要寫文件,錄音,定位等還需要下面的權限

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
...
<!-- Needed only if your app targets Android 5.0 (API level 21) or higher. -->
<uses-feature android:name="android.hardware.location.gps" />

2.調起系統或者三方相機直接拍照

  • 拍照
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
  • 獲取拍照后的照片
Uri contentUri = FileProvider.getUriForFile(this, "com.example.android.fileprovider", photoFile);

3.通過 Camera API 1 進行拍照

  • 相機設備檢測
(context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA))
  • 打開相機
Camera.open();
// Camera.open(0)
// Camera.open(1)
  • 創建預覽界面
/** A basic Camera preview class */
public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback {
    ......
    public CameraPreview(Context context, Camera camera) {
        ......
        mHolder.addCallback(this);
    }

    public void surfaceCreated(SurfaceHolder holder) {
       ......
            mCamera.setPreviewDisplay(holder);
            mCamera.startPreview();
       ......
    }

    public void surfaceDestroyed(SurfaceHolder holder) {
        ......
        mCamera.stopPreview();
    }

    public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
        ......
            mCamera.setPreviewDisplay(mHolder);
            mCamera.startPreview();
       ......
    }
}
...
}
  • 設置相機參數
public void setCamera(Camera camera) {
    ......
    if (mCamera != null) {
        List<Size> localSizes = mCamera.getParameters().getSupportedPreviewSizes();
        mSupportedPreviewSizes = localSizes;
        requestLayout();
       ......
      // 相機參數設置完成后,需要重新啟動預覽界面
      mCamera.setPreviewDisplay(mHolder);
      mCamera.startPreview();
       ......
    }
}
  • 停止預覽及釋放相機
    這個建議放在 onDestroy() 中調用
private void stopPreviewAndFreeCamera() {
    ......
        mCamera.stopPreview();
    ......
        mCamera.release();
        mCamera = null;
    }
}

以上就是如何通過調起系統或者三方相機以及通過調用 Camera API 1 來進行拍照的講解,相對來說還是比較簡單的。一般來說掌握 Camera API 1 的用法基本能滿足常規開發了,但當我們需要獲取更多相機設備的特性時,顯然需要通過 Camera API 2 所提供的更加豐富的功能來達到目的了。對于基本的拍照以及 API 1 的講解這里只是簡單過一下,重點在 API 2 的介紹。

三、全新 Camera API 2

Camera API 2 是從 Android 5.0 L 版本開始引入的。官網對相機介紹的引導文檔里是沒有涉及到 API 2 的講解的,都是基于 API 1 的。能找到的是其推薦的一篇博客Detecting camera features with Camera2 以及官方的 API 文檔。通過文檔大概了解到其比較重要的優點如下:

  • 改進了新硬件的性能。
  • 以更快的間隔拍攝圖像。
  • 顯示來自多個攝像頭的預覽。
  • 直接應用效果和過濾器。

看起來很爽,但是用起來那就是酸爽了,如下是梳理的一個思維導圖。看看就知道有多麻煩了。


Camera API 2 拍照.jpg

四、官方 demo 分析

正是由于 Camera 的 API 從 1 到 2 發生了架構上的變化,而且使用難度也是大大地增加了好幾倍,加上 Android 的碎片化又是如斯的嚴重。因此官方考慮到大家掌握不好,推出了其官方的 demo 供我們參考和學習——cameraview。這里也將基于官方的 demo 來深入掌握 Android 相機 API 2 的使用。

1. 主要類圖

先來看看工程中主要的類圖及其關系,好對整個工程以及 Camera2 中的相關類有一個基本的認知。

工程主類圖

(1) 類圖結構上封裝了 CameraView 用于給 Activity 直接調用。
(2) 抽象了相機類 CameraViewImpl 和預覽類 PreviewImpl。根據不同的版本由其具體實現類來解決版本之間的差異以及兼容。
(3) 用于預覽的既可以是 SurfaceView 也可以是 TextureView,框架內根據不同版本做了相應的適配。
(4) Camera1 即使用的舊版 Camera 及其相關的 API。而 Camera2 使用了新的 Camera2 API,這里簡要介紹一下這幾個類的作用。

序號 說明
1 CameraManager 這是一個系統服務,主要用于管理相機設備的,如相機的打開。與 AlarmManager 同等級。
2 CameraDevice 這個就是相機設備了,與 Camra1 中的 Camera 同等級。
3 ImageReader 用于從相機打開的通道中讀取需要的格式的原始圖像數據,理論上一個設備可以連接 N 多個 ImageReader。在這里可以看成是和 Preview 同等級。
4 CaptureRequest.Builder CaptureRequest 構造器,主要給相機設置參數的類。Builder 設計模式真好用。
5 CameraCharacteristics 與 CaptureRequest 反過來,主要是獲取相機參數的。
6 CameraCaptureSession 請求抓取相機圖像幀的會話,會話的建立主要會建立起一個通道。源端是相機,另一端是 Target,Target 可以是 Preview,也可以是 ImageReader。

2.CameraView 初始化

先看一看 CameraView 初始化的時序圖,大概一共做了 13 事情。當然,初始化做的事情其實都是簡單的,主要就是初始化必要的對象且設置一些監聽。

CameraView 初始化.jpg
  • CameraView 的構建方法
public CameraView(Context context, AttributeSet attrs, int defStyleAttr) {
       ......
        // 創建預覽視圖
        final PreviewImpl preview = createPreviewImpl(context);
        // Callback 橋接器,將相機內部的回調轉發給調用層
        mCallbacks = new CallbackBridge();
        // 根據不同的 SDK 版本選擇不同的 Camera 實現,這里假設選擇了 Camera2
        if (Build.VERSION.SDK_INT < 21) {
            mImpl = new Camera1(mCallbacks, preview);
        } else if (Build.VERSION.SDK_INT < 23) {
            mImpl = new Camera2(mCallbacks, preview, context);
        } else {
            mImpl = new Camera2Api23(mCallbacks, preview, context);
        }
       ......
        // 設置相機 ID,如前置或者后置
        setFacing(a.getInt(R.styleable.CameraView_facing, FACING_BACK));
        ......
        // 設置預覽界面的比例,如 4:3 或者 16:9
        setAspectRatio(AspectRatio.parse(aspectRatio));
        // 設置對焦方式
        setAutoFocus(a.getBoolean(R.styleable.CameraView_autoFocus, true));
       // 設置閃光燈
        setFlash(a.getInt(R.styleable.CameraView_flash, Constants.FLASH_AUTO));
       ......
        // 初始化顯示設備(主要指手機屏幕)的旋轉監聽,主要用來設置相機的旋轉方向
        mDisplayOrientationDetector = new DisplayOrientationDetector(context) {
            @Override
            public void onDisplayOrientationChanged(int displayOrientation) {
                mImpl.setDisplayOrientation(displayOrientation);
            }
        };
}

構造方法中所做的事情都在注釋里進行說明,沒有需要展開的。下面來看 createPreviewImpl()。

  • createPreviewImpl() 的實現
private PreviewImpl createPreviewImpl(Context context) {
        PreviewImpl preview;
        if (Build.VERSION.SDK_INT < 14) {
            preview = new SurfaceViewPreview(context, this);
        } else {
            preview = new TextureViewPreview(context, this);
        }
        return preview;
    }

這里的 SurfaceViewPreview 以及 TextureViewPreview 都是一個包裝類,從名字上就可以知道其內部分別包裝了 SurfaceView 和 TextureView 實例來實現相機的預覽界面的。關于 SurfaceView 以及 TextureView 的區別,這里也再簡單提一下,詳細的可以參考其他大神的文章說明:
SurfaceView:是一個獨立的 Window,由系統 WMS 直接管理,可支持硬件加速,也可以不支持硬件加速。
TextureView:可以看成是一個普通的 View,屬于所于應用的視圖層級樹中,屬于 ViewRootImpl 管理,只支持硬件加速。

盡管 SurfaceView 和 TextureView 有區別,但本質上它們都是對 Surface 的一個封裝實現。

這里假設選擇的是 TextureViewPreview。TextureViewPreview 的構造方法很簡單,就是從 xml 里獲取 TextureView 的實例,并且同時設置 TextureView 的監聽 TextureView.SurfaceTextureListener,這個后面會再詳細講。

接下來是根據不同的版本選擇 Camera,這里假設選擇的是 Camera2,主線上我們也只分析它就可以了。那么就來看一看 Camera2 的實現吧。

  • 初始化 Camera2
Camera2(Callback callback, PreviewImpl preview, Context context) {
        super(callback, preview);
        mCameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
        mPreview.setCallback(new PreviewImpl.Callback() {
            @Override
            public void onSurfaceChanged() {
                startCaptureSession();
            }
        });
    }

首先是初始化 CameraManager 的實例,這是相比 Camera1 多出來的步驟,這么說 Camera 有一個專業的管理者了。其次可以看到這里是向 Context 獲取一個系統 Service "CAMERA_SERVICE" 來初始化 CameraManager 的,這也說明了其被上升到了一個系統服務的高度了。

然后就是向 Preview 添加回調,監聽其 Surface 的變化來作進一步的事情。

  • 關于 Camera 與 Preview 的選擇
    這里 github 首頁給出了 Android 的推薦選擇。
API Level Camera API Preview View
9-13 Camera1 SurfaceView
14-20 Camera1 TextureView
21-23 Camera2 TextureView
24 Camera2 SurfaceView

API 20 以下用 Camera 1,20 以上用 Camera 2,這個沒有爭議。但是對于 Preview 的選擇也根據 API 來選擇, 這個就不應該了??催^其他相應的實現,除了 SDK API 的檢查,應用 TextureView 前還應該要判斷一下當前的運行環境是否支持硬件加速。

而讓我有疑問的是這里的 24 以上推薦使用 SurfaceView,這個是為什么呢?而其里面的代碼實際實現,看上面createPreviewImpl() 的實現可知又不是這樣的,也是選擇了 TextureView。

  • setFacing、setAspectRatio、setAutoFocus、setFlash
    這些都是設置參數,其實際生效的方法在 Camera2 中,而這個時候相機都還沒有打開,對于它們的設置目前來說是不會立即生效的,只是記錄下它們的值而已。后面我們分析時已默認值來分析即可。

當然,這里只是給出了 4 個參數,其實還有很多,后面還會講到。

小結


到這里就分析完了 CameraView 的初始化了,其主要做了以下幾件事情:
(1) 通過 getSystemService 初始化了 CameraManager。
(2) 準備好了 Preview ,用于相機的預覽
(3) 設定好了相機要用的參數

3.打開相機

同樣,先來看一看打開相機的時序圖。概括了有 15 個步驟,但其實關鍵步驟沒有這么多。


CameraView 打開相機.jpg
  • CameraView.start()
/**
     * Open a camera device and start showing camera preview. This is typically called from
     * {@link Activity#onResume()}.
     */
    public void start() {
        if (!mImpl.start()) {
            //store the state ,and restore this state after fall back o Camera1
            Parcelable state=onSaveInstanceState();
            // Camera2 uses legacy hardware layer; fall back to Camera1
            mImpl = new Camera1(mCallbacks, createPreviewImpl(getContext()));
            onRestoreInstanceState(state);
            mImpl.start();
        }
    }

這里給了幾個關鍵的信息:
(1) 此方法推薦的是在 Activity#onResume() 方法里面進行調用,這個是很重要的,告訴了我們打開相機的最適合時機。
(2) 按照前面的場景,這里調用了 Camera2#start()。這是要進一步分析的。
(3) 如果打開 Camera2 失敗了,則降級到 Camera1。做了回退保護,考慮的確實比較周到。

  • Camera2.start()
boolean start() {
        if (!chooseCameraIdByFacing()) {
            return false;
        }
        collectCameraInfo();
        prepareImageReader();
        startOpeningCamera();
        return true;
    }

都是內部調用,下面逐個分析這些方法的實現。

  • chooseCameraIdByFacing()
private boolean chooseCameraIdByFacing() {
        try {
            // 1.根據 mFacing 選擇相機
            int internalFacing = INTERNAL_FACINGS.get(mFacing);
            // 2.獲取所有的可用相機 ID 列表,注意相機的 ID 是 字串類型了
            final String[] ids = mCameraManager.getCameraIdList();
            ......
            for (String id : ids) {
                // 根據相機的 ID 獲取相機特征
                CameraCharacteristics characteristics = mCameraManager.getCameraCharacteristics(id);
                // 查詢其支持的硬件級別
                Integer level = characteristics.get(
                      CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
                if (level == null ||
                        level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
                    continue;
                }
                // 查詢相機的方向(前置,后置或者外接),也可以同等看成是其整型的 ID
                Integer internal = characteristics.get(CameraCharacteristics.LENS_FACING);
                if (internal == null) {
                    throw new NullPointerException("Unexpected state: LENS_FACING null");
                }
                // 查出來的與所期望的相等,則認為就是要找到的相機設備
                if (internal == internalFacing) {
                    // 保存相機的 ID
                    mCameraId = id;
                    // 保存相機的特征參數
                    mCameraCharacteristics = characteristics;
                    return true;
                }
            }
            // 如果沒找到就取第 0 個。后面的過程就跟上面是一樣的。這里就省略了。一般來說第 0 個就是 ID 為 "1" 的相機,其方向為后置。
            mCameraId = ids[0];
            ......
            return true;
        } catch (CameraAccessException e) {
            throw new RuntimeException("Failed to get a list of camera devices", e);
        }
    }

這段代碼確實有點長,并且信息量也多。其主要的目的是根據 mFacing 指定的相機方向選擇一個正確的相機,但如果沒有的話就默認選擇后置相機。這個過程涉及到了幾個比較重要的相機參數及其 API 調用。

(1) 關于選擇相機方向
相機方向主要是相對于手機屏幕而言的,系統可取的值有 LENS_FACING_FRONT(前置),LENS_FACING_BACK(后置),LENS_FACING_EXTERNAL(外接)。但工程里只給我們定義了前置與后置。

static {
        INTERNAL_FACINGS.put(Constants.FACING_BACK, CameraCharacteristics.LENS_FACING_BACK);
        INTERNAL_FACINGS.put(Constants.FACING_FRONT, CameraCharacteristics.LENS_FACING_FRONT);
    }

(2)關于CameraCharacteristics
這里是查詢出了所有的相機 ID ,然后來逐個遍歷看是否與所期望的相機方向相符合的相機設備。這里要注意的是相機的 ID 是實際是字符串,這個需要記住并且很重要,后面的相機操作,如打開設備、查詢或者設置參數等都是需要這個 ID 的。
通過 CameraManager. getCameraCharacteristics(ID) 查詢出了相關設備的特征信息,特征信息都被封裝在了 CameraCharacteristics 中。它以 Key<?>-Value 的形式儲存了所有的相機設備的參數信息。注意這個 Key<?> ,它又是一個泛型,這說明了 Key 也是可以以不同的形式存在的。這樣的擴展性就強了。特別是對于現在一些特殊攝像頭的發展,如3D 攝像頭,那么廠商就可自行添加參數支持而不用添加私有 API 了。這也是主要需要理解的部分。

(3)關于支持的硬件級別
了解了第(2)點,其他的就都只是參數查詢的問題了。這里摘抄官網了。

  • LEGACY 對于較舊的Android設備,設備以向后兼容模式運行,并且功能非常有限。
  • LIMITED設備代表基線功能集,還可能包括作為子集的附加功能FULL。
  • FULL 設備還支持傳感器,閃光燈,鏡頭和后處理設置的每幀手動控制,以及高速率的圖像捕獲。
  • LEVEL_3 設備還支持YUV重新處理和RAW圖像捕獲,以及其他輸出流配置。
  • EXTERNAL設備類似于LIMITED設備,例如某些傳感器或鏡頭信息未重新排列或不太穩定的幀速率。

CameraCharacteristics 中還有非常多的參數,這里僅列出其所提及到的,其他的參數如果你真的實際會在開發中用到,建議還是過一遍。這樣一來,相機能做什么,具備什么特性就會有一個整體感知了。

  • collectCameraInfo()
private void collectCameraInfo() {
        // 獲取此攝像機設備支持的可用流配置,其包括格式、大小、持續時間和停頓持續時間等
        StreamConfigurationMap map = mCameraCharacteristics.get(
                CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
        if (map == null) {
            throw new IllegalStateException("Failed to get configuration map: " + mCameraId);
        }
        mPreviewSizes.clear();
        // 根據需要渲染到的目標類型選擇合適的輸出大小
        for (android.util.Size size : map.getOutputSizes(mPreview.getOutputClass())) {
            int width = size.getWidth();
            int height = size.getHeight();
            if (width <= MAX_PREVIEW_WIDTH && height <= MAX_PREVIEW_HEIGHT) {
                mPreviewSizes.add(new Size(width, height));
            }
        }
        // 根據圖片格式選擇圖片大小
        mPictureSizes.clear();
        collectPictureSizes(mPictureSizes, map);
        // 把預覽中所支持的大小比例,但在圖片大小比例不支持的 比例 移除掉
        for (AspectRatio ratio : mPreviewSizes.ratios()) {
            if (!mPictureSizes.ratios().contains(ratio)) {
                mPreviewSizes.remove(ratio);
            }
        }
        // 如果設置的比例不完全相符合,那選擇一個接近的。
        if (!mPreviewSizes.ratios().contains(mAspectRatio)) {
            mAspectRatio = mPreviewSizes.ratios().iterator().next();
        }
    }

這段代碼相對來說要簡單一些,主要完成的是獲取預覽尺寸,圖片尺寸以及合適的顯示比例。

  • prepareImageReader()
private void prepareImageReader() {
        if (mImageReader != null) {
            mImageReader.close();
        }
        Size largest = mPictureSizes.sizes(mAspectRatio).last();
        mImageReader = ImageReader.newInstance(largest.getWidth(), largest.getHeight(),
                ImageFormat.JPEG, /* maxImages */ 2);
        mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, null);
    }

根據合適的圖片尺寸初始化 ImageReader,主要是用于接收圖片的原始數據信息,且這里的原始數據信息為 ImageFormat.JPEG。當然你也可以指定為 YUV 等更原始的數據信息。這樣一來除了除了讓圖像顯示在預覽界面上,我們還可以同時獲取原始數據信息做進一步處理,如增加濾鏡效果后再保存等。

而要獲取到原始數據信息,就需要向 ImageReader 注冊相應的監聽器 ImageReader.OnImageAvailableListener,當有相機的圖像幀后會通過onImageAvailable 進行回調。這里展開看一下它的實現。

public void onImageAvailable(ImageReader reader) {
           // 獲取  Image
            try (Image image = reader.acquireNextImage()) {
               // 獲取 Image 的平面
                Image.Plane[] planes = image.getPlanes();
                if (planes.length > 0) {
                    // 獲取平面 0 的 ByteBuffer,并從 ByteBuffer 中獲取 byte[]
                    ByteBuffer buffer = planes[0].getBuffer();
                    byte[] data = new byte[buffer.remaining()];
                    buffer.get(data);
                    mCallback.onPictureTaken(data);
                }
            }
        }

這里涉及到了圖像格式的知識, 這里就不細述了,感興趣的同學可以自己去查一下資料。

  • startOpeningCamera()
private void startOpeningCamera() {
        try {
            mCameraManager.openCamera(mCameraId, mCameraDeviceCallback, null);
        } catch (CameraAccessException e) {
            throw new RuntimeException("Failed to open camera: " + mCameraId, e);
        }
    }

最后一步就是打開相機了,打開相機需要傳遞前面所確定的 CameraID,注意它是個字符串。還傳入了一個 mCameraDeviceCallback,它的類型是 CameraDevice.StateCallback??匆豢此膶崿F。

private final CameraDevice.StateCallback mCameraDeviceCallback
            = new CameraDevice.StateCallback() {
       // 相機打開
        @Override
        public void onOpened(@NonNull CameraDevice camera) {
            mCamera = camera;
            mCallback.onCameraOpened();
            startCaptureSession();
        }
        // 相機關閉
        @Override
        public void onClosed(@NonNull CameraDevice camera) {
            mCallback.onCameraClosed();
        }
       // 相機斷開連接
        @Override
        public void onDisconnected(@NonNull CameraDevice camera) {
            mCamera = null;
        }
       // 打開相機出錯
        @Override
        public void onError(@NonNull CameraDevice camera, int error) {
            Log.e(TAG, "onError: " + camera.getId() + " (" + error + ")");
            mCamera = null;
        }

    };

這里就是打開相機狀態的回調監聽,主要關注的是 onOpened()。在這個回調方法中返回了 CameraDevice ,也就是實際的相機設備。關于 CameraDevice 再來看一個類圖。


CameraDevice.jpg

看出來了吧,CameraDevice 的實現類 CameraDeviceImpl 是持有了一個 Binder 端的代理。這里不看源碼,只憑推測可知,實際的相機設備對象應該被放到了系統進程 SystemServer 或者別的進程中去了。這和 Camera 1 就有本質上的區別了。

然后就是通知調用者,再然后就是一個 startCaptureSession() 調用。這個調用非常重要,它建立起了相機與 Target(這里是 Preview 以及 ImageReader) 的通道連接。

  • startCaptureSession()
void startCaptureSession() {
        if (!isCameraOpened() || !mPreview.isReady() || mImageReader == null) {
            return;
        }
        // 根據 Preivew 的大小從 mPreviewSize 中選擇一個最佳的。
        Size previewSize = chooseOptimalSize();
       // 設置 Preview Buffer 的大小
        mPreview.setBufferSize(previewSize.getWidth(), previewSize.getHeight());
       // 獲取 Preview 的 Surface,將被用來作用相機實際預覽的 Surface
        Surface surface = mPreview.getSurface();
        try {
           // 構建一個預覽請求
            mPreviewRequestBuilder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
           // 添加 Target ,通道的輸出端之一,這里只添加了 preview
            mPreviewRequestBuilder.addTarget(surface);
           // 建立 capture 會話,打通通道。設置輸出列表,并且還設置了回調 SessionCallback
            mCamera.createCaptureSession(Arrays.asList(surface, mImageReader.getSurface()),
                    mSessionCallback, null);
        } catch (CameraAccessException e) {
            throw new RuntimeException("Failed to start camera session");
        }
    }

該方法總的來說就是設置 Surface 的 Buffer 大小,創建請求參數,建立會話,打通通道。而關于創建請求參數,這里用了 CameraDevice.TEMPLATE_PREVIEW。其主要支持的參數有TEMPLATE_PREVIEW(預覽)、TEMPLATE_RECORD(拍攝視頻)、TEMPLATE_STILL_CAPTURE(拍照)等參數。接下來是調用了 createCaptureSession()。

在 createCaptureSession 時設置了輸出端列表,還設置了回調 mSessionCallback,它是CameraCaptureSession.StateCallback類型。

細心的讀者可能會發現,在這里,mPreivewRequestBuilder 并沒有用上,在 createCaptureSeesion 的參數中并沒有它。并且你應該還注意到,mPreviewRequestBuilder 通過 addTarget() 添加了輸出端,而 createCaptureSeesion 也添加添加了輸出列表。它們之間應該存在著某種關系。

先來說 createCaptureSeeson 的輸出列表。這個輸出列表決定了 CameraDevices 將根據列表的不同 Surface 將創建不同的圖像數據,比如這里的 preview surface 以及 ImageReader 的 Surface。而 PreviewRequestBuilder 中的 addTarget() 表示的是針對 CaptureRequest 應該將圖像數據輸出到哪里去,并且要求這里被添加到 target 的 Surface 必須是 createCaptureSession 的輸出列表的其中之一。那針對這段代碼來說,被創建的圖像數據有 2 種,一種是用于 preview 顯示的,一種是用于 ImageReader 的 jpeg。要想在預覽中也獲取 jpeg 數據,則把 ImageReader 的 surface 添加到 PreviewRequestBuilder 的 target 中去即中。

這里理清了這 2 個列表的關系,接下來看看 createCaptureSeesion 時的第 2 參數 mSessionCallback,它是 CameraCaptureSession.StateCallback 類型的。會話一旦被創建,它的回調方法便會被調用,這里主要關注 onConfigured() 的實現,在這里將關聯起 PreviewRequestBuilder 和會話。

       @Override
        public void onConfigured(@NonNull CameraCaptureSession session) {
            if (mCamera == null) {
                return;
            }
            mCaptureSession = session;
            // 設置對焦模式
            updateAutoFocus();
           // 設置閃光燈模式
            updateFlash();
            try {
               // 設定參數,并請求此捕獲會話不斷重復捕獲圖像,這樣就能連續不斷的得到圖像幀輸出到預覽界面
                mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(),
                        mCaptureCallback, null);
            } catch (CameraAccessException e) {
                Log.e(TAG, "Failed to start camera preview because it couldn't access camera", e);
            } catch (IllegalStateException e) {
                Log.e(TAG, "Failed to start camera preview.", e);
            }
        }

會話創建好之后,我們還要告訴會話該怎么用。查看 API 可知,接下來可以進行的是 capture, captureBurst, setRepeatingRequest,或 setRepeatingBurst 的提交。其中 capture 會在后面拍照章節中講述,***Burst 是用于連拍的。這里所調用的便是 setRepeatingRequest。通過 setRepeatingRequest 請求就將 mPreivewRequestBuilder 提交給了會話,而該提交就是請求此捕獲會話不斷重復捕獲圖像,這樣就能連續不斷的得到圖像幀輸出到預覽界面。

提交 setRepeatingRequest 請求時,還設置了一個參數 mCaptureCallback,它是 PictureCaptureCallback 類型的,而 PictureCaptureCallback 又是繼承自 CameraCaptureSession.CaptureCallback。捕獲到圖像后會同時調用 CaptureCallback 相應的回調方法,然而對于預覽模式下在這里并沒有什么處理。

關于 updateAutoFocus() 和 updateFlash() 看下面進一步的展開說明。

void updateAutoFocus() {
        if (mAutoFocus) {
            int[] modes = mCameraCharacteristics.get(
                    CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES);
            // Auto focus is not supported
            if (modes == null || modes.length == 0 ||
                    (modes.length == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF)) {
                mAutoFocus = false;
                mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
                        CaptureRequest.CONTROL_AF_MODE_OFF);
            } else {
                mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
                        CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
            }
        } else {
            mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
                    CaptureRequest.CONTROL_AF_MODE_OFF);
        }
    }

這段代碼的目的是如果設置了并且支持自動對焦,則 CONTROL_AF_MODE(auto-focus) 就設置為 CONTROL_AF_MODE_CONTINUOUS_PICTURE,否則就為 CONTROL_AF_MODE_OFF。有關 auto-focus 的值的含義概述如下。

value 說明
CONTROL_AF_MODE_AUTO 基本自動對焦模式
CONTROL_AF_MODE_CONTINUOUS_PICTURE 圖片模式下的連續對焦
CONTROL_AF_MODE_CONTINUOUS_VIDEO 視頻模式下的連續對焦
CONTROL_AF_MODE_EDOF 擴展景深(數字對焦)模式
CONTROL_AF_MODE_MACRO 特寫聚焦模式
CONTROL_AF_MODE_OFF 無自動對焦

這個表格中的每個 value 我也并不是每個都熟悉,因此,只作了解即可。

void updateFlash() {
        switch (mFlash) {
            case Constants.FLASH_OFF:
                mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
                        CaptureRequest.CONTROL_AE_MODE_ON);
                mPreviewRequestBuilder.set(CaptureRequest.FLASH_MODE,
                        CaptureRequest.FLASH_MODE_OFF);
                break;
            case Constants.FLASH_ON:
                mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
                        CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH);
                mPreviewRequestBuilder.set(CaptureRequest.FLASH_MODE,
                        CaptureRequest.FLASH_MODE_OFF);
                break;
            case Constants.FLASH_TORCH:
                mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
                        CaptureRequest.CONTROL_AE_MODE_ON);
                mPreviewRequestBuilder.set(CaptureRequest.FLASH_MODE,
                        CaptureRequest.FLASH_MODE_TORCH);
                break;
            case Constants.FLASH_AUTO:
                mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
                        CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
                mPreviewRequestBuilder.set(CaptureRequest.FLASH_MODE,
                        CaptureRequest.FLASH_MODE_OFF);
                break;
            case Constants.FLASH_RED_EYE:
                mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
                        CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE);
                mPreviewRequestBuilder.set(CaptureRequest.FLASH_MODE,
                        CaptureRequest.FLASH_MODE_OFF);
                break;
        }
    }

通過 PreviewRequestBuilder 設定閃光燈的模式,其需要同時設定 CONTROL_AE_MODE 和 FLASH_MODE。

(1) FLASH_MODE,對應是控制閃光燈。

參數 說明
FLASH_MODE_OFF 關閉模式
FLASH_MODE_SINGLE 閃一下模式
FLASH_MODE_TORCH 長亮模式

(2) CONTROL_AE_MODE,對應是曝光,即 auto-exposure。

參數 說明
CONTROL_AE_MODE_ON_AUTO_FLASH 自動曝光
CONTROL_AE_MODE_ON_ALWAYS_FLASH 強制曝光
CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE 不閃光

到這里,基本上就成功打開相機了,然后就能看到相機的畫面了。歷經磨難,終于打開相機了。而關于相機參數設置,在 Camera 2 中則更加豐富,工程里沒有涉及到的這里就不做詳細講解,在實際開發中再去慢慢消化,慢慢理解。

接下來,終于可以進行愉快的拍照了。

4.拍照

分析之前也先來看一看拍照的時序圖。梳理了 16 個步驟,但其實拍照的關鍵步驟就 2 步:通過 CameraDevice 創建一個 TEMPLATE_STILL_CAPTURE 的 CaptureRequest,然后通過 CaptureSession 的 capture 方法提交請求即是拍照的主要步驟。


CameraView 拍照.jpg

CameraView 的 takePicture 就是進一步調用 Camera2 的 takePicture,所以直接從 takePicture() 開始吧。

  • takePicture()
void takePicture() {
        if (mAutoFocus) {
            lockFocus();
        } else {
            captureStillPicture();
        }
    }

CameraView 初始化時默認是自動對焦,因此這里是走入 lockFocus(),時序圖也是依據此來繪制的。

  • lockFocus()
private void lockFocus() {
       // 設置當前立刻觸發自動對焦
        mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
                CaptureRequest.CONTROL_AF_TRIGGER_START);
        try {
           // 這里是修改了 PictureCaptureCallback 的狀態為 STATE_LOCKING
            mCaptureCallback.setState(PictureCaptureCallback.STATE_LOCKING);
          // 向會話提交 capture 請求,以鎖定自動對焦
            mCaptureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, null);
        } catch (CameraAccessException e) {
            Log.e(TAG, "Failed to lock focus.", e);
        }
    }

設置了立刻觸發自動對焦,修改了 PictureCaptureCallback 狀態為 STATE_LOCKING。接下來就是等待 PictureCaptureCallback 的 onCaptureCompleted() 被系統回調。在 onCaptureCompleted() 中進步調用了 process(),而在 process() 中以不同的狀態進行不同的處理。這里根據前面的設置處理的是 STATE_LOCKING。

 private void process(@NonNull CaptureResult result) {
            switch (mState) {
                case STATE_LOCKING: {
                    ......
if (af == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED ||
                            af == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) {
                            setState(STATE_CAPTURING);
                            onReady();
                       ......
                    break;
                }
                case STATE_PRECAPTURE: {
                    ......
                    setState(STATE_WAITING);
                    break;
                }
                case STATE_WAITING: {
                    ......
                     setState(STATE_CAPTURING);
                     onReady();
                    break;
                }
            }
        }

為了避免不必要的麻煩,在不影響對代碼理解的情況下,這里省略了其他狀態的處理。這里假設自動對焦成功了且達到了一個很好的狀態下,那么當前的自動對對焦就會進入被鎖定的狀態,即 CONTROL_AF_STATE_FOCUSED_LOCKED。而自動對焦前面在 updateAutoFocus() 中已經設置為 CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE 了。接下來就會進入真正的抓取圖片的處理了。這里先設置了狀態為 STATE_CAPTURING,然后調用了自已擴展的 onReady()。onReady() 的實現很簡單,就是調用 captureStillPicture()。

  • captureStillPicture()
void captureStillPicture() {
        try {
            //1. 創建一個新的CaptureRequest.Builder,且其參數為 TEMPLATE_STILL_CAPTURE
            CaptureRequest.Builder captureRequestBuilder = mCamera.createCaptureRequest(
                    CameraDevice.TEMPLATE_STILL_CAPTURE);
            //2. 添加它的 target 為 ImageReader 的 Surface
            captureRequestBuilder.addTarget(mImageReader.getSurface());
            //3. 設置自動對焦模式為預覽的自動對焦模式
            captureRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
                    mPreviewRequestBuilder.get(CaptureRequest.CONTROL_AF_MODE));
            //4. 設置閃光燈與曝光參數
            switch (mFlash) {
                case Constants.FLASH_OFF:
                    captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
                            CaptureRequest.CONTROL_AE_MODE_ON);
                    captureRequestBuilder.set(CaptureRequest.FLASH_MODE,
                            CaptureRequest.FLASH_MODE_OFF);
                    break;
                case Constants.FLASH_ON:
                    captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
                            CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH);
                    break;
                case Constants.FLASH_TORCH:
                    captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
                            CaptureRequest.CONTROL_AE_MODE_ON);
                    captureRequestBuilder.set(CaptureRequest.FLASH_MODE,
                            CaptureRequest.FLASH_MODE_TORCH);
                    break;
                case Constants.FLASH_AUTO:
                    captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
                            CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
                    break;
                case Constants.FLASH_RED_EYE:
                    captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
                            CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
                    break;
            }
            // 5. 計算 JPEG 的旋轉角度
            @SuppressWarnings("ConstantConditions")
            int sensorOrientation = mCameraCharacteristics.get(
                    CameraCharacteristics.SENSOR_ORIENTATION);
            captureRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION,
                    (sensorOrientation +
                            mDisplayOrientation * (mFacing == Constants.FACING_FRONT ? 1 : -1) +
                            360) % 360);
            // 6.停止預覽
            mCaptureSession.stopRepeating();
            // 7.抓取當前圖片
            mCaptureSession.capture(captureRequestBuilder.build(),
                    new CameraCaptureSession.CaptureCallback() {
                        @Override
                        public void onCaptureCompleted(@NonNull CameraCaptureSession session,
                                @NonNull CaptureRequest request,
                                @NonNull TotalCaptureResult result) {
                            // 8.解鎖對自動對焦的鎖定
                            unlockFocus();
                        }
                    }, null);
        } catch (CameraAccessException e) {
            Log.e(TAG, "Cannot capture a still picture.", e);
        }
    }

這是拍照的關鍵實現,代碼有點長,但通過增加了帶時序的注釋,邏輯上看起來也就并不復雜了。這里只強調 3 個點,其他的看一看注釋即可,而關于設置閃光燈和曝光這里就省略了。
(1) 這里創建了一個新的 CaptureRequest.Builder ,且其參數為TEMPLATE_STILL_CAPTURE。相應的其 CallBack 也是新的。
(2) 請求的 Target 只有 ImageReader 的 Surface,因此獲取到圖片后會輸出到 ImageReader。最后會在 ImageReader.OnImageAvailableListener 的 onImageAvailable 得到回調。
(3) 拍照前先停止了預覽請求,從這里可以看出拍照就是捕獲預覽模式下自動對焦成功鎖定后的圖像數據。

接下來就是等待 onCaptureCompleted 被系統回調,然后進一步調用 unlockFocus()。

  • unlockFocus()
void unlockFocus() {
        // 取消了立即自動對焦的觸發
        mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
                CaptureRequest.CONTROL_AF_TRIGGER_CANCEL);
        try {
            mCaptureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, null);
            updateAutoFocus();
            updateFlash();
            mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
                    CaptureRequest.CONTROL_AF_TRIGGER_IDLE);
        
    // 重新打開預覽
    mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), mCaptureCallback,
                    null);
            mCaptureCallback.setState(PictureCaptureCallback.STATE_PREVIEW);
        } catch (CameraAccessException e) {
            Log.e(TAG, "Failed to restart camera preview.", e);
        }
    }

該方法主要做的事情就是重新打開預覽,并且取消了立即自動對焦,同時將其設置為 CONTROL_AF_TRIGGER_IDLE,這將會解除自動對焦的狀態,即其狀態不再是 CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED。

系統組織好 ImageReader 需要的圖像數據后,就會回調其監聽 ImageReader.OnImageAvailableListener 的 onImageAvailable()。

  • onImageAvailable()
public void onImageAvailable(ImageReader reader) {
            try (Image image = reader.acquireNextImage()) {
                Image.Plane[] planes = image.getPlanes();
                if (planes.length > 0) {
                    ByteBuffer buffer = planes[0].getBuffer();
                    byte[] data = new byte[buffer.remaining()];
                    buffer.get(data);
                    mCallback.onPictureTaken(data);
                }
            }
        }

從 ImageReader 中獲取到 Image,Image 相比 Bitmap 就要復雜的多了,這里簡單說明一下。ImageReader 封裝了圖像的數據平面,而每個平面又封裝了 ByteBuffer 來保存原始數據。關于圖像的數據平面這個相對于圖像的格式來說的,比如 rgb 就只一個平面,而 YUV 一般就有 3 個平面。從 ByteBuffer 中獲取的數據都是最原始的數據,對于 rgb 格式的數據,就可以直接將其轉換成 Bitmap 然后給 ImageView 顯示。

到這里就分析完了拍照的過程了。

5.關閉相機

void stop() {
        if (mCaptureSession != null) {
            mCaptureSession.close();
            mCaptureSession = null;
        }
        if (mCamera != null) {
            mCamera.close();
            mCamera = null;
        }
        if (mImageReader != null) {
            mImageReader.close();
            mImageReader = null;
        }
    }

全場最簡單,關閉會話,關閉相機,關閉 ImageReader,Game voer !!!

五、總結

文章對 Android Camera 編程進行了一個較為詳細的概括,尤其是對于偏難的 Camera 2 的 API 的理解,結合了官方的 Demo 對 API 及其參數進行了詳細的分析,以使得對 API 的理解更加透徹。

另外,如果你的項目需要集成 Camera,又不想自己去封裝,同時又覺得官方的 demo 還不夠,這里另外推薦一個 github 開源項目 camerakit-android。其也是從官方 demo fork 出來的,自動支持 camera api 1 以及 camera api 2。

最后,感謝你能讀到并讀完此文章。受限于作者水平有限,如果分析的過程中存在錯誤或者疑問都歡迎留言討論。如果我的分享能夠幫助到你,也請記得幫忙點個贊吧,鼓勵我繼續寫下去,謝謝。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容