Android Camera 編程從入門到精通

1、前言

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

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

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

Android  相機.jpg

1.權限及需求說明

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

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

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

<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 的介紹。設計模式

3、全新 Camera API 2

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

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

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

Camera API 2 拍照.jpg

4、官方 demo 分析

正是因爲 Camera 的 API 從 1 到 2 發生了架構上的變化,並且使用難度也是大大地增長了好幾倍,加上 Android 的碎片化又是如斯的嚴重。所以官方考慮到你們掌握很差,推出了其官方的 demo 供咱們參考和學習——cameraview。這裏也將基於官方的 demo 來深刻掌握 Android 相機 API 2 的使用。session

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。看一看它的實現。

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 !!!

5、總結

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

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

最後,感謝你能讀到並讀完此文章。受限於做者水平有限,若是分析的過程當中存在錯誤或者疑問都歡迎留言討論。若是個人分享可以幫助到你,也請記得幫忙點個贊吧,鼓勵我繼續寫下去,謝謝。

相關文章
相關標籤/搜索