Android 用 camera2 API 自定义相机

0.246字数 611阅读 11638

前言

笔者因为项目需要自定义相机,所以了解了一下 Android 关于 camera 这块的 API。Android SDK 21(LOLLIPOP) 开始已经弃用了之前的 Camera 类,提供了 camera2 相关 API,目前网上关于 camera2 API 介绍的资料比较少,笔者搜集网上资料,结合自己的实践,在这里做一个总结。

流程

因为 camera2 提供的接口比较多,虽然很灵活,但是也增加了使用的复杂度。首先来大致了解一下调用 camera2 的流程,方便我们理清思路。

要显示相机捕捉的画面,只需要三步:初始化相机,预览,更新预览。也就是上图中左侧的部分。要实现这三步,需要用到的主要接口类和它们的作用步骤如上图右侧部分所示。下面就用代码来详解一下。

案例

首先创建一个相机界面:

activity_camera.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">
    
    <TextureView
            android:id="@+id/camera_texture_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

    <ImageButton
            android:id="@+id/capture_ib"
            android:layout_width="60dp"
            android:layout_height="60dp"
            android:layout_marginBottom="10dp"
            android:layout_gravity="bottom|center"
            android:background="@drawable/send_pres"/>

</LinearLayout>

界面很简单,只有一个 TexureView 和一个按钮。
接下来在 Activity 中初始化并显示相机捕捉的画面。

首先要解决的一个问题就是画面拉伸的问题。
要解决这个问题,首先要从 TextureView 下手。

CameraActivity.java

mTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
            @Override
            public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
                mWidth = width;
                mHeight = height;
                getCameraId();
                openCamera();
            }

            @Override
            public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int i, int i1) {

            }

            @Override
            public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
                return false;
            }

            @Override
            public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {

            }
        });

在 onSurfaceTextureAvailable 中初始化相机。通过 CameraManager 对象 openCamera,这正是流程图中 Init 步骤中的第一步。openCamera 有三个参数,第一个是 String 类型的 cameraId,第二个是 CameraDevice.StateCallback,第三个是 Handler。这里我们要声明一个 StateCallback:

private CameraDevice.StateCallback mCameraDeviceStateCallback = new CameraDevice.StateCallback() {
        @Override
        public void onOpened(CameraDevice cameraDevice) {
            mCameraDevice = cameraDevice;
            createCameraPreview();
        }

        @Override
        public void onDisconnected(CameraDevice cameraDevice) {
            mCameraDevice.close();
            mCameraDevice = null;
        }

        @Override
        public void onError(CameraDevice cameraDevice, int i) {
            mCameraDevice.close();
            mCameraDevice = null;
        }
    };

可以看到,在 camera 准备完毕之后就可以创建预览界面了。解决画面拉伸的问题就是要为预览界面设置一个合适比例的 SurfaceTexture buffer size。

private void createCameraPreview() {
        try {
            SurfaceTexture texture = mTextureView.getSurfaceTexture();
            assert texture != null;
            CameraCharacteristics characteristics = mCameraManager.getCameraCharacteristics(mCameraId);
            StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
            int deviceOrientation = getWindowManager().getDefaultDisplay().getOrientation();
            int totalRotation = sensorToDeviceRotation(characteristics, deviceOrientation);
            boolean swapRotation = totalRotation == 90 || totalRotation == 270;
            int rotatedWidth = mWidth;
            int rotatedHeight = mHeight;
            if (swapRotation) {
                rotatedWidth = mHeight;
                rotatedHeight = mWidth;
            }
            mPreviewSize = getPreferredPreviewSize(map.getOutputSizes(SurfaceTexture.class), rotatedWidth, rotatedHeight);
            texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
            Log.e("CameraActivity", "OptimalSize width: " + mPreviewSize.getWidth() + " height: " + mPreviewSize.getHeight());
            ...

这里根据当前设备及传感器的旋转角度来判断是否交换宽高值,然后通过 CameraCharacteristics 来得到最适合当前大小比例的宽高,然后把这个宽高设置给 SurfaceTexture 。

private Size getPreferredPreviewSize(Size[] sizes, int width, int height) {
        List<Size> collectorSizes = new ArrayList<>();
        for (Size option : sizes) {
            if (width > height) {
                if (option.getWidth() > width && option.getHeight() > height) {
                    collectorSizes.add(option);
                }
            } else {
                if (option.getHeight() > width && option.getWidth() > height) {
                    collectorSizes.add(option);
                }
            }
        }
        if (collectorSizes.size() > 0) {
            return Collections.min(collectorSizes, new Comparator<Size>() {
                @Override
                public int compare(Size s1, Size s2) {
                    return Long.signum(s1.getWidth() * s1.getHeight() - s2.getWidth() * s2.getHeight());
                }
            });
        }
        return sizes[0];
    }

这里 Sizes 是相机返回的支持的分辨率,从我们传递的参数找找到一个最接近的分辨率。

接下来就要通过 CaptureRequest.Builder以及 CameraCaptureSession.StateCallback 来创建及更新预览界面:

...
Surface surface = new Surface(texture);
            mBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
            // 设置预览对象
            mBuilder.addTarget(surface);
            mCameraDevice.createCaptureSession(Arrays.asList(surface), new CameraCaptureSession.StateCallback() {
                @Override
                public void onConfigured(CameraCaptureSession cameraCaptureSession) {
                    if (null == mCameraDevice) {
                        return;
                    }
                    mSession = cameraCaptureSession;
                    mBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO);
                    try {
                        // 不停地将捕捉的画面更新到 TextureView
                        mSession.setRepeatingRequest(mBuilder.build(), mSessionCaptureCallback, mBackgroundHandler);
                    } catch (CameraAccessException e) {
                        e.printStackTrace();
                    }
                }

                @Override
                public void onConfigureFailed(CameraCaptureSession cameraCaptureSession) {
                    Toast.makeText(CameraActivity.this, "Camera configuration change", Toast.LENGTH_SHORT).show();
                }
            }, null);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }

这样就完成了自定义相机第一步。源码地址请戳这里。

推荐阅读更多精彩内容