app: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
  • 2

然後我們可以在OnResume()方法中設置監聽SurefaceTexture的事件

textureView.setSurfaceTextureListener(textureListener);
  • 1
  • 2

當SurefaceTexture準備好後會回調SurfaceTextureListener 的onSurfaceTextureAvailable()方法

TextureView.SurfaceTextureListener textureListener = new TextureView.SurfaceTextureListener() {
    @Override
    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
        //當SurefaceTexture可用的時候,設置相機參數並打開相機
        setupCamera(width, height);
        openCamera();
    }
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

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();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

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();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

實現StateCallback 接口,當相機打開後會回調onOpened方法,在這個方法裏面開啓預覽

private final CameraDevice.StateCallback stateCallback = new CameraDevice.StateCallback() {
    @Override
    public void onOpened(CameraDevice camera) {
        mCameraDevice = camera;
        //開啓預覽
        startPreview();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

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();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

注意:一定要調用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() {

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

關閉相機時別忘了關閉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);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

設置拍照尺寸,可以跟預覽尺寸一起設置,然後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());
                    }
                });
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

創建保存圖片的線程

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();
                    }
                }
            }
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

然後當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);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

然後開啓預覽創建CaptureSession時把ImageReader添加進去

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

現在準備工作做好了,還需要響應點擊拍照事件,我們設置點擊拍照按鈕調用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();
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

重啓預覽的方法很簡單了

 private void restartPreview() {
        try {
        //執行setRepeatingRequest方法就行了,注意mCaptureRequest是之前開啓預覽設置的請求
                       mCameraCaptureSession.setRepeatingRequest(mCaptureRequest, null, mCameraHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

 http://blog.csdn.net/lb377463323/article/details/52740411

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章