稀有猿诉

十年磨一剑,历炼出锋芒,说话千百句,不如码二行。

Camera2 API Made Easy

从Android 5.0 (API 21)开始谷歌废弃了Camera,并提供了一套新的API,称之为Camera 2,不再是大而全的一个类了,也使用了更多的回调以异步化,流程与参数的控制更加的灵活,但也变得更加的复杂了,今天就来学习一下这套新API的使用方法。

基本使用大法

新的这一套API使用起来略复杂,涉及的较多的对象,可以按照如下步骤来使用。

Step 1: 申请相机使用权限

自从Android M(6.0)以后,对权限的限制比以前严格多了,必须要在运行时动态的去申请权限,征得用户同意后,方可正常使用。

需要在AndroidManifest中声明相机权限:

1
<uses-permission android:name="android.permission.CAMERA" />

注意:官方还建议在manifest中声明use camera feature,但用处不大,这主要是给Google Play用的,Google Play可以根据这些方便精准投递你的app,如设备无camera硬件,或者硬件配置不支持,就不会安装,但就国内的市场来说基本用不到。

但这还不够,还需要在运行时动态检查,如果未授权,则要去申请,如仍被用户拒绝,则可直接退出,因为相机应用嘛,没有相机权限啥也做不了。一般来说,这个过程放在onCreate中做就可以了:

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
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_camera);

    if (ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.CAMERA) ==
            PackageManager.PERMISSION_GRANTED) {
        setupCamera();
    } else {
        requestPermissions(new String[]{Manifest.permission.CAMERA}, REQ_CODE_PERM);
    }
}

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    if (requestCode == REQ_CODE_PERM) {
        if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            setupCamera();
        } else {
            Toast.makeText(getApplicationContext(),
                    "To use this app you must grant CAMERA permission.",
                    Toast.LENGTH_LONG).show();
            finish();
        }
    }
}

注意:不建议放在onResume中去做,因为可能会造成死循环,比如权限申请,虽然看起来像一个对话框,但其实是一个Activity,也就是说从权限授权那个弹窗页面回来,是会走onResume的,如果用户同意授予了还好,直接就往下走流程了,假如用户拒绝了权限,检查发现权限还未授予,会继续去申请,这就会死循环了。

权限申请是必须要做,且要处理好,在应用的入口做好处理,保证后面的流程权限是有的,这样可以为业务逻辑减轻复担,里面就可以不用管权限了。基于这样的假设:要么权限已授予,要么就不会走进来(无权限时应该直接退出)。

Step 2: 获取相机的配置情况

处理好了权限以后,就要真正开始搞了,无论你的use case是什么,第一步肯定 是获取相机的配置信息情况,也就是说要搞清楚有几个硬件camera可以用,它的能力是什么(输出能满足什么样的需求),它的特点是什么(前置还是后置)。这里最主要用到的是就是CameraManager对象,它是一个注册在ServiceManager里面的独立service,所以可以通过Context来获取:

1
cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);

然后就是获取当前有多少可用的摄像头:

1
String[] ids = cameraManager.getCameraIdList();

这个列表就是当前可用的camera,用一个String形式的id来标识,查询具体的摄像头的能力和配置都需要传入对应的id,比如说简单的dump一下信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void enumerateCameras(Consumer<String> consumer) {
   final List<String> cameraIds = cameraManager.getCameraIdList();
   List<String> status = cameraIds.stream()
                .map(cameraManager::getCameraCharacteristics)
                .map(CameraContext::dumpCameraInfo)
                .collect(Collectors.toList());
}

private static String dumpCameraInfo(CameraCharacteristics characteristics) {
    StringBuilder sb = new StringBuilder();
    sb.append("{Physical Id: ");
    sb.append(characteristics.getPhysicalCameraIds());
    sb.append(", Facing: ");
    int facing = characteristics.get(CameraCharacteristics.LENS_FACING);
    sb.append(facing == CameraMetadata.LENS_FACING_FRONT ? "FRONT" : "BACK");
    sb.append("}");
    return sb.toString();
}

因为大多数场景都是默认使用后摄,但要先把找出来:

1
2
3
4
5
6
7
8
9
   final List<String> cameraIds = cameraManager.getCameraIdList();
   cameraIds.stream()
            .forEach(id -> {
                        CameraCharacteristics cc = cameraManager.getCameraCharacteristics(id);
                        if (cc.get(CameraCharacteristics.LENS_FACING) == CameraMetadata.LENS_FACING_BACK) {
                            currentCamera = new CameraAgent(id, cc, cameraThreadHandler);
                            return;
                        }
            });

因为官方的CameraManager封装略差一些,以及它的每一个方法都要检查权限异常,所以做一下进一步的简单封装:

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
class CameraManagerWrapper {
    private final CameraManager cameraManager;

    public CameraManagerWrapper(Context context) {
        cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
    }

    public List<String> getCameraIdList() {
        try {
            return Arrays.asList(cameraManager.getCameraIdList());
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
        return new ArrayList<>();
    }

    public CameraCharacteristics getCameraCharacteristics(String id) {
        try {
            return cameraManager.getCameraCharacteristics(id);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
        return null;
    }
}

Step 3:打开相机

找到了摄像头,下一步就可以尝试打开并使用了。通过CameraManager#openCamera)来获取CameraDevice对象,这里的CameraDevice就是一个摄像头的封装,后面所有的操作都要在它上面进行。还需要提供一个CameraDevice.StateCallback,以及一个Handler,它是用于StateCallback运行的线程,也就是说StateCallback的几个方法都是在Handler所在的线程中运行的。

因为打开摄像头是需要去操作硬件的,所以可能会耗时,因此,在这里引入了异步的回调,用以通知打开的结果CameraManager#openCamera是没有任何返回值的,结果 是通过StateCallback来通知给应用程序,且是运行在指定的Handler中的,如果不传Handler,那么StateCallback就会运行在主线程中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 private CameraDevice.StateCallback stateCallback = new CameraDevice.StateCallback() {
    @Override
    public void onOpened(@NonNull CameraDevice camera) {
        Log.d(LOG_TAG, "Device Status: onOpened");
        cameraDevice = Optional.of(camera);
    }

    @Override
    public void onDisconnected(@NonNull CameraDevice camera) {
        Log.d(LOG_TAG, "Device Status: onDisconnected");
        cameraDevice = Optional.empty();
        errorCode = 0;
    }

    @Override
    public void onError(@NonNull CameraDevice camera, int error) {
        cameraDevice = Optional.empty();
        errorCode = error;
    }
};

cameraManager.openCamera(backId, stateCallback, handler);

打开操作仅是一个开端,本身也没有特别复杂的,就是需要在StateCallback中做好处理就可以了。

Step 4: 创建拍照会话

下一步就是要创建拍照会话,对于每一个CameraDevice,打开了以后,要想使用必须创建一个CameraCaptureSession,所有的use case都需要通过有效的CameraCaptureSeesion对象来实现(如预览和拍照),CameraDevice对象本身提供的功能并不多。与打开类似,也需要提供一个CameraCaptureSession.StateCallback以接收创建session的结果,同时也要提供一个StateCallback使用的线程Handler:

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
private CameraCaptureSession.StateCallback regularSessionCallback = new CameraCaptureSession.StateCallback() {
    @Override
    public void onConfigured(@NonNull CameraCaptureSession session) {
        Log.d(LOG_TAG, "Session State: onConfigured");
        captureSession = Optional.of(session);
    }

    @Override
    public void onConfigureFailed(@NonNull CameraCaptureSession session) {
        captureSession = Optional.empty();
    }

    @Override
    public void onClosed(@NonNull CameraCaptureSession session) {
        Log.d(LOG_TAG, "Session State: onClosed");
        super.onClosed(session);
        captureSession = Optional.empty();
    }
};

private void createCaptureSession(Surface surface) {
    SessionConfiguration sessionConfig = new SessionConfiguration(
            SessionConfiguration.SESSION_REGULAR,
            Arrays.asList(new OutputConfiguration(surface)),
            executor,
            regularSessionCallback);
    try {
        cameraDevice.get().createCaptureSession(sessionConfig);
    } catch (CameraAccessException ignored) {
    }
}

需要注意,同时需要提供一个camera的输出目标,比如预览的Surface。另外就是,创建CaptureSession是有2个API的,以前常用的是createCaptureSession(List outputs, CameraCaptureSession.StateCallback callback, Handler handler)),但这个方法已被标记为Deprecated的了,推荐使用createCaptureSession(SessionConfiguration config))。这两者基本没区别,所需要传入的参数 都差不多,唯一不同的就是供回调所使用的线程,旧的API是用Handler,新的这个要求是Executor,这里有些不太一致,但容易解决,通过Handler可以很容易的创建出来一个Executor:

1
executor = command -> handler.post(command);

Step 5:请求预览

有了CameraCaptureSession对象以后,就可以启动预览了,这个是相机类应用的最为基础的一个use case。

主要是通过setRepeatingRequest)这个方法,需要提供一个CaptureRequest,一个CaptureCallback以及一个供回调使用的Handler,其实,这一步通常与Step 4融合在一起:

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
36
    // 省略的代码
    @Override
    public void onConfigured(@NonNull CameraCaptureSession session) {
        Log.d(LOG_TAG, "Session State: onConfigured");
        captureSession = Optional.of(session);
        // In session state callback
        try {
            captureSession.get().setRepeatingRequest(requestBuilder.build(), captureCallback, cameraHandler);
        } catch (CameraAccessException ignored) {
        }
    }
   // 省略的代码

private CameraCaptureSession.CaptureCallback captureCallback = new CameraCaptureSession.CaptureCallback() {
    @Override
    public void onCaptureStarted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, long timestamp, long frameNumber) {
        super.onCaptureStarted(session, request, timestamp, frameNumber);
    }
};

private void doStartPreview(Surface surface) {
    SessionConfiguration sessionConfig = new SessionConfiguration(
            SessionConfiguration.SESSION_REGULAR,
            Arrays.asList(new OutputConfiguration(surface)),
            executor,
            regularSessionCallback);
    try {
        requestBuilder = cameraDevice.get().createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
        requestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureResult.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
        requestBuilder.addTarget(surface);
        sessionConfig.setSessionParameters(requestBuilder.build());

        cameraDevice.get().createCaptureSession(sessionConfig);
    } catch (CameraAccessException ignored) {
    }
}

总结

前面说了这么多,其实关键的就是三个对象和三个回调,三个对象分别是

  • CameraManager – 用以获取静态的配置信息
  • CameraDevice – 用以创建拍照会话
  • CameraCaptureSession – 用以启动预览和进行拍照

三大回调分别是:

  • CameraDevice.StateCallback – 用于获取CameraDevice
  • CameraCaptureSession.StateCallback – 用于获取拍照会话
  • CameraCaptureSession.CaptureCallback – 用于获取预览和拍照的结果

再有就是,提供回调的时候要提供一个回调发生的线程,通过Handler或者Executor来指定。因为硬件操作可能会耗时甚至完全卡死,所以回调和线程是用来实现异步的。

参考资料

Comments