Android 自定義Camera2 系列(二)

該博客主要記錄在開發(fā)過程中所運用到的Api 和部分技術(shù)調(diào)用的記錄,閱讀時間:15分鐘+ ,該博客記錄內(nèi)容相對簡單,僅以用于開發(fā)過程記錄。

本文已獨家授權(quán)微信公眾號:鴻洋(hongyangAndroid)在微信公眾號平臺原創(chuàng)首發(fā)

說明

在我們前一篇文章中提到Camera,在開發(fā)中發(fā)現(xiàn)很多api 都已經(jīng)不推薦使用,google給出的替代方案則就是我們今天的主角 Camera2 ,從5.0開始(API Level 21),可以完全控制Android設備相機的新api 。當然如果產(chǎn)品覆蓋的還是有4.0版本的Android 用戶的話,還是建議 使用Camera。但是在以前的Camera 中,對相機的手動控制都是通過更改系統(tǒng)才能實現(xiàn),而且api也不友好。Camera2 這時針對這一點進行了管理的分離,Api會更加的友好,分工明確。

文章對應 Demo 地址
google demo 地址
文章參考地址
很感謝該篇文章作者的描述。

效果圖


在這里插入圖片描述

1. camera2 概念

camera2流程圖

相對于camera ,camera2 在Api上將拍照對象進行了獨立,camera2采用pipeline的方式,將Camera 設備和 Android 設備連接起來,Android Device通過管道發(fā)送CaptureRequest拍照請求給Camera Device,Camera Device通過管道返回CameraMetadata數(shù)據(jù)給Android Device,這一切都發(fā)生在CameraCaptureSession的會話中。

2. camera2 API簡要說明

camera2 主要API

CameraCaptureSession:這是一個非常重要的API,當程序需要預覽、拍照時,都通過該類的實例創(chuàng)建Session,控制預覽的方法setRepeatingRequest();控制拍照的方法為capture()。

CameraDevices:提供一組靜態(tài)屬性信息,描述硬件設備以及設備的可用設置和輸出參數(shù)。通過getCameraCharacteristics獲得。

CameraManager:所有相機設備(CameraDevice)的管理者,要枚舉,查詢和打開可用的相機設備,用于打開和關(guān)閉系統(tǒng)攝像頭,就獲取CameraManager實例。

CaptureRequest:定義了相機設備捕獲單個映像所需的所有捕獲參數(shù)。該請求還列出了哪些配置的輸出表面應該用作此捕獲的目標。

CameraDevice:具有用于為給定用例創(chuàng)建請求構(gòu)建器的工廠方法,針對應用程序正在運行的Android設備進行了優(yōu)化,描述系統(tǒng)攝像頭,類似于早期的Camera。

CameraRequest CameraRequest.Builder:當程序調(diào)用setRepeatingRequest()方法進行預覽時,或調(diào)用capture()方法進行拍照時,都需要傳入CameraRequest參數(shù)。CameraRequest代表了一次捕獲請求,用于描述捕獲圖片的各種參數(shù)設置,程序?qū)φ掌龅母鞣N控制,都通過CameraRequest參數(shù)進行設置。CameraRequest.Builder則負責生成CameraRequest對象。

CameraCharacteristics:描述攝像頭的各種特性,我們可以通過CameraManager的getCameraCharacteristics(@NonNull String cameraId)方法來獲取。

CaptureResult:描述拍照完成后的結(jié)果。

ImageReader :通過添加 PreviewRequestBuilder.addTarget(mImageReader.getSurface()); 可以在 OnImageAvailableListener 接口中實時獲取 yuv 數(shù)據(jù)。

在這里插入圖片描述

3.Camera2接口使用的流程

接口流程圖

1.調(diào)用openCamera方法后會回調(diào)CameraDevice.StateCallback這個方法,在該方法里重寫onOpened函數(shù)。

2.在onOpened方法中調(diào)用createCaptureSession,該方法又回調(diào)CameraCaptureSession.StateCallback方法。

3.CameraCaptureSession.StateCallback中重寫onConfigured方法,設置setRepeatingRequest方法(也就是開啟預覽)。

4.setRepeatingRequest又會回調(diào) CameraCaptureSession.CaptureCallback方法。

5.重寫CameraCaptureSession.CaptureCallback中的onCaptureCompleted方法,result就是未經(jīng)過處理的幀數(shù)據(jù)了。

4. 自定義Camera2

Camera2與Camera一樣也有cameraId的概念,我們通過mCameraManager.getCameraIdList()來獲取cameraId列表,然后通過mCameraManager.getCameraCharacteristics(id) 獲取每個id對應攝像頭的參數(shù)。

關(guān)于CameraCharacteristics里面的參數(shù),主要用到的有以下幾個:

LENS_FACING:前置攝像頭(LENS_FACING_FRONT)還是后置攝像頭(LENS_FACING_BACK)。

SENSOR_ORIENTATION:攝像頭拍照方向。

FLASH_INFO_AVAILABLE:是否支持閃光燈。

CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL:獲取當前設備支持的相機特性。

注:事實上,在各個廠商的的Android設備上,Camera2的各種特性并不都是可用的,需要通過characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)方法 來根據(jù)返回值來獲取支持的級別,具體說來:

INFO_SUPPORTED_HARDWARE_LEVEL_FULL:全方位的硬件支持,允許手動控制全高清的攝像、支持連拍模式以及其他新特性。

INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED:有限支持,這個需要單獨查詢。

INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY:所有設備都會支持,也就是和過時的Camera API支持的特性是一致的。

利用這個INFO_SUPPORTED_HARDWARE_LEVEL參數(shù),我們可以來判斷是使用Camera還是使用Camera

通過上面的 原理的說明,大致流程可能還會有點模糊,我們直接對應上述邏輯開始代碼。

4.1 界面布局

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/black"
    tools:context="cn.tongue.tonguecamera.ui.CameraActivity">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        // 自定義 TextureView
        <cn.tongue.tonguecamera.view.AutoFitTextureView
            android:id="@+id/textureView_g"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

    </FrameLayout>

    <RelativeLayout
        android:id="@+id/homecamera_bottom_relative2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#00ffffff"
        android:layout_alignParentBottom="true">
        // 返回按鈕
        <ImageView
            android:id="@+id/iv_back_g"
            android:layout_width="40dp"
            android:layout_height="30dp"
            android:scaleType="centerInside"
            android:layout_marginBottom="20dp"
            android:layout_marginStart="20dp"
            android:layout_centerVertical="true"
            android:background="@drawable/icon_back" />
        // 拍照按鈕
        <ImageView
            android:id="@+id/img_camera_g"
            android:layout_width="80dp"
            android:layout_height="80dp"
            android:scaleType="centerInside"
            android:layout_marginBottom="20dp"
            android:layout_centerInParent="true"
            android:background="@drawable/camera" />

    </RelativeLayout>

</RelativeLayout>

通過上述布局我們會發(fā)現(xiàn) camera2 中我們并沒有繼續(xù)選擇 SurfaceView作為呈現(xiàn)圖片的載體,這里選擇的TextureView。

    @RequiresApi(api = Build.VERSION_CODES.M)
    @Override
    public void onResume() {
        super.onResume();
        // 啟動 HandlerThread ,后臺維護一個 handler
        startBackgroundThread();
        // 存在關(guān)聯(lián)則打開相機,沒有則綁定事件
        if (mTextureView.isAvailable()) {
            openCamera(mTextureView.getWidth(), mTextureView.getHeight());
        } else {
            // 綁定 載體監(jiān)聽事件
            mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
        }
    }

如果是第一次進入 就需要配置 相機相關(guān)配置

    /**
     * 打開相機
     *
     * @param width 寬度
     * @param height 長度
     */
    @RequiresApi(api = Build.VERSION_CODES.M)
    private void openCamera(int width, int height) {
        // 判斷相機權(quán)限 6.0 以上的動態(tài)權(quán)限
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
                != PackageManager.PERMISSION_GRANTED) {
            requestPermissions(new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA_PERMISSION);
            return;
        }
        // 配置 相機的 預覽尺寸
        setUpCameraOutputs(width, height);
        // Matrix 轉(zhuǎn)換配置為 mTextureView
        configureTransform(width, height);
        CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
        try {
            if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
                throw new RuntimeException("Time out waiting to lock camera opening.");
            }
            // 都配置完成后 打開相機 并綁定回調(diào)接口
            manager.openCamera(mCameraId, mStateCallback, mBackgroundHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            throw new RuntimeException("Interrupted while trying to lock camera opening.", e);
        }
    }

上述代碼中,setUpCameraOutputs() 方法主要進行 相機參數(shù)的設置(前后攝像頭,閃光燈的配置),設置mImageReader 的成像格式及數(shù)據(jù)流的回調(diào)監(jiān)聽事件 OnImageAvailableListener,并且根據(jù)硬件數(shù)據(jù) 查看是否需要交換尺寸以獲得相對于傳感器的預覽尺寸。

    /**
     * CameraDevice 改變狀態(tài)時候 調(diào)用
     */
    private final CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {

        // 打開事件監(jiān)聽
        @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
        @Override
        public void onOpened(@NonNull CameraDevice cameraDevice) {
            //打開相機時會調(diào)用此方法。 我們在這里開始相機預覽。
            mCameraOpenCloseLock.release();
            mCameraDevice = cameraDevice;
            createCameraPreviewSession();
        }

        // 關(guān)閉監(jiān)聽
        @Override
        public void onDisconnected(@NonNull CameraDevice cameraDevice) {
            mCameraOpenCloseLock.release();
            cameraDevice.close();
            mCameraDevice = null;
        }

        @Override
        public void onError(@NonNull CameraDevice cameraDevice, int error) {
            mCameraOpenCloseLock.release();
            cameraDevice.close();
            mCameraDevice = null;
            finish();
        }

    };

從回調(diào)方法中,我們 通過 CameraCaptureSession 開始 預覽圖像,createCameraPreviewSession() 則是創(chuàng)建一個 相機預覽。

    /**
     * 創(chuàng)建一個新的相機預覽
     */
    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private void createCameraPreviewSession() {
        try {
            SurfaceTexture texture = mTextureView.getSurfaceTexture();
            //將默認緩沖區(qū)的大小配置為相機預覽的大小。
            texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
            Surface surface = new Surface(texture);
            //  Camera2都是通過創(chuàng)建請求會話的方式進行調(diào)用的
            mPreviewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
            // 使用Surface設置CaptureRequest.Builder
            mPreviewRequestBuilder.addTarget(surface);
            // 方法創(chuàng)建CaptureSession。
            mCameraDevice.createCaptureSession(Arrays.asList(surface, mImageReader.getSurface()),
                    new CameraCaptureSession.StateCallback() {

                        @Override
                        public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
                            if (null == mCameraDevice) {
                                return;
                            }
                            mCaptureSession = cameraCaptureSession;
                            try {
                                // 自動變焦是連續(xù)的
                                mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
                                        CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
                                setAutoFlash(mPreviewRequestBuilder);
                                // 顯示相機預覽
                                mPreviewRequest = mPreviewRequestBuilder.build();
                                //設置反復捕獲數(shù)據(jù)的請求,這樣預覽界面就會一直有數(shù)據(jù)顯示
                                mCaptureSession.setRepeatingRequest(mPreviewRequest,
                                        mCaptureCallback, mBackgroundHandler);
                            } catch (CameraAccessException e) {
                                e.printStackTrace();
                            }
                        }

                        @Override
                        public void onConfigureFailed(
                                @NonNull CameraCaptureSession cameraCaptureSession) {
                        }
                    }, null
            );
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

createCaptureRequest()方法里參數(shù)templateType代表了請求類型,請求類型一共分為六種,分別為:
TEMPLATE_PREVIEW:創(chuàng)建預覽的請求
TEMPLATE_STILL_CAPTURE:創(chuàng)建一個適合于靜態(tài)圖像捕獲的請求,圖像質(zhì)量優(yōu)先于幀速率。
TEMPLATE_RECORD:創(chuàng)建視頻錄制的請求
TEMPLATE_VIDEO_SNAPSHOT:創(chuàng)建視視頻錄制時截屏的請求
TEMPLATE_ZERO_SHUTTER_LAG:創(chuàng)建一個適用于零快門延遲的請求。在不影響預覽幀率的情況下最大化圖像質(zhì)量。
TEMPLATE_MANUAL:創(chuàng)建一個基本捕獲請求,這種請求中所有的自動控制都是禁用的(自動曝光,自動白平衡、自動焦點)。

createCaptureSession()方法一共包含三個參數(shù):
1.List outputs:我們需要輸出到的Surface列表。
2.CameraCaptureSession.StateCallback callback:會話狀態(tài)相關(guān)回調(diào)。
3.Handler handler:callback可以有多個(來自不同線程),這個handler用來區(qū)別那個callback應該被回調(diào),一般寫當前線程的Handler即可。

關(guān)于CameraCaptureSession.StateCallback里的回調(diào)方法
1.onConfigured(@NonNull CameraCaptureSession session); 攝像頭完成配置,可以處理Capture請求了。
2.onConfigureFailed(@NonNull CameraCaptureSession session); 攝像頭配置失敗
3.onReady(@NonNull CameraCaptureSession session); 攝像頭處于就緒狀態(tài),當前沒有請求需要處理。

onActive(@NonNull CameraCaptureSession session); 攝像頭正在處理請求。
onClosed(@NonNull CameraCaptureSession session); 會話被關(guān)閉
onSurfacePrepared(@NonNull CameraCaptureSession session, @NonNull Surface surface); Surface準備就緒
理解了這些東西,創(chuàng)建預覽請求就十分簡單了。

設置預覽界面尺寸信息,Surface就把它與CaptureRequestBuilder對象關(guān)聯(lián),然后就是設置會話開始捕獲畫面。最后的回調(diào)CameraCaptureSession.CaptureCallback就給我們設置預覽完成的邏輯處理

/**
     * ImageReader 的回調(diào)對象。 靜止圖像已準備好保存。
     */
    private final ImageReader.OnImageAvailableListener mOnImageAvailableListener
            = new ImageReader.OnImageAvailableListener() {
        @Override
        public void onImageAvailable(ImageReader reader) {
            Log.e(TAG, "onImageAvailable:-------------------");
            Image image = reader.acquireLatestImage();
            //我們可以將這幀數(shù)據(jù)轉(zhuǎn)成字節(jié)數(shù)組,類似于Camera1的PreviewCallback回調(diào)的預覽幀數(shù)據(jù)
            ByteBuffer buffer = image.getPlanes()[0].getBuffer();
            byte[] data = new byte[buffer.remaining()];
            buffer.get(data);
            image.close();
//            mBackgroundHandler.post(new ImageSaver(reader.acquireNextImage(), mFile));
        }

    };

最后這里的方法 就是同 camera 中的 實時幀數(shù)據(jù),這里我們可以在這里獲取 靜止圖片的 幀數(shù)據(jù),進行一些 圖層 水印的處理。google 源碼中 一直維系了一個mBackgroundHandler ,我們可以在這里發(fā)送 子線程任務reade.acquireNextImage方法獲取 靜止圖片信息,將圖片保存到本地文件夾中,基本流程完成后,我們只需要看一下如何觸發(fā) 拍照即可。

    /**
     * 鎖定焦點設置
     */
    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private void lockFocus() {
        try {
            // 相機鎖定的方法 (設置相機對焦)
            mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
                    CameraMetadata.CONTROL_AF_TRIGGER_START);
            // mCaptureCallback 等待鎖定  //修改狀態(tài)
            mState = STATE_WAITING_LOCK;
            //發(fā)送對焦請求
            mCaptureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback,
                    mBackgroundHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

通過點擊事件,調(diào)用相機鎖定 ,設置 mCaptureSession.capture,

    /**
     * 處理與jpg文件捕捉的事件監(jiān)聽(預覽)
     */
    private CameraCaptureSession.CaptureCallback mCaptureCallback
            = new CameraCaptureSession.CaptureCallback() {

        @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
        private void process(CaptureResult result) {
            switch (mState) {
                case STATE_PREVIEW: {
                    // 預覽正常
                    break;
                }
                  //等待對焦
                case STATE_WAITING_LOCK: { 
                    Integer afState = result.get(CaptureResult.CONTROL_AF_STATE);
                    if (afState == null) {
                        // 對焦失敗 直接拍照
                        captureStillPicture();
                    } else if (CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED == afState ||
                            CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED == afState) {
                        Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
                        if (aeState == null ||
                                aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) {
                            mState = STATE_PICTURE_TAKEN;
                            //  對焦完成,進行拍照
                            captureStillPicture();
                        } else {
                            runPrecaptureSequence();
                        }
                    }
                    break;
                }
                case STATE_WAITING_PRECAPTURE: {
                    Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
                    if (aeState == null ||
                            aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE ||
                            aeState == CaptureRequest.CONTROL_AE_STATE_FLASH_REQUIRED) {
                        mState = STATE_WAITING_NON_PRECAPTURE;
                    }
                    break;
                }
                case STATE_WAITING_NON_PRECAPTURE: {
                    Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
                    if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) {
                        mState = STATE_PICTURE_TAKEN;
                        captureStillPicture();
                    }
                    break;
                }
                default:
                    break;
            }
        }

        @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
        @Override
        public void onCaptureProgressed(@NonNull CameraCaptureSession session,
                                        @NonNull CaptureRequest request,
                                        @NonNull CaptureResult partialResult) {
            process(partialResult);
        }

        @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
        @Override
        public void onCaptureCompleted(@NonNull CameraCaptureSession session,
                                       @NonNull CaptureRequest request,
                                       @NonNull TotalCaptureResult result) {
            process(result);
        }

    };
    /**
     * 拍攝靜止圖片。 當我們得到響應時,應該調(diào)用此方法
     */
    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private void captureStillPicture() {
        try {
            if (null == activity || null == mCameraDevice) {
                return;
            }
            final CaptureRequest.Builder captureBuilder =
                    mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
            captureBuilder.addTarget(mImageReader.getSurface());

            // 使用與預覽相同的AE和AF模式。
            captureBuilder.set(CaptureRequest.CONTROL_AF_MODE,
                    CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
            setAutoFlash(captureBuilder);

            int rotation = getWindowManager().getDefaultDisplay().getRotation();
            // 設置 方向
            captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(rotation));
            //創(chuàng)建會話
            CameraCaptureSession.CaptureCallback CaptureCallback
                    = new CameraCaptureSession.CaptureCallback() {

                @Override
                public void onCaptureCompleted(@NonNull CameraCaptureSession session,
                                               @NonNull CaptureRequest request,
                                               @NonNull TotalCaptureResult result) {
                    Log.e(TAG, mFile.toString());
                    unlockFocus();
                }
            };

            mCaptureSession.stopRepeating();
            mCaptureSession.abortCaptures();
            mCaptureSession.capture(captureBuilder.build(), CaptureCallback, mBackgroundHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

此篇文章主要記錄 camera2 自定義相機的簡單流程,代碼基本都來自于 Google 提供的代碼,當然 如果有 Kotlin 的朋友,也可以在文章 開頭進入Google github 地址查看Kotlin 版本。

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

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