Android Camera 编程从入门到精通

一、前言

想通过一篇文章就让我们精通 Android 的 Camera 那肯定是不可能的事情。但通过对 Android 中相机拍照的所有的方式的梳理和理解,包括直接调起相机拍照,Camera API 1 以及 Camera API 2 的分析与理解,为我们指明一条通往精通 Android Camera 的路还是有可能的。文章将先对 Android Camera 有一个全局的认知,然后再分析拍照的各个关键路径及相关知识点。在实际开发过程中碰到问题再深入去了解 API 及其相关参数,应该就能解决我们在 Android Camera 编程中的大部分问题了。

二、相机基本使用以及 Camra API 1

这里主要涉及到的是如何直接调起系统相机拍照以及基于 Camra API 1 实现拍照。如下的思维导图是一个基本的导读。


Android 相机.jpg

1.权限及需求说明

要使用相机必须声明 CAMERA 权限以及告诉系统你要使用这个功能。

<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />

上面这是最基本的,但如果你需要写文件,录音,定位等还需要下面的权限

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
...
<!-- Needed only if your app targets Android 5.0 (API level 21) or higher. -->
<uses-feature android:name="android.hardware.location.gps" />

2.调起系统或者三方相机直接拍照

  • 拍照
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
  • 获取拍照后的照片
Uri contentUri = FileProvider.getUriForFile(this, "com.example.android.fileprovider", photoFile);

3.通过 Camera API 1 进行拍照

  • 相机设备检测
(context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA))
  • 打开相机
Camera.open();
// Camera.open(0)
// Camera.open(1)
  • 创建预览界面
/** A basic Camera preview class */
public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback {
    ......
    public CameraPreview(Context context, Camera camera) {
        ......
        mHolder.addCallback(this);
    }

    public void surfaceCreated(SurfaceHolder holder) {
       ......
            mCamera.setPreviewDisplay(holder);
            mCamera.startPreview();
       ......
    }

    public void surfaceDestroyed(SurfaceHolder holder) {
        ......
        mCamera.stopPreview();
    }

    public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
        ......
            mCamera.setPreviewDisplay(mHolder);
            mCamera.startPreview();
       ......
    }
}
...
}
  • 设置相机参数
public void setCamera(Camera camera) {
    ......
    if (mCamera != null) {
        List<Size> localSizes = mCamera.getParameters().getSupportedPreviewSizes();
        mSupportedPreviewSizes = localSizes;
        requestLayout();
       ......
      // 相机参数设置完成后,需要重新启动预览界面
      mCamera.setPreviewDisplay(mHolder);
      mCamera.startPreview();
       ......
    }
}
  • 停止预览及释放相机
    这个建议放在 onDestroy() 中调用
private void stopPreviewAndFreeCamera() {
    ......
        mCamera.stopPreview();
    ......
        mCamera.release();
        mCamera = null;
    }
}

以上就是如何通过调起系统或者三方相机以及通过调用 Camera API 1 来进行拍照的讲解,相对来说还是比较简单的。一般来说掌握 Camera API 1 的用法基本能满足常规开发了,但当我们需要获取更多相机设备的特性时,显然需要通过 Camera API 2 所提供的更加丰富的功能来达到目的了。对于基本的拍照以及 API 1 的讲解这里只是简单过一下,重点在 API 2 的介绍。

三、全新 Camera API 2

Camera API 2 是从 Android 5.0 L 版本开始引入的。官网对相机介绍的引导文档里是没有涉及到 API 2 的讲解的,都是基于 API 1 的。能找到的是其推荐的一篇博客Detecting camera features with Camera2 以及官方的 API 文档。通过文档大概了解到其比较重要的优点如下:

  • 改进了新硬件的性能。
  • 以更快的间隔拍摄图像。
  • 显示来自多个摄像头的预览。
  • 直接应用效果和过滤器。

看起来很爽,但是用起来那就是酸爽了,如下是梳理的一个思维导图。看看就知道有多麻烦了。


Camera API 2 拍照.jpg

四、官方 demo 分析

正是由于 Camera 的 API 从 1 到 2 发生了架构上的变化,而且使用难度也是大大地增加了好几倍,加上 Android 的碎片化又是如斯的严重。因此官方考虑到大家掌握不好,推出了其官方的 demo 供我们参考和学习——cameraview。这里也将基于官方的 demo 来深入掌握 Android 相机 API 2 的使用。

1. 主要类图

先来看看工程中主要的类图及其关系,好对整个工程以及 Camera2 中的相关类有一个基本的认知。

工程主类图

(1) 类图结构上封装了 CameraView 用于给 Activity 直接调用。
(2) 抽象了相机类 CameraViewImpl 和预览类 PreviewImpl。根据不同的版本由其具体实现类来解决版本之间的差异以及兼容。
(3) 用于预览的既可以是 SurfaceView 也可以是 TextureView,框架内根据不同版本做了相应的适配。
(4) Camera1 即使用的旧版 Camera 及其相关的 API。而 Camera2 使用了新的 Camera2 API,这里简要介绍一下这几个类的作用。

序号 说明
1 CameraManager 这是一个系统服务,主要用于管理相机设备的,如相机的打开。与 AlarmManager 同等级。
2 CameraDevice 这个就是相机设备了,与 Camra1 中的 Camera 同等级。
3 ImageReader 用于从相机打开的通道中读取需要的格式的原始图像数据,理论上一个设备可以连接 N 多个 ImageReader。在这里可以看成是和 Preview 同等级。
4 CaptureRequest.Builder CaptureRequest 构造器,主要给相机设置参数的类。Builder 设计模式真好用。
5 CameraCharacteristics 与 CaptureRequest 反过来,主要是获取相机参数的。
6 CameraCaptureSession 请求抓取相机图像帧的会话,会话的建立主要会建立起一个通道。源端是相机,另一端是 Target,Target 可以是 Preview,也可以是 ImageReader。

2.CameraView 初始化

先看一看 CameraView 初始化的时序图,大概一共做了 13 事情。当然,初始化做的事情其实都是简单的,主要就是初始化必要的对象且设置一些监听。

CameraView 初始化.jpg
  • CameraView 的构建方法
public CameraView(Context context, AttributeSet attrs, int defStyleAttr) {
       ......
        // 创建预览视图
        final PreviewImpl preview = createPreviewImpl(context);
        // Callback 桥接器,将相机内部的回调转发给调用层
        mCallbacks = new CallbackBridge();
        // 根据不同的 SDK 版本选择不同的 Camera 实现,这里假设选择了 Camera2
        if (Build.VERSION.SDK_INT < 21) {
            mImpl = new Camera1(mCallbacks, preview);
        } else if (Build.VERSION.SDK_INT < 23) {
            mImpl = new Camera2(mCallbacks, preview, context);
        } else {
            mImpl = new Camera2Api23(mCallbacks, preview, context);
        }
       ......
        // 设置相机 ID,如前置或者后置
        setFacing(a.getInt(R.styleable.CameraView_facing, FACING_BACK));
        ......
        // 设置预览界面的比例,如 4:3 或者 16:9
        setAspectRatio(AspectRatio.parse(aspectRatio));
        // 设置对焦方式
        setAutoFocus(a.getBoolean(R.styleable.CameraView_autoFocus, true));
       // 设置闪光灯
        setFlash(a.getInt(R.styleable.CameraView_flash, Constants.FLASH_AUTO));
       ......
        // 初始化显示设备(主要指手机屏幕)的旋转监听,主要用来设置相机的旋转方向
        mDisplayOrientationDetector = new DisplayOrientationDetector(context) {
            @Override
            public void onDisplayOrientationChanged(int displayOrientation) {
                mImpl.setDisplayOrientation(displayOrientation);
            }
        };
}

构造方法中所做的事情都在注释里进行说明,没有需要展开的。下面来看 createPreviewImpl()。

  • createPreviewImpl() 的实现
private PreviewImpl createPreviewImpl(Context context) {
        PreviewImpl preview;
        if (Build.VERSION.SDK_INT < 14) {
            preview = new SurfaceViewPreview(context, this);
        } else {
            preview = new TextureViewPreview(context, this);
        }
        return preview;
    }

这里的 SurfaceViewPreview 以及 TextureViewPreview 都是一个包装类,从名字上就可以知道其内部分别包装了 SurfaceView 和 TextureView 实例来实现相机的预览界面的。关于 SurfaceView 以及 TextureView 的区别,这里也再简单提一下,详细的可以参考其他大神的文章说明:
SurfaceView:是一个独立的 Window,由系统 WMS 直接管理,可支持硬件加速,也可以不支持硬件加速。
TextureView:可以看成是一个普通的 View,属于所于应用的视图层级树中,属于 ViewRootImpl 管理,只支持硬件加速。

尽管 SurfaceView 和 TextureView 有区别,但本质上它们都是对 Surface 的一个封装实现。

这里假设选择的是 TextureViewPreview。TextureViewPreview 的构造方法很简单,就是从 xml 里获取 TextureView 的实例,并且同时设置 TextureView 的监听 TextureView.SurfaceTextureListener,这个后面会再详细讲。

接下来是根据不同的版本选择 Camera,这里假设选择的是 Camera2,主线上我们也只分析它就可以了。那么就来看一看 Camera2 的实现吧。

  • 初始化 Camera2
Camera2(Callback callback, PreviewImpl preview, Context context) {
        super(callback, preview);
        mCameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
        mPreview.setCallback(new PreviewImpl.Callback() {
            @Override
            public void onSurfaceChanged() {
                startCaptureSession();
            }
        });
    }

首先是初始化 CameraManager 的实例,这是相比 Camera1 多出来的步骤,这么说 Camera 有一个专业的管理者了。其次可以看到这里是向 Context 获取一个系统 Service "CAMERA_SERVICE" 来初始化 CameraManager 的,这也说明了其被上升到了一个系统服务的高度了。

然后就是向 Preview 添加回调,监听其 Surface 的变化来作进一步的事情。

  • 关于 Camera 与 Preview 的选择
    这里 github 首页给出了 Android 的推荐选择。
API Level Camera API Preview View
9-13 Camera1 SurfaceView
14-20 Camera1 TextureView
21-23 Camera2 TextureView
24 Camera2 SurfaceView

API 20 以下用 Camera 1,20 以上用 Camera 2,这个没有争议。但是对于 Preview 的选择也根据 API 来选择, 这个就不应该了。看过其他相应的实现,除了 SDK API 的检查,应用 TextureView 前还应该要判断一下当前的运行环境是否支持硬件加速。

而让我有疑问的是这里的 24 以上推荐使用 SurfaceView,这个是为什么呢?而其里面的代码实际实现,看上面createPreviewImpl() 的实现可知又不是这样的,也是选择了 TextureView。

  • setFacing、setAspectRatio、setAutoFocus、setFlash
    这些都是设置参数,其实际生效的方法在 Camera2 中,而这个时候相机都还没有打开,对于它们的设置目前来说是不会立即生效的,只是记录下它们的值而已。后面我们分析时已默认值来分析即可。

当然,这里只是给出了 4 个参数,其实还有很多,后面还会讲到。

小结


到这里就分析完了 CameraView 的初始化了,其主要做了以下几件事情:
(1) 通过 getSystemService 初始化了 CameraManager。
(2) 准备好了 Preview ,用于相机的预览
(3) 设定好了相机要用的参数

3.打开相机

同样,先来看一看打开相机的时序图。概括了有 15 个步骤,但其实关键步骤没有这么多。


CameraView 打开相机.jpg
  • CameraView.start()
/**
     * Open a camera device and start showing camera preview. This is typically called from
     * {@link Activity#onResume()}.
     */
    public void start() {
        if (!mImpl.start()) {
            //store the state ,and restore this state after fall back o Camera1
            Parcelable state=onSaveInstanceState();
            // Camera2 uses legacy hardware layer; fall back to Camera1
            mImpl = new Camera1(mCallbacks, createPreviewImpl(getContext()));
            onRestoreInstanceState(state);
            mImpl.start();
        }
    }

这里给了几个关键的信息:
(1) 此方法推荐的是在 Activity#onResume() 方法里面进行调用,这个是很重要的,告诉了我们打开相机的最适合时机。
(2) 按照前面的场景,这里调用了 Camera2#start()。这是要进一步分析的。
(3) 如果打开 Camera2 失败了,则降级到 Camera1。做了回退保护,考虑的确实比较周到。

  • Camera2.start()
boolean start() {
        if (!chooseCameraIdByFacing()) {
            return false;
        }
        collectCameraInfo();
        prepareImageReader();
        startOpeningCamera();
        return true;
    }

都是内部调用,下面逐个分析这些方法的实现。

  • chooseCameraIdByFacing()
private boolean chooseCameraIdByFacing() {
        try {
            // 1.根据 mFacing 选择相机
            int internalFacing = INTERNAL_FACINGS.get(mFacing);
            // 2.获取所有的可用相机 ID 列表,注意相机的 ID 是 字串类型了
            final String[] ids = mCameraManager.getCameraIdList();
            ......
            for (String id : ids) {
                // 根据相机的 ID 获取相机特征
                CameraCharacteristics characteristics = mCameraManager.getCameraCharacteristics(id);
                // 查询其支持的硬件级别
                Integer level = characteristics.get(
                      CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
                if (level == null ||
                        level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
                    continue;
                }
                // 查询相机的方向(前置,后置或者外接),也可以同等看成是其整型的 ID
                Integer internal = characteristics.get(CameraCharacteristics.LENS_FACING);
                if (internal == null) {
                    throw new NullPointerException("Unexpected state: LENS_FACING null");
                }
                // 查出来的与所期望的相等,则认为就是要找到的相机设备
                if (internal == internalFacing) {
                    // 保存相机的 ID
                    mCameraId = id;
                    // 保存相机的特征参数
                    mCameraCharacteristics = characteristics;
                    return true;
                }
            }
            // 如果没找到就取第 0 个。后面的过程就跟上面是一样的。这里就省略了。一般来说第 0 个就是 ID 为 "1" 的相机,其方向为后置。
            mCameraId = ids[0];
            ......
            return true;
        } catch (CameraAccessException e) {
            throw new RuntimeException("Failed to get a list of camera devices", e);
        }
    }

这段代码确实有点长,并且信息量也多。其主要的目的是根据 mFacing 指定的相机方向选择一个正确的相机,但如果没有的话就默认选择后置相机。这个过程涉及到了几个比较重要的相机参数及其 API 调用。

(1) 关于选择相机方向
相机方向主要是相对于手机屏幕而言的,系统可取的值有 LENS_FACING_FRONT(前置),LENS_FACING_BACK(后置),LENS_FACING_EXTERNAL(外接)。但工程里只给我们定义了前置与后置。

static {
        INTERNAL_FACINGS.put(Constants.FACING_BACK, CameraCharacteristics.LENS_FACING_BACK);
        INTERNAL_FACINGS.put(Constants.FACING_FRONT, CameraCharacteristics.LENS_FACING_FRONT);
    }

(2)关于CameraCharacteristics
这里是查询出了所有的相机 ID ,然后来逐个遍历看是否与所期望的相机方向相符合的相机设备。这里要注意的是相机的 ID 是实际是字符串,这个需要记住并且很重要,后面的相机操作,如打开设备、查询或者设置参数等都是需要这个 ID 的。
通过 CameraManager. getCameraCharacteristics(ID) 查询出了相关设备的特征信息,特征信息都被封装在了 CameraCharacteristics 中。它以 Key<?>-Value 的形式储存了所有的相机设备的参数信息。注意这个 Key<?> ,它又是一个泛型,这说明了 Key 也是可以以不同的形式存在的。这样的扩展性就强了。特别是对于现在一些特殊摄像头的发展,如3D 摄像头,那么厂商就可自行添加参数支持而不用添加私有 API 了。这也是主要需要理解的部分。

(3)关于支持的硬件级别
了解了第(2)点,其他的就都只是参数查询的问题了。这里摘抄官网了。

  • LEGACY 对于较旧的Android设备,设备以向后兼容模式运行,并且功能非常有限。
  • LIMITED设备代表基线功能集,还可能包括作为子集的附加功能FULL。
  • FULL 设备还支持传感器,闪光灯,镜头和后处理设置的每帧手动控制,以及高速率的图像捕获。
  • LEVEL_3 设备还支持YUV重新处理和RAW图像捕获,以及其他输出流配置。
  • EXTERNAL设备类似于LIMITED设备,例如某些传感器或镜头信息未重新排列或不太稳定的帧速率。

CameraCharacteristics 中还有非常多的参数,这里仅列出其所提及到的,其他的参数如果你真的实际会在开发中用到,建议还是过一遍。这样一来,相机能做什么,具备什么特性就会有一个整体感知了。

  • collectCameraInfo()
private void collectCameraInfo() {
        // 获取此摄像机设备支持的可用流配置,其包括格式、大小、持续时间和停顿持续时间等
        StreamConfigurationMap map = mCameraCharacteristics.get(
                CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
        if (map == null) {
            throw new IllegalStateException("Failed to get configuration map: " + mCameraId);
        }
        mPreviewSizes.clear();
        // 根据需要渲染到的目标类型选择合适的输出大小
        for (android.util.Size size : map.getOutputSizes(mPreview.getOutputClass())) {
            int width = size.getWidth();
            int height = size.getHeight();
            if (width <= MAX_PREVIEW_WIDTH && height <= MAX_PREVIEW_HEIGHT) {
                mPreviewSizes.add(new Size(width, height));
            }
        }
        // 根据图片格式选择图片大小
        mPictureSizes.clear();
        collectPictureSizes(mPictureSizes, map);
        // 把预览中所支持的大小比例,但在图片大小比例不支持的 比例 移除掉
        for (AspectRatio ratio : mPreviewSizes.ratios()) {
            if (!mPictureSizes.ratios().contains(ratio)) {
                mPreviewSizes.remove(ratio);
            }
        }
        // 如果设置的比例不完全相符合,那选择一个接近的。
        if (!mPreviewSizes.ratios().contains(mAspectRatio)) {
            mAspectRatio = mPreviewSizes.ratios().iterator().next();
        }
    }

这段代码相对来说要简单一些,主要完成的是获取预览尺寸,图片尺寸以及合适的显示比例。

  • prepareImageReader()
private void prepareImageReader() {
        if (mImageReader != null) {
            mImageReader.close();
        }
        Size largest = mPictureSizes.sizes(mAspectRatio).last();
        mImageReader = ImageReader.newInstance(largest.getWidth(), largest.getHeight(),
                ImageFormat.JPEG, /* maxImages */ 2);
        mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, null);
    }

根据合适的图片尺寸初始化 ImageReader,主要是用于接收图片的原始数据信息,且这里的原始数据信息为 ImageFormat.JPEG。当然你也可以指定为 YUV 等更原始的数据信息。这样一来除了除了让图像显示在预览界面上,我们还可以同时获取原始数据信息做进一步处理,如增加滤镜效果后再保存等。

而要获取到原始数据信息,就需要向 ImageReader 注册相应的监听器 ImageReader.OnImageAvailableListener,当有相机的图像帧后会通过onImageAvailable 进行回调。这里展开看一下它的实现。

public void onImageAvailable(ImageReader reader) {
           // 获取  Image
            try (Image image = reader.acquireNextImage()) {
               // 获取 Image 的平面
                Image.Plane[] planes = image.getPlanes();
                if (planes.length > 0) {
                    // 获取平面 0 的 ByteBuffer,并从 ByteBuffer 中获取 byte[]
                    ByteBuffer buffer = planes[0].getBuffer();
                    byte[] data = new byte[buffer.remaining()];
                    buffer.get(data);
                    mCallback.onPictureTaken(data);
                }
            }
        }

这里涉及到了图像格式的知识, 这里就不细述了,感兴趣的同学可以自己去查一下资料。

  • startOpeningCamera()
private void startOpeningCamera() {
        try {
            mCameraManager.openCamera(mCameraId, mCameraDeviceCallback, null);
        } catch (CameraAccessException e) {
            throw new RuntimeException("Failed to open camera: " + mCameraId, e);
        }
    }

最后一步就是打开相机了,打开相机需要传递前面所确定的 CameraID,注意它是个字符串。还传入了一个 mCameraDeviceCallback,它的类型是 CameraDevice.StateCallback。看一看它的实现。

private final CameraDevice.StateCallback mCameraDeviceCallback
            = new CameraDevice.StateCallback() {
       // 相机打开
        @Override
        public void onOpened(@NonNull CameraDevice camera) {
            mCamera = camera;
            mCallback.onCameraOpened();
            startCaptureSession();
        }
        // 相机关闭
        @Override
        public void onClosed(@NonNull CameraDevice camera) {
            mCallback.onCameraClosed();
        }
       // 相机断开连接
        @Override
        public void onDisconnected(@NonNull CameraDevice camera) {
            mCamera = null;
        }
       // 打开相机出错
        @Override
        public void onError(@NonNull CameraDevice camera, int error) {
            Log.e(TAG, "onError: " + camera.getId() + " (" + error + ")");
            mCamera = null;
        }

    };

这里就是打开相机状态的回调监听,主要关注的是 onOpened()。在这个回调方法中返回了 CameraDevice ,也就是实际的相机设备。关于 CameraDevice 再来看一个类图。


CameraDevice.jpg

看出来了吧,CameraDevice 的实现类 CameraDeviceImpl 是持有了一个 Binder 端的代理。这里不看源码,只凭推测可知,实际的相机设备对象应该被放到了系统进程 SystemServer 或者别的进程中去了。这和 Camera 1 就有本质上的区别了。

然后就是通知调用者,再然后就是一个 startCaptureSession() 调用。这个调用非常重要,它建立起了相机与 Target(这里是 Preview 以及 ImageReader) 的通道连接。

  • startCaptureSession()
void startCaptureSession() {
        if (!isCameraOpened() || !mPreview.isReady() || mImageReader == null) {
            return;
        }
        // 根据 Preivew 的大小从 mPreviewSize 中选择一个最佳的。
        Size previewSize = chooseOptimalSize();
       // 设置 Preview Buffer 的大小
        mPreview.setBufferSize(previewSize.getWidth(), previewSize.getHeight());
       // 获取 Preview 的 Surface,将被用来作用相机实际预览的 Surface
        Surface surface = mPreview.getSurface();
        try {
           // 构建一个预览请求
            mPreviewRequestBuilder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
           // 添加 Target ,通道的输出端之一,这里只添加了 preview
            mPreviewRequestBuilder.addTarget(surface);
           // 建立 capture 会话,打通通道。设置输出列表,并且还设置了回调 SessionCallback
            mCamera.createCaptureSession(Arrays.asList(surface, mImageReader.getSurface()),
                    mSessionCallback, null);
        } catch (CameraAccessException e) {
            throw new RuntimeException("Failed to start camera session");
        }
    }

该方法总的来说就是设置 Surface 的 Buffer 大小,创建请求参数,建立会话,打通通道。而关于创建请求参数,这里用了 CameraDevice.TEMPLATE_PREVIEW。其主要支持的参数有TEMPLATE_PREVIEW(预览)、TEMPLATE_RECORD(拍摄视频)、TEMPLATE_STILL_CAPTURE(拍照)等参数。接下来是调用了 createCaptureSession()。

在 createCaptureSession 时设置了输出端列表,还设置了回调 mSessionCallback,它是CameraCaptureSession.StateCallback类型。

细心的读者可能会发现,在这里,mPreivewRequestBuilder 并没有用上,在 createCaptureSeesion 的参数中并没有它。并且你应该还注意到,mPreviewRequestBuilder 通过 addTarget() 添加了输出端,而 createCaptureSeesion 也添加添加了输出列表。它们之间应该存在着某种关系。

先来说 createCaptureSeeson 的输出列表。这个输出列表决定了 CameraDevices 将根据列表的不同 Surface 将创建不同的图像数据,比如这里的 preview surface 以及 ImageReader 的 Surface。而 PreviewRequestBuilder 中的 addTarget() 表示的是针对 CaptureRequest 应该将图像数据输出到哪里去,并且要求这里被添加到 target 的 Surface 必须是 createCaptureSession 的输出列表的其中之一。那针对这段代码来说,被创建的图像数据有 2 种,一种是用于 preview 显示的,一种是用于 ImageReader 的 jpeg。要想在预览中也获取 jpeg 数据,则把 ImageReader 的 surface 添加到 PreviewRequestBuilder 的 target 中去即中。

这里理清了这 2 个列表的关系,接下来看看 createCaptureSeesion 时的第 2 参数 mSessionCallback,它是 CameraCaptureSession.StateCallback 类型的。会话一旦被创建,它的回调方法便会被调用,这里主要关注 onConfigured() 的实现,在这里将关联起 PreviewRequestBuilder 和会话。

       @Override
        public void onConfigured(@NonNull CameraCaptureSession session) {
            if (mCamera == null) {
                return;
            }
            mCaptureSession = session;
            // 设置对焦模式
            updateAutoFocus();
           // 设置闪光灯模式
            updateFlash();
            try {
               // 设定参数,并请求此捕获会话不断重复捕获图像,这样就能连续不断的得到图像帧输出到预览界面
                mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(),
                        mCaptureCallback, null);
            } catch (CameraAccessException e) {
                Log.e(TAG, "Failed to start camera preview because it couldn't access camera", e);
            } catch (IllegalStateException e) {
                Log.e(TAG, "Failed to start camera preview.", e);
            }
        }

会话创建好之后,我们还要告诉会话该怎么用。查看 API 可知,接下来可以进行的是 capture, captureBurst, setRepeatingRequest,或 setRepeatingBurst 的提交。其中 capture 会在后面拍照章节中讲述,***Burst 是用于连拍的。这里所调用的便是 setRepeatingRequest。通过 setRepeatingRequest 请求就将 mPreivewRequestBuilder 提交给了会话,而该提交就是请求此捕获会话不断重复捕获图像,这样就能连续不断的得到图像帧输出到预览界面。

提交 setRepeatingRequest 请求时,还设置了一个参数 mCaptureCallback,它是 PictureCaptureCallback 类型的,而 PictureCaptureCallback 又是继承自 CameraCaptureSession.CaptureCallback。捕获到图像后会同时调用 CaptureCallback 相应的回调方法,然而对于预览模式下在这里并没有什么处理。

关于 updateAutoFocus() 和 updateFlash() 看下面进一步的展开说明。

void updateAutoFocus() {
        if (mAutoFocus) {
            int[] modes = mCameraCharacteristics.get(
                    CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES);
            // Auto focus is not supported
            if (modes == null || modes.length == 0 ||
                    (modes.length == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF)) {
                mAutoFocus = false;
                mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
                        CaptureRequest.CONTROL_AF_MODE_OFF);
            } else {
                mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
                        CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
            }
        } else {
            mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
                    CaptureRequest.CONTROL_AF_MODE_OFF);
        }
    }

这段代码的目的是如果设置了并且支持自动对焦,则 CONTROL_AF_MODE(auto-focus) 就设置为 CONTROL_AF_MODE_CONTINUOUS_PICTURE,否则就为 CONTROL_AF_MODE_OFF。有关 auto-focus 的值的含义概述如下。

value 说明
CONTROL_AF_MODE_AUTO 基本自动对焦模式
CONTROL_AF_MODE_CONTINUOUS_PICTURE 图片模式下的连续对焦
CONTROL_AF_MODE_CONTINUOUS_VIDEO 视频模式下的连续对焦
CONTROL_AF_MODE_EDOF 扩展景深(数字对焦)模式
CONTROL_AF_MODE_MACRO 特写聚焦模式
CONTROL_AF_MODE_OFF 无自动对焦

这个表格中的每个 value 我也并不是每个都熟悉,因此,只作了解即可。

void updateFlash() {
        switch (mFlash) {
            case Constants.FLASH_OFF:
                mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
                        CaptureRequest.CONTROL_AE_MODE_ON);
                mPreviewRequestBuilder.set(CaptureRequest.FLASH_MODE,
                        CaptureRequest.FLASH_MODE_OFF);
                break;
            case Constants.FLASH_ON:
                mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
                        CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH);
                mPreviewRequestBuilder.set(CaptureRequest.FLASH_MODE,
                        CaptureRequest.FLASH_MODE_OFF);
                break;
            case Constants.FLASH_TORCH:
                mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
                        CaptureRequest.CONTROL_AE_MODE_ON);
                mPreviewRequestBuilder.set(CaptureRequest.FLASH_MODE,
                        CaptureRequest.FLASH_MODE_TORCH);
                break;
            case Constants.FLASH_AUTO:
                mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
                        CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
                mPreviewRequestBuilder.set(CaptureRequest.FLASH_MODE,
                        CaptureRequest.FLASH_MODE_OFF);
                break;
            case Constants.FLASH_RED_EYE:
                mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
                        CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE);
                mPreviewRequestBuilder.set(CaptureRequest.FLASH_MODE,
                        CaptureRequest.FLASH_MODE_OFF);
                break;
        }
    }

通过 PreviewRequestBuilder 设定闪光灯的模式,其需要同时设定 CONTROL_AE_MODE 和 FLASH_MODE。

(1) FLASH_MODE,对应是控制闪光灯。

参数 说明
FLASH_MODE_OFF 关闭模式
FLASH_MODE_SINGLE 闪一下模式
FLASH_MODE_TORCH 长亮模式

(2) CONTROL_AE_MODE,对应是曝光,即 auto-exposure。

参数 说明
CONTROL_AE_MODE_ON_AUTO_FLASH 自动曝光
CONTROL_AE_MODE_ON_ALWAYS_FLASH 强制曝光
CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE 不闪光

到这里,基本上就成功打开相机了,然后就能看到相机的画面了。历经磨难,终于打开相机了。而关于相机参数设置,在 Camera 2 中则更加丰富,工程里没有涉及到的这里就不做详细讲解,在实际开发中再去慢慢消化,慢慢理解。

接下来,终于可以进行愉快的拍照了。

4.拍照

分析之前也先来看一看拍照的时序图。梳理了 16 个步骤,但其实拍照的关键步骤就 2 步:通过 CameraDevice 创建一个 TEMPLATE_STILL_CAPTURE 的 CaptureRequest,然后通过 CaptureSession 的 capture 方法提交请求即是拍照的主要步骤。


CameraView 拍照.jpg

CameraView 的 takePicture 就是进一步调用 Camera2 的 takePicture,所以直接从 takePicture() 开始吧。

  • takePicture()
void takePicture() {
        if (mAutoFocus) {
            lockFocus();
        } else {
            captureStillPicture();
        }
    }

CameraView 初始化时默认是自动对焦,因此这里是走入 lockFocus(),时序图也是依据此来绘制的。

  • lockFocus()
private void lockFocus() {
       // 设置当前立刻触发自动对焦
        mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
                CaptureRequest.CONTROL_AF_TRIGGER_START);
        try {
           // 这里是修改了 PictureCaptureCallback 的状态为 STATE_LOCKING
            mCaptureCallback.setState(PictureCaptureCallback.STATE_LOCKING);
          // 向会话提交 capture 请求,以锁定自动对焦
            mCaptureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, null);
        } catch (CameraAccessException e) {
            Log.e(TAG, "Failed to lock focus.", e);
        }
    }

设置了立刻触发自动对焦,修改了 PictureCaptureCallback 状态为 STATE_LOCKING。接下来就是等待 PictureCaptureCallback 的 onCaptureCompleted() 被系统回调。在 onCaptureCompleted() 中进步调用了 process(),而在 process() 中以不同的状态进行不同的处理。这里根据前面的设置处理的是 STATE_LOCKING。

 private void process(@NonNull CaptureResult result) {
            switch (mState) {
                case STATE_LOCKING: {
                    ......
if (af == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED ||
                            af == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) {
                            setState(STATE_CAPTURING);
                            onReady();
                       ......
                    break;
                }
                case STATE_PRECAPTURE: {
                    ......
                    setState(STATE_WAITING);
                    break;
                }
                case STATE_WAITING: {
                    ......
                     setState(STATE_CAPTURING);
                     onReady();
                    break;
                }
            }
        }

为了避免不必要的麻烦,在不影响对代码理解的情况下,这里省略了其他状态的处理。这里假设自动对焦成功了且达到了一个很好的状态下,那么当前的自动对对焦就会进入被锁定的状态,即 CONTROL_AF_STATE_FOCUSED_LOCKED。而自动对焦前面在 updateAutoFocus() 中已经设置为 CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE 了。接下来就会进入真正的抓取图片的处理了。这里先设置了状态为 STATE_CAPTURING,然后调用了自已扩展的 onReady()。onReady() 的实现很简单,就是调用 captureStillPicture()。

  • captureStillPicture()
void captureStillPicture() {
        try {
            //1. 创建一个新的CaptureRequest.Builder,且其参数为 TEMPLATE_STILL_CAPTURE
            CaptureRequest.Builder captureRequestBuilder = mCamera.createCaptureRequest(
                    CameraDevice.TEMPLATE_STILL_CAPTURE);
            //2. 添加它的 target 为 ImageReader 的 Surface
            captureRequestBuilder.addTarget(mImageReader.getSurface());
            //3. 设置自动对焦模式为预览的自动对焦模式
            captureRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
                    mPreviewRequestBuilder.get(CaptureRequest.CONTROL_AF_MODE));
            //4. 设置闪光灯与曝光参数
            switch (mFlash) {
                case Constants.FLASH_OFF:
                    captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
                            CaptureRequest.CONTROL_AE_MODE_ON);
                    captureRequestBuilder.set(CaptureRequest.FLASH_MODE,
                            CaptureRequest.FLASH_MODE_OFF);
                    break;
                case Constants.FLASH_ON:
                    captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
                            CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH);
                    break;
                case Constants.FLASH_TORCH:
                    captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
                            CaptureRequest.CONTROL_AE_MODE_ON);
                    captureRequestBuilder.set(CaptureRequest.FLASH_MODE,
                            CaptureRequest.FLASH_MODE_TORCH);
                    break;
                case Constants.FLASH_AUTO:
                    captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
                            CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
                    break;
                case Constants.FLASH_RED_EYE:
                    captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
                            CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
                    break;
            }
            // 5. 计算 JPEG 的旋转角度
            @SuppressWarnings("ConstantConditions")
            int sensorOrientation = mCameraCharacteristics.get(
                    CameraCharacteristics.SENSOR_ORIENTATION);
            captureRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION,
                    (sensorOrientation +
                            mDisplayOrientation * (mFacing == Constants.FACING_FRONT ? 1 : -1) +
                            360) % 360);
            // 6.停止预览
            mCaptureSession.stopRepeating();
            // 7.抓取当前图片
            mCaptureSession.capture(captureRequestBuilder.build(),
                    new CameraCaptureSession.CaptureCallback() {
                        @Override
                        public void onCaptureCompleted(@NonNull CameraCaptureSession session,
                                @NonNull CaptureRequest request,
                                @NonNull TotalCaptureResult result) {
                            // 8.解锁对自动对焦的锁定
                            unlockFocus();
                        }
                    }, null);
        } catch (CameraAccessException e) {
            Log.e(TAG, "Cannot capture a still picture.", e);
        }
    }

这是拍照的关键实现,代码有点长,但通过增加了带时序的注释,逻辑上看起来也就并不复杂了。这里只强调 3 个点,其他的看一看注释即可,而关于设置闪光灯和曝光这里就省略了。
(1) 这里创建了一个新的 CaptureRequest.Builder ,且其参数为TEMPLATE_STILL_CAPTURE。相应的其 CallBack 也是新的。
(2) 请求的 Target 只有 ImageReader 的 Surface,因此获取到图片后会输出到 ImageReader。最后会在 ImageReader.OnImageAvailableListener 的 onImageAvailable 得到回调。
(3) 拍照前先停止了预览请求,从这里可以看出拍照就是捕获预览模式下自动对焦成功锁定后的图像数据。

接下来就是等待 onCaptureCompleted 被系统回调,然后进一步调用 unlockFocus()。

  • unlockFocus()
void unlockFocus() {
        // 取消了立即自动对焦的触发
        mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
                CaptureRequest.CONTROL_AF_TRIGGER_CANCEL);
        try {
            mCaptureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, null);
            updateAutoFocus();
            updateFlash();
            mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
                    CaptureRequest.CONTROL_AF_TRIGGER_IDLE);
        
    // 重新打开预览
    mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), mCaptureCallback,
                    null);
            mCaptureCallback.setState(PictureCaptureCallback.STATE_PREVIEW);
        } catch (CameraAccessException e) {
            Log.e(TAG, "Failed to restart camera preview.", e);
        }
    }

该方法主要做的事情就是重新打开预览,并且取消了立即自动对焦,同时将其设置为 CONTROL_AF_TRIGGER_IDLE,这将会解除自动对焦的状态,即其状态不再是 CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED。

系统组织好 ImageReader 需要的图像数据后,就会回调其监听 ImageReader.OnImageAvailableListener 的 onImageAvailable()。

  • onImageAvailable()
public void onImageAvailable(ImageReader reader) {
            try (Image image = reader.acquireNextImage()) {
                Image.Plane[] planes = image.getPlanes();
                if (planes.length > 0) {
                    ByteBuffer buffer = planes[0].getBuffer();
                    byte[] data = new byte[buffer.remaining()];
                    buffer.get(data);
                    mCallback.onPictureTaken(data);
                }
            }
        }

从 ImageReader 中获取到 Image,Image 相比 Bitmap 就要复杂的多了,这里简单说明一下。ImageReader 封装了图像的数据平面,而每个平面又封装了 ByteBuffer 来保存原始数据。关于图像的数据平面这个相对于图像的格式来说的,比如 rgb 就只一个平面,而 YUV 一般就有 3 个平面。从 ByteBuffer 中获取的数据都是最原始的数据,对于 rgb 格式的数据,就可以直接将其转换成 Bitmap 然后给 ImageView 显示。

到这里就分析完了拍照的过程了。

5.关闭相机

void stop() {
        if (mCaptureSession != null) {
            mCaptureSession.close();
            mCaptureSession = null;
        }
        if (mCamera != null) {
            mCamera.close();
            mCamera = null;
        }
        if (mImageReader != null) {
            mImageReader.close();
            mImageReader = null;
        }
    }

全场最简单,关闭会话,关闭相机,关闭 ImageReader,Game voer !!!

五、总结

文章对 Android Camera 编程进行了一个较为详细的概括,尤其是对于偏难的 Camera 2 的 API 的理解,结合了官方的 Demo 对 API 及其参数进行了详细的分析,以使得对 API 的理解更加透彻。

另外,如果你的项目需要集成 Camera,又不想自己去封装,同时又觉得官方的 demo 还不够,这里另外推荐一个 github 开源项目 camerakit-android。其也是从官方 demo fork 出来的,自动支持 camera api 1 以及 camera api 2。

最后,感谢你能读到并读完此文章。受限于作者水平有限,如果分析的过程中存在错误或者疑问都欢迎留言讨论。如果我的分享能够帮助到你,也请记得帮忙点个赞吧,鼓励我继续写下去,谢谢。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容