1. 程式人生 > >Android Camera2教程之開啟相機、開啟預覽、實現PreviewCallback、拍照

Android Camera2教程之開啟相機、開啟預覽、實現PreviewCallback、拍照

Android API 21新增了Camera2,這與之前的camera架構完全不同,使用起來也比較複雜,但是功能變得很強大。

在講解開啟預覽之前,首先需要了解camera2的幾個比較重要的類:

CameraManager: 管理手機上的所有攝像頭裝置,它的作用主要是獲取攝像頭列表和開啟指定的攝像頭
CameraDevice: 具體的攝像頭裝置,它有一系列引數(預覽尺寸、拍照尺寸等),可以通過CameraManager的getCameraCharacteristics()方法獲取。它的作用主要是建立CameraCaptureSession和CaptureRequest
CameraCaptureSession: 相機捕獲會話,用於處理拍照和預覽的工作(很重要)
CaptureRequest: 捕獲請求,定義輸出緩衝區以及顯示介面(TextureView或SurfaceView)等
1,定義TextureView作為預覽介面
在佈局檔案中加入TextureView控制元件,然後實現其監聽事件

textureView = (TextureView) findViewById(R.id.textureView);
1
然後我們可以在OnResume()方法中設定監聽SurefaceTexture的事件

textureView.setSurfaceTextureListener(textureListener);
1
當SurefaceTexture準備好後會回撥SurfaceTextureListener 的onSurfaceTextureAvailable()方法

TextureView.SurfaceTextureListener textureListener = new TextureView.SurfaceTextureListener() {
   

@Override
    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
        //當SurefaceTexture可用的時候,設定相機引數並開啟相機
        setupCamera(width, height);
        openCamera();
    }
};

2,設定相機引數
為了更好地預覽,我們根據TextureView的尺寸設定預覽尺寸,Camera2中使用CameraManager來管理攝像頭

private void setupCamera(int width, int height) {
        //獲取攝像頭的管理者CameraManager
    CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
    try {
        //遍歷所有攝像頭
        for (String cameraId: manager.getCameraIdList()) {
            CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId);
            //預設開啟後置攝像頭
            if (characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT)
                continue;
            //獲取StreamConfigurationMap,它是管理攝像頭支援的所有輸出格式和尺寸
            StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
            //根據TextureView的尺寸設定預覽尺寸
            mPreviewSize = getOptimalSize(map.getOutputSizes(SurfaceTexture.class), width, height);
            mCameraId = cameraId;
            break;
        }
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}

3,開啟相機
Camera2中開啟相機也需要通過CameraManager類

private void openCamera() {
    //獲取攝像頭的管理者CameraManager
    CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
    //檢查許可權
    try {
        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
            return;
        }
        //開啟相機,第一個引數指示開啟哪個攝像頭,第二個引數stateCallback為相機的狀態回撥介面,第三個引數用來確定Callback在哪個執行緒執行,為null的話就在當前執行緒執行
        manager.openCamera(mCameraId, stateCallback, null);
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}

實現StateCallback 介面,當相機開啟後會回撥onOpened方法,在這個方法裡面開啟預覽

private final CameraDevice.StateCallback stateCallback = new CameraDevice.StateCallback() {
    @Override
    public void onOpened(CameraDevice camera) {
        mCameraDevice = camera;
        //開啟預覽
        startPreview();
    }
}

4,開啟相機預覽
我們使用TextureView顯示相機預覽資料,Camera2的預覽和拍照資料都是使用CameraCaptureSession會話來請求的

private void startPreview() {
    SurfaceTexture mSurfaceTexture = mTextureView.getSurfaceTexture();
    //設定TextureView的緩衝區大小
    mSurfaceTexture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
    //獲取Surface顯示預覽資料
    Surface mSurface = new Surface(mSurfaceTexture);
    try {
        //建立CaptureRequestBuilder,TEMPLATE_PREVIEW比表示預覽請求
        mCaptureRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
        //設定Surface作為預覽資料的顯示介面
        mCaptureRequestBuilder.addTarget(mSurface);
        //建立相機捕獲會話,第一個引數是捕獲資料的輸出Surface列表,第二個引數是CameraCaptureSession的狀態回撥介面,當它建立好後會回撥onConfigured方法,第三個引數用來確定Callback在哪個執行緒執行,為null的話就在當前執行緒執行
        mCameraDevice.createCaptureSession(Arrays.asList(mSurface), new CameraCaptureSession.StateCallback() {
            @Override
            public void onConfigured(CameraCaptureSession session) {
                try {
                        //建立捕獲請求
                        mCaptureRequest = mCaptureRequestBuilder.build();
                        mPreviewSession = session;
                        //設定反覆捕獲資料的請求,這樣預覽介面就會一直有資料顯示
                        mPreviewSession.setRepeatingRequest(mCaptureRequest, mSessionCaptureCallback, null);
                    } catch (CameraAccessException e) {
                        e.printStackTrace();
                    }
                }

            @Override
            public void onConfigureFailed(CameraCaptureSession session) {

            }
        }, null);
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}

5,實現PreviewCallback
Camera2中並沒有Camera1中的PreviewCallback介面,那怎麼實現獲取預覽幀資料呢?答案就是使用ImageReader間接實現

首先建立一個ImageReader,並監聽它的事件

private void setupImageReader() {
    //前三個引數分別是需要的尺寸和格式,最後一個引數代表每次最多獲取幾幀資料,本例的2代表ImageReader中最多可以獲取兩幀影象流
    mImageReader = ImageReader.newInstance(mPreviewSize.getWidth(), mPreviewSize.getHeight(),
                ImageFormat.JPEG, 2);
    //監聽ImageReader的事件,當有影象流資料可用時會回撥onImageAvailable方法,它的引數就是預覽幀資料,可以對這幀資料進行處理
    mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
        @Override
        public void onImageAvailable(ImageReader reader) {
            Image image = reader.acquireLatestImage();
            //我們可以將這幀資料轉成位元組陣列,類似於Camera1的PreviewCallback回撥的預覽幀資料
            ByteBuffer buffer = image.getPlanes()[0].getBuffer();
            byte[] data = new byte[buffer.remaining()];
            buffer.get(data);
            image.close();
        }
    }, null);
}

注意:一定要呼叫reader.acquireLatestImage()和close()方法,否則畫面就會卡住

然後我們在開啟預覽之前,設定ImageReader為輸出Surface

setupImageReader();

//獲取ImageReader的Surface
Surface imageReaderSurface = mImageReader.getSurface();

//CaptureRequest新增imageReaderSurface,不加的話就會導致ImageReader的onImageAvailable()方法不會回撥
mCaptureRequestBuilder.addTarget(imageReaderSurface);

//建立CaptureSession時加上imageReaderSurface,如下,這樣預覽資料就會同時輸出到previewSurface和imageReaderSurface了
mCameraDevice.createCaptureSession(Arrays.asList(previewSurface, imageReaderSurface), new CameraCaptureSession.StateCallback() {

}

關閉相機時別忘了關閉ImageReader

6,拍照
Camera2拍照也是通過ImageReader來實現的

首先先做些準備工作,設定拍照引數,如方向、尺寸等

private static final SparseIntArray ORIENTATION = new SparseIntArray();
    static {
        ORIENTATION.append(Surface.ROTATION_0, 90);
        ORIENTATION.append(Surface.ROTATION_90, 0);
        ORIENTATION.append(Surface.ROTATION_180, 270);
        ORIENTATION.append(Surface.ROTATION_270, 180);
    }

設定拍照尺寸,可以跟預覽尺寸一起設定,然後ImageReader初始化使用此尺寸

mCaptureSize = Collections.max(Arrays.asList(map.getOutputSizes(ImageFormat.JPEG)), new Comparator<Size>() {
                    @Override
                    public int compare(Size lhs, Size rhs) {
                        return Long.signum(lhs.getWidth() * lhs.getHeight() - rhs.getHeight() * rhs.getWidth());
                    }
                });

建立儲存圖片的執行緒

public static class imageSaver implements Runnable {
        private Image mImage;
        public imageSaver(Image image) {
            mImage = image;
        }
        @Override
        public void run() {
            ByteBuffer buffer = mImage.getPlanes()[0].getBuffer();
            byte[] data = new byte[buffer.remaining()];
            buffer.get(data);
            mImageFile = new File(Environment.getExternalStorageDirectory() + "/DCIM/myPicture.jpg");
            FileOutputStream fos = null;
            try {
                fos = new FileOutputStream(mImageFile);
                fos.write(data, 0 ,data.length);
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                mImageFile = null;
                if (fos != null) {
                    try {
                        fos.close();
                        fos = null;
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

然後當ImageReader有資料時,通過此執行緒儲存圖片

//使用前面獲取的拍照尺寸
mImageReader = ImageReader.newInstance(mCaptureSize.getWidth(), mCaptureSize.getHeight(),
                ImageFormat.JPEG, 2);
mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
            @Override
            public void onImageAvailable(ImageReader reader) {
                //執行影象儲存子執行緒
                mCameraHandler.post(new imageSaver(reader.acquireNextImage()));
            }
        }, mCameraHandler);

然後開啟預覽建立CaptureSession時把ImageReader新增進去

mCameraDevice.createCaptureSession(Arrays.asList(previewSurface, mImageReader.getSurface()), new CameraCaptureSession.StateCallback() { 
}

現在準備工作做好了,還需要響應點選拍照事件,我們設定點選拍照按鈕呼叫capture()方法,capture()方法即實現拍照

private void capture() {
        try {
            //首先我們建立請求拍照的CaptureRequest
            final CaptureRequest.Builder mCaptureBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
            //獲取螢幕方向
            int rotation = getWindowManager().getDefaultDisplay().getRotation();
            //設定CaptureRequest輸出到mImageReader
            mCaptureBuilder.addTarget(mImageReader.getSurface());
            //設定拍照方向
            mCaptureBuilder.set(CaptureRequest.JPEG_ORIENTATION, ORIENTATION.get(rotation));
            //這個回撥介面用於拍照結束時重啟預覽,因為拍照會導致預覽停止
            CameraCaptureSession.CaptureCallback mImageSavedCallback = new CameraCaptureSession.CaptureCallback() {
                @Override
                public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) {
                    Toast.makeText(getApplicationContext(), "Image Saved!", Toast.LENGTH_SHORT).show();
                    //重啟預覽
                    restartPreview();
                }
            };
            //停止預覽
            mCameraCaptureSession.stopRepeating();
            //開始拍照,然後回撥上面的介面重啟預覽,因為mCaptureBuilder設定ImageReader作為target,所以會自動回撥ImageReader的onImageAvailable()方法儲存圖片
            mCameraCaptureSession.capture(mCaptureBuilder.build(), mImageSavedCallback, null);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

重啟預覽的方法很簡單了

 private void restartPreview() {
        try {
        //執行setRepeatingRequest方法就行了,注意mCaptureRequest是之前開啟預覽設定的請求
                       mCameraCaptureSession.setRepeatingRequest(mCaptureRequest, null, mCameraHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }