Android Camera 編程從入門到精通

一、前言

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

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

這里主要涉及到的是如何直接調(diào)起系統(tǒng)相機拍照以及基于 Camra API 1 實現(xiàn)拍照。如下的思維導(dǎo)圖是一個基本的導(dǎo)讀。


Android 相機.jpg

1.權(quán)限及需求說明

要使用相機必須聲明 CAMERA 權(quán)限以及告訴系統(tǒng)你要使用這個功能。

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

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

<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.調(diào)起系統(tǒng)或者三方相機直接拍照

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

3.通過 Camera API 1 進行拍照

  • 相機設(shè)備檢測
(context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA))
  • 打開相機
Camera.open();
// Camera.open(0)
// Camera.open(1)
  • 創(chuàng)建預(yù)覽界面
/** 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();
       ......
    }
}
...
}
  • 設(shè)置相機參數(shù)
public void setCamera(Camera camera) {
    ......
    if (mCamera != null) {
        List<Size> localSizes = mCamera.getParameters().getSupportedPreviewSizes();
        mSupportedPreviewSizes = localSizes;
        requestLayout();
       ......
      // 相機參數(shù)設(shè)置完成后,需要重新啟動預(yù)覽界面
      mCamera.setPreviewDisplay(mHolder);
      mCamera.startPreview();
       ......
    }
}
  • 停止預(yù)覽及釋放相機
    這個建議放在 onDestroy() 中調(diào)用
private void stopPreviewAndFreeCamera() {
    ......
        mCamera.stopPreview();
    ......
        mCamera.release();
        mCamera = null;
    }
}

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

三、全新 Camera API 2

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

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

看起來很爽,但是用起來那就是酸爽了,如下是梳理的一個思維導(dǎo)圖??纯淳椭烙卸嗦闊┝?。


Camera API 2 拍照.jpg

四、官方 demo 分析

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

1. 主要類圖

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

工程主類圖

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

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

2.CameraView 初始化

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

CameraView 初始化.jpg
  • CameraView 的構(gòu)建方法
public CameraView(Context context, AttributeSet attrs, int defStyleAttr) {
       ......
        // 創(chuàng)建預(yù)覽視圖
        final PreviewImpl preview = createPreviewImpl(context);
        // Callback 橋接器,將相機內(nèi)部的回調(diào)轉(zhuǎn)發(fā)給調(diào)用層
        mCallbacks = new CallbackBridge();
        // 根據(jù)不同的 SDK 版本選擇不同的 Camera 實現(xiàn),這里假設(shè)選擇了 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);
        }
       ......
        // 設(shè)置相機 ID,如前置或者后置
        setFacing(a.getInt(R.styleable.CameraView_facing, FACING_BACK));
        ......
        // 設(shè)置預(yù)覽界面的比例,如 4:3 或者 16:9
        setAspectRatio(AspectRatio.parse(aspectRatio));
        // 設(shè)置對焦方式
        setAutoFocus(a.getBoolean(R.styleable.CameraView_autoFocus, true));
       // 設(shè)置閃光燈
        setFlash(a.getInt(R.styleable.CameraView_flash, Constants.FLASH_AUTO));
       ......
        // 初始化顯示設(shè)備(主要指手機屏幕)的旋轉(zhuǎn)監(jiān)聽,主要用來設(shè)置相機的旋轉(zhuǎn)方向
        mDisplayOrientationDetector = new DisplayOrientationDetector(context) {
            @Override
            public void onDisplayOrientationChanged(int displayOrientation) {
                mImpl.setDisplayOrientation(displayOrientation);
            }
        };
}

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

  • createPreviewImpl() 的實現(xiàn)
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 都是一個包裝類,從名字上就可以知道其內(nèi)部分別包裝了 SurfaceView 和 TextureView 實例來實現(xiàn)相機的預(yù)覽界面的。關(guān)于 SurfaceView 以及 TextureView 的區(qū)別,這里也再簡單提一下,詳細的可以參考其他大神的文章說明:
SurfaceView:是一個獨立的 Window,由系統(tǒng) WMS 直接管理,可支持硬件加速,也可以不支持硬件加速。
TextureView:可以看成是一個普通的 View,屬于所于應(yīng)用的視圖層級樹中,屬于 ViewRootImpl 管理,只支持硬件加速。

盡管 SurfaceView 和 TextureView 有區(qū)別,但本質(zhì)上它們都是對 Surface 的一個封裝實現(xiàn)。

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

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

  • 初始化 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 有一個專業(yè)的管理者了。其次可以看到這里是向 Context 獲取一個系統(tǒng) Service "CAMERA_SERVICE" 來初始化 CameraManager 的,這也說明了其被上升到了一個系統(tǒng)服務(wù)的高度了。

然后就是向 Preview 添加回調(diào),監(jiān)聽其 Surface 的變化來作進一步的事情。

  • 關(guān)于 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 的選擇也根據(jù) API 來選擇, 這個就不應(yīng)該了??催^其他相應(yīng)的實現(xiàn),除了 SDK API 的檢查,應(yīng)用 TextureView 前還應(yīng)該要判斷一下當(dāng)前的運行環(huán)境是否支持硬件加速。

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

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

當(dāng)然,這里只是給出了 4 個參數(shù),其實還有很多,后面還會講到。

小結(jié)


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

3.打開相機

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


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();
        }
    }

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

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

都是內(nèi)部調(diào)用,下面逐個分析這些方法的實現(xiàn)。

  • chooseCameraIdByFacing()
private boolean chooseCameraIdByFacing() {
        try {
            // 1.根據(jù) mFacing 選擇相機
            int internalFacing = INTERNAL_FACINGS.get(mFacing);
            // 2.獲取所有的可用相機 ID 列表,注意相機的 ID 是 字串類型了
            final String[] ids = mCameraManager.getCameraIdList();
            ......
            for (String id : ids) {
                // 根據(jù)相機的 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");
                }
                // 查出來的與所期望的相等,則認為就是要找到的相機設(shè)備
                if (internal == internalFacing) {
                    // 保存相機的 ID
                    mCameraId = id;
                    // 保存相機的特征參數(shù)
                    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);
        }
    }

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

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

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

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

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

  • collectCameraInfo()
private void collectCameraInfo() {
        // 獲取此攝像機設(shè)備支持的可用流配置,其包括格式、大小、持續(xù)時間和停頓持續(xù)時間等
        StreamConfigurationMap map = mCameraCharacteristics.get(
                CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
        if (map == null) {
            throw new IllegalStateException("Failed to get configuration map: " + mCameraId);
        }
        mPreviewSizes.clear();
        // 根據(jù)需要渲染到的目標(biāo)類型選擇合適的輸出大小
        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));
            }
        }
        // 根據(jù)圖片格式選擇圖片大小
        mPictureSizes.clear();
        collectPictureSizes(mPictureSizes, map);
        // 把預(yù)覽中所支持的大小比例,但在圖片大小比例不支持的 比例 移除掉
        for (AspectRatio ratio : mPreviewSizes.ratios()) {
            if (!mPictureSizes.ratios().contains(ratio)) {
                mPreviewSizes.remove(ratio);
            }
        }
        // 如果設(shè)置的比例不完全相符合,那選擇一個接近的。
        if (!mPreviewSizes.ratios().contains(mAspectRatio)) {
            mAspectRatio = mPreviewSizes.ratios().iterator().next();
        }
    }

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

  • 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);
    }

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

而要獲取到原始數(shù)據(jù)信息,就需要向 ImageReader 注冊相應(yīng)的監(jiān)聽器 ImageReader.OnImageAvailableListener,當(dāng)有相機的圖像幀后會通過onImageAvailable 進行回調(diào)。這里展開看一下它的實現(xiàn)。

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);
                }
            }
        }

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

  • 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(xiàn)。

private final CameraDevice.StateCallback mCameraDeviceCallback
            = new CameraDevice.StateCallback() {
       // 相機打開
        @Override
        public void onOpened(@NonNull CameraDevice camera) {
            mCamera = camera;
            mCallback.onCameraOpened();
            startCaptureSession();
        }
        // 相機關(guān)閉
        @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;
        }

    };

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


CameraDevice.jpg

看出來了吧,CameraDevice 的實現(xiàn)類 CameraDeviceImpl 是持有了一個 Binder 端的代理。這里不看源碼,只憑推測可知,實際的相機設(shè)備對象應(yīng)該被放到了系統(tǒng)進程 SystemServer 或者別的進程中去了。這和 Camera 1 就有本質(zhì)上的區(qū)別了。

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

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

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

在 createCaptureSession 時設(shè)置了輸出端列表,還設(shè)置了回調(diào) mSessionCallback,它是CameraCaptureSession.StateCallback類型。

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

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

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

       @Override
        public void onConfigured(@NonNull CameraCaptureSession session) {
            if (mCamera == null) {
                return;
            }
            mCaptureSession = session;
            // 設(shè)置對焦模式
            updateAutoFocus();
           // 設(shè)置閃光燈模式
            updateFlash();
            try {
               // 設(shè)定參數(shù),并請求此捕獲會話不斷重復(fù)捕獲圖像,這樣就能連續(xù)不斷的得到圖像幀輸出到預(yù)覽界面
                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);
            }
        }

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

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

關(guān)于 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);
        }
    }

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

value 說明
CONTROL_AF_MODE_AUTO 基本自動對焦模式
CONTROL_AF_MODE_CONTINUOUS_PICTURE 圖片模式下的連續(xù)對焦
CONTROL_AF_MODE_CONTINUOUS_VIDEO 視頻模式下的連續(xù)對焦
CONTROL_AF_MODE_EDOF 擴展景深(數(shù)字對焦)模式
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 設(shè)定閃光燈的模式,其需要同時設(shè)定 CONTROL_AE_MODE 和 FLASH_MODE。

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

參數(shù) 說明
FLASH_MODE_OFF 關(guān)閉模式
FLASH_MODE_SINGLE 閃一下模式
FLASH_MODE_TORCH 長亮模式

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

參數(shù) 說明
CONTROL_AE_MODE_ON_AUTO_FLASH 自動曝光
CONTROL_AE_MODE_ON_ALWAYS_FLASH 強制曝光
CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE 不閃光

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

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

4.拍照

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


CameraView 拍照.jpg

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

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

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

  • lockFocus()
private void lockFocus() {
       // 設(shè)置當(dāng)前立刻觸發(fā)自動對焦
        mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
                CaptureRequest.CONTROL_AF_TRIGGER_START);
        try {
           // 這里是修改了 PictureCaptureCallback 的狀態(tài)為 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);
        }
    }

設(shè)置了立刻觸發(fā)自動對焦,修改了 PictureCaptureCallback 狀態(tài)為 STATE_LOCKING。接下來就是等待 PictureCaptureCallback 的 onCaptureCompleted() 被系統(tǒng)回調(diào)。在 onCaptureCompleted() 中進步調(diào)用了 process(),而在 process() 中以不同的狀態(tài)進行不同的處理。這里根據(jù)前面的設(shè)置處理的是 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;
                }
            }
        }

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

  • captureStillPicture()
void captureStillPicture() {
        try {
            //1. 創(chuàng)建一個新的CaptureRequest.Builder,且其參數(shù)為 TEMPLATE_STILL_CAPTURE
            CaptureRequest.Builder captureRequestBuilder = mCamera.createCaptureRequest(
                    CameraDevice.TEMPLATE_STILL_CAPTURE);
            //2. 添加它的 target 為 ImageReader 的 Surface
            captureRequestBuilder.addTarget(mImageReader.getSurface());
            //3. 設(shè)置自動對焦模式為預(yù)覽的自動對焦模式
            captureRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
                    mPreviewRequestBuilder.get(CaptureRequest.CONTROL_AF_MODE));
            //4. 設(shè)置閃光燈與曝光參數(shù)
            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 的旋轉(zhuǎn)角度
            @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.停止預(yù)覽
            mCaptureSession.stopRepeating();
            // 7.抓取當(dāng)前圖片
            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);
        }
    }

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

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

  • unlockFocus()
void unlockFocus() {
        // 取消了立即自動對焦的觸發(fā)
        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);
        
    // 重新打開預(yù)覽
    mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), mCaptureCallback,
                    null);
            mCaptureCallback.setState(PictureCaptureCallback.STATE_PREVIEW);
        } catch (CameraAccessException e) {
            Log.e(TAG, "Failed to restart camera preview.", e);
        }
    }

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

系統(tǒng)組織好 ImageReader 需要的圖像數(shù)據(jù)后,就會回調(diào)其監(jiān)聽 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 就要復(fù)雜的多了,這里簡單說明一下。ImageReader 封裝了圖像的數(shù)據(jù)平面,而每個平面又封裝了 ByteBuffer 來保存原始數(shù)據(jù)。關(guān)于圖像的數(shù)據(jù)平面這個相對于圖像的格式來說的,比如 rgb 就只一個平面,而 YUV 一般就有 3 個平面。從 ByteBuffer 中獲取的數(shù)據(jù)都是最原始的數(shù)據(jù),對于 rgb 格式的數(shù)據(jù),就可以直接將其轉(zhuǎn)換成 Bitmap 然后給 ImageView 顯示。

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

5.關(guān)閉相機

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

全場最簡單,關(guān)閉會話,關(guān)閉相機,關(guān)閉 ImageReader,Game voer ?。?!

五、總結(jié)

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

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

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容