控制相机

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

注意:本页会提到已废弃的 Camera 类。建议您改用 CameraX;在特定的使用场景下,也可以改用 Camera2。CameraX 和 Camera2 都支持 Android 5.0(API 级别 21)及更高版本。

相比向现有的相机应用请求照片或视频,直接控制设备的相机所需要的代码要多得多。不过,如果您想要开发专门的相机应用或者完全集成到您的应用界面中的功能,可以参考本课中介绍的具体方式。

请参阅以下相关资源:

打开 Camera 对象

获取 Camera 对象的实例是直接控制相机流程的第一步。要访问相机,建议的方式是通过从 onCreate() 中启动的一个单独线程打开 Camera,Android 自己的相机应用也是采取这种方式。这种方法非常好,因为这样做可能会争取一些时间,从而延迟界面线程。在更基本的实现方案中,打开相机的操作可以交给后面的 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 对象,然后才能启动图片实时预览,具体见下一部分中的说明。

设置并启动预览

必须按特定的顺序来创建 Camera 实例及其相关预览,其中 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()

如果要连续抓拍图片,您可以创建一个实现 onPreviewFrame()Camera.PreviewCallback。对于这些图片之间的图片,您可以仅捕捉选定的预览画面,或设置一项延迟的操作来调用 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() 方法的一部分,因此要初始化相机,您必须先停止预览。