控制相机

在本课程中,我们将讨论如何使用框架 API 直接控制相机硬件。

与从已有相机应用请求照片或视频相比,直接控制设备相机所需要的代码要多得多。不过,如果您要开发专门的相机应用或者与您的应用界面完全集成的应用,本课程将向您介绍具体操作方法。

请参阅以下相关资源:

打开 Camera 对象

获取 Camera 对象的实例是直接控制相机流程的第一步。与使用 Android 自身相机应用的方式一样,建议通过从 onCreate() 启动的单独线程上打开 Camera 的方式访问相机。这种方法非常好,因为这样做可能会争取一些时间,从而延迟界面线程。在更基本的实现中,打开相机的操作可以推迟到 onResume() 方法,这样便于重用代码并尽可能简化控制流程。

如果相机已被其他应用使用,则调用 Camera.open() 会抛出异常,因此我们将其封装在 try 块中。

Kotlin

    private fun safeCameraOpen(id: Int): Boolean {
        return try {
            releaseCameraAndPreview()
            mCamera = Camera.open(id)
            true
        } catch (e: Exception) {
            Log.e(getString(R.string.app_name), "failed to open Camera")
            e.printStackTrace()
            false
        }
    }

    private fun releaseCameraAndPreview() {
        preview?.setCamera(null)
        mCamera?.also { camera ->
            camera.release()
            mCamera = null
        }
    }
    

Java

    private boolean safeCameraOpen(int id) {
        boolean qOpened = false;

        try {
            releaseCameraAndPreview();
            camera = Camera.open(id);
            qOpened = (camera != null);
        } catch (Exception e) {
            Log.e(getString(R.string.app_name), "failed to open Camera");
            e.printStackTrace();
        }

        return qOpened;
    }

    private void releaseCameraAndPreview() {
        preview.setCamera(null);
        if (camera != null) {
            camera.release();
            camera = null;
        }
    }
    

从 API 级别 9 开始,相机框架支持多个摄像头。如果您使用的是旧版 API 并且调用不带参数的 open(),则会获得第一个后置摄像头。

创建相机预览

在拍摄照片时,用户通常要先看到拍摄目标的预览效果,然后再按下快门。为此,您可以使用 SurfaceView 绘制摄像头传感器需要获取的内容的预览效果。

预览类

如需开始显示预览,您需要预览类。预览需要实现 android.view.SurfaceHolder.Callback 接口,该接口用于将图片数据从相机硬件传递到相机应用。

Kotlin

    class Preview(
            context: Context,
            val surfaceView: SurfaceView = SurfaceView(context)
    ) : ViewGroup(context), SurfaceHolder.Callback {

        var mHolder: SurfaceHolder = surfaceView.holder.apply {
            addCallback(this@Preview)
            setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS)
        }
        ...
    }
    

Java

    class Preview extends ViewGroup implements SurfaceHolder.Callback {

        SurfaceView surfaceView;
        SurfaceHolder holder;

        Preview(Context context) {
            super(context);

            surfaceView = new SurfaceView(context);
            addView(surfaceView);

            // Install a SurfaceHolder.Callback so we get notified when the
            // underlying surface is created and destroyed.
            holder = surfaceView.getHolder();
            holder.addCallback(this);
            holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
        }
    ...
    }
    

您必须先将预览类传递给 Camera 对象,然后动态图片预览才会启动,如下一部分中所示。

设置并启动预览

您必须按特定顺序创建相机实例及其相关预览,其中相机对象位于第一位。在下面的代码段中,初始化相机的过程已经过封装,这样每当用户执行操作以更改相机时,setCamera() 方法都会调用 Camera.startPreview()。您还必须在预览类 surfaceChanged() 回调方法中重启预览。

Kotlin

    fun setCamera(camera: Camera?) {
        if (mCamera == camera) {
            return
        }

        stopPreviewAndFreeCamera()

        mCamera = camera

        mCamera?.apply {
            mSupportedPreviewSizes = parameters.supportedPreviewSizes
            requestLayout()

            try {
                setPreviewDisplay(holder)
            } catch (e: IOException) {
                e.printStackTrace()
            }

            // Important: Call startPreview() to start updating the preview
            // surface. Preview must be started before you can take a picture.
            startPreview()
        }
    }
    

Java

    public void setCamera(Camera camera) {
        if (mCamera == camera) { return; }

        stopPreviewAndFreeCamera();

        mCamera = camera;

        if (mCamera != null) {
            List<Size> localSizes = mCamera.getParameters().getSupportedPreviewSizes();
            supportedPreviewSizes = localSizes;
            requestLayout();

            try {
                mCamera.setPreviewDisplay(holder);
            } catch (IOException e) {
                e.printStackTrace();
            }

            // Important: Call startPreview() to start updating the preview
            // surface. Preview must be started before you can take a picture.
            mCamera.startPreview();
        }
    }
    

修改相机设置

相机设置会更改相机拍照的方式(从缩放级别到曝光补偿)。此示例只会更改预览大小;请查看相机应用的源代码以了解更多信息。

Kotlin

    override fun surfaceChanged(holder: SurfaceHolder, format: Int, w: Int, h: Int) {
        mCamera?.apply {
            // Now that the size is known, set up the camera parameters and begin
            // the preview.
            parameters?.also { params ->
                params.setPreviewSize(previewSize.width, previewSize.height)
                requestLayout()
                parameters = params
            }

            // Important: Call startPreview() to start updating the preview surface.
            // Preview must be started before you can take a picture.
            startPreview()
        }
    }
    

Java

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
        // Now that the size is known, set up the camera parameters and begin
        // the preview.
        Camera.Parameters parameters = mCamera.getParameters();
        parameters.setPreviewSize(previewSize.width, previewSize.height);
        requestLayout();
        mCamera.setParameters(parameters);

        // Important: Call startPreview() to start updating the preview surface.
        // Preview must be started before you can take a picture.
        mCamera.startPreview();
    }
    

设置预览屏幕方向

大多数相机应用会将屏幕锁定为横屏模式,因为这是摄像头传感器的自然屏幕方向。此设置不会阻止您拍摄纵向模式的照片,因为设备的屏幕方向会记录在 EXIF 头文件中。通过 setCameraDisplayOrientation() 方法,您可以更改预览的显示方式,而不会影响图片的记录方式。不过,在 API 级别 14 之前的 Android 中,您必须先停止预览,再更改屏幕方向,然后重启预览。

拍照

使用 Camera.takePicture() 方法在启动预览后拍照。您可以创建 Camera.PictureCallbackCamera.ShutterCallback 对象并将其传递到 Camera.takePicture()

如果要连续抓取图片,您可以创建 Camera.PreviewCallback,用于实现 onPreviewFrame()。对于这些图片之间的图片,您可以仅捕获选定的预览画面,或设置延迟操作以调用 takePicture()

重启预览

拍摄一张照片后,您必须重启预览,然后用户才能拍摄另一张照片。在下面的示例中,重启是通过过载快门按钮完成的。

Kotlin

    fun onClick(v: View) {
        previewState = if (previewState == K_STATE_FROZEN) {
            camera?.startPreview()
            K_STATE_PREVIEW
        } else {
            camera?.takePicture(null, rawCallback, null)
            K_STATE_BUSY
        }
        shutterBtnConfig()
    }
    

Java

    @Override
    public void onClick(View v) {
        switch(previewState) {
        case K_STATE_FROZEN:
            camera.startPreview();
            previewState = K_STATE_PREVIEW;
            break;

        default:
            camera.takePicture( null, rawCallback, null);
            previewState = K_STATE_BUSY;
        } // switch
        shutterBtnConfig();
    }
    

停止预览并释放相机

应用使用相机完成操作后,应该对相机进行清理。具体来说,您必须释放 Camera 对象,否则可能会面临其他应用(包括您自己应用的新实例)崩溃的风险。

应在何时停止预览并释放相机?如果预览表面遭到破坏,则明确表示您应该停止预览并释放相机(如 Preview 类中的下列方法所示)。

Kotlin

    override fun surfaceDestroyed(holder: SurfaceHolder) {
        // Surface will be destroyed when we return, so stop the preview.
        // Call stopPreview() to stop updating the preview surface.
        mCamera?.stopPreview()
    }

    /**
     * When this function returns, mCamera will be null.
     */
    private fun stopPreviewAndFreeCamera() {
        mCamera?.apply {
            // Call stopPreview() to stop updating the preview surface.
            stopPreview()

            // Important: Call release() to release the camera for use by other
            // applications. Applications should release the camera immediately
            // during onPause() and re-open() it during onResume()).
            release()

            mCamera = null
        }
    }
    

Java

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        // Surface will be destroyed when we return, so stop the preview.
        if (mCamera != null) {
            // Call stopPreview() to stop updating the preview surface.
            mCamera.stopPreview();
        }
    }

    /**
     * When this function returns, mCamera will be null.
     */
    private void stopPreviewAndFreeCamera() {

        if (mCamera != null) {
            // Call stopPreview() to stop updating the preview surface.
            mCamera.stopPreview();

            // Important: Call release() to release the camera for use by other
            // applications. Applications should release the camera immediately
            // during onPause() and re-open() it during onResume()).
            mCamera.release();

            mCamera = null;
        }
    }
    

如本课程前面所述,此过程也是 setCamera() 方法的一部分,因此要初始化相机,您必须先停止预览。