加载中......
输入验证码,即可复制
微信扫码下载好向圈APP, 登陆后即可进入消息页面查看验证码
只需要3秒时间
你可能没想到 CameraX也是 Jetpack 的一部分。


我们的生活已经越来越离不开相机,从自拍到直播,扫码再到VR等等。相机的优劣自然就成为了厂商竞相追逐的赛场。对于app开发者来说,如何快速驱动相机,提供优秀的拍摄体验,优化相机的使用功耗,是一直以来追求的目标。

前言

Android 5.0 时期Camera接口便已弃用,所以一般的做法是使用其替代者Camera2接口。但随着CameraX的出现,这个选择变得不再唯一。

我们先来回顾下图像预览这一简单的需求,使用Camera2接口是如何实现的。

Camera2

抛开回调,异常等附加处理,依旧需要多个步骤才能实现,比较繁琐。※篇幅原因省略代码只阐述步骤※


Android开发又一新挑战!CameraX 即将一统江湖?-1.jpg

同样是图像预览采用CameraX的话,实现就非常简洁。

CameraX

图像预览
可以说十几行就可以完成。和Camera2一样需要展示预览的控件PreviewView到布局上,并确保获得了camera权限。差异的地方主要体现在相机的配置步骤上。

private void setupCamera(PreviewView previewView) {    ListenableFuture<ProcessCameraProvider> cameraProviderFuture =            ProcessCameraProvider.getInstance(this);    cameraProviderFuture.addListener(() -> {        try {            mCameraProvider = cameraProviderFuture.get();            bindPreview(mCameraProvider, previewView);        } catch (ExecutionException | InterruptedException e) {            e.printStackTrace();        }    }, ContextCompat.getMainExecutor(this));}private void bindPreview(@NonNull ProcessCameraProvider cameraProvider,                         PreviewView previewView) {    mPreview = new Preview.Builder().build();    mCamera = cameraProvider.bindToLifecycle(this,            CameraSelector.DEFAULT_BACK_CAMERA, mPreview);    mPreview.setSurfaceProvider(previewView.getSurfaceProvider());}
Android开发又一新挑战!CameraX 即将一统江湖?-2.jpg

镜头切换
如果想要切换镜头,只要将目标镜头的CameraSelector示例绑定到CameraProvider即可。我们在画面上添加按钮以切换镜头。

public void onChangeGo(View view) {    if (mCameraProvider != null) {        isBack = !isBack;        bindPreview(mCameraProvider, binding.previewView);    }}private void bindPreview(@NonNull ProcessCameraProvider cameraProvider,                         PreviewView previewView) {    ...    CameraSelector cameraSelector = isBack ? CameraSelector.DEFAULT_BACK_CAMERA            : CameraSelector.DEFAULT_FRONT_CAMERA;    // 绑定前确保解除了所有绑定,防止CameraProvider重复绑定到Lifecycle发生异常    cameraProvider.unbindAll();     mCamera = cameraProvider.bindToLifecycle(this, cameraSelector, mPreview);    ...}
Android开发又一新挑战!CameraX 即将一统江湖?-3.jpg

镜头聚焦
无法聚焦的拍摄是不完整的,我们监听Preview的触摸事件将触摸坐标告知CameraX开始聚焦。

protected void onCreate(@Nullable Bundle savedInstanceState) {    ...    binding.previewView.setOnTouchListener((v, event) -> {        FocusMeteringAction action = new FocusMeteringAction.Builder(                binding.previewView.getMeteringPointFactory()                        .createPoint(event.getX(), event.getY())).build();        try {            showTapView((int) event.getX(), (int) event.getY());            mCamera.getCameraControl().startFocusAndMetering(action);        }...    });}private void showTapView(int x, int y) {    PopupWindow popupWindow = new PopupWindow(ViewGroup.LayoutParams.WRAP_CONTENT,            ViewGroup.LayoutParams.WRAP_CONTENT);    ImageView imageView = new ImageView(this);    imageView.setImageResource(R.drawable.ic_focus_view);    popupWindow.setContentView(imageView);    popupWindow.showAsDropDown(binding.previewView, x, y);    binding.previewView.postDelayed(popupWindow::dismiss, 600);    binding.previewView.playSoundEffect(SoundEffectConstants.CLICK);}
Android开发又一新挑战!CameraX 即将一统江湖?-4.jpg

除了图像预览以外还有很多其他使用场景,比如图像拍摄,图像分析和视频录制。CameraX将这些使用场景统一抽象为UseCase

它有四个子类,分别为PreviewImageCaptureImageAnalysisVideoCapture。接下来介绍下它们如何使用。

图像拍摄
借助ImageCapture提供的takePicture()可以将图像拍摄下来。支持保存到外部存储空间,当然需要获得external storage的读写权限。

private void takenPictureInternal(boolean isExternal) {    final ContentValues contentValues = new ContentValues();    contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, CAPTURED_FILE_NAME            + "_" + picCount++);    contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg");    ImageCapture.OutputFileOptions outputFileOptions =             new ImageCapture.OutputFileOptions.Builder(                    getContentResolver(),                    MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)            .build();    if (mImageCapture != null) {        mImageCapture.takePicture(outputFileOptions, CameraXExecutors.mainThreadExecutor(),                new ImageCapture.OnImageSavedCallback() {                    @Override                    public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {                        Toast.makeText(DemoActivityLite.this, "Picture got"                                + (outputFileResults.getSavedUri() != null                                ? " @ " + outputFileResults.getSavedUri().getPath()                                : "") + ".", Toast.LENGTH_SHORT)                                .show();                    }                    ...                });    }}private void bindPreview(@NonNull ProcessCameraProvider cameraProvider,                         PreviewView previewView) {    ...    mImageCapture =  new ImageCapture.Builder()            .setTargetRotation(previewView.getDisplay().getRotation())            .build();    ...    // 需要将ImageCapture场景一并绑定    mCamera = cameraProvider.bindToLifecycle(this, cameraSelector, mPreview, mImageCapture);    ...}
Android开发又一新挑战!CameraX 即将一统江湖?-5.jpg

图像分析
图像分析指的是对预览的图像实时分析,将色彩,内容等信息识别出来,应用在机器学习,二维码识别等业务场景。

继续对demo做些改造,添加扫描二维码的按钮。点击按钮后进入扫码模式,并在二维码解析成功后弹出解析结果。

public void onAnalyzeGo(View view) {    if (!isAnalyzing) {        mImageAnalysis.setAnalyzer(CameraXExecutors.mainThreadExecutor(), image -> {           analyzeQRCode(image);        });    }    ...}// 从ImageProxy取出图像数据,交由二维码框架zxing解析private void analyzeQRCode(@NonNull ImageProxy imageProxy) {    ByteBuffer byteBuffer = imageProxy.getPlanes()[0].getBuffer();    byte[] data = new byte[byteBuffer.remaining()];    byteBuffer.get(data);    ...    BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));    Result result;    try {        result = multiFormatReader.decode(bitmap);    }    ...    showQRCodeResult(result);    imageProxy.close();}private void showQRCodeResult(@Nullable Result result) {    if (binding != null && binding.qrCodeResult != null) {        binding.qrCodeResult.post(() ->                binding.qrCodeResult.setText(result != null ? "Link:\n" + result.getText() : ""));        binding.qrCodeResult.playSoundEffect(SoundEffectConstants.CLICK);    }}
Android开发又一新挑战!CameraX 即将一统江湖?-6.jpg

视频录制
依托VideoCapturestartRecording()可以进行视频录制。

在demo上添加一个图像拍摄和视频录制模式的切换按钮,切换到视频录制模式的时候将视频拍摄的UseCase綁定到CameraProvider

public void onVideoGo(View view) {    bindPreview(mCameraProvider, binding.previewView, isVideoMode);}private void bindPreview(@NonNull ProcessCameraProvider cameraProvider,                         PreviewView previewView, boolean isVideo) {    ...    mVideoCapture = new VideoCapture.Builder()            .setTargetRotation(previewView.getDisplay().getRotation())            .setVideoFrameRate(25)            .setBitRate(3 * 1024 * 1024)            .build();    cameraProvider.unbindAll();    if (isVideo) {        mCamera = cameraProvider.bindToLifecycle(this, cameraSelector,                mPreview, mVideoCapture);    } else {        mCamera = cameraProvider.bindToLifecycle(this, cameraSelector,                mPreview, mImageCapture, mImageAnalysis);    }    mPreview.setSurfaceProvider(previewView.getSurfaceProvider());}
点击录制按钮后首先确保获得外部存储和audio权限,之后再开始视频的录制。

public void onCaptureGo(View view) {    if (isVideoMode) {        if (!isRecording) {            // Check permission first.            ensureAudioStoragePermission(REQUEST_STORAGE_VIDEO);        }    }    ...}private void ensureAudioStoragePermission(int requestId) {    ...    if (requestId == REQUEST_STORAGE_VIDEO) {        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)                != PackageManager.PERMISSION_GRANTED                || ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)                != PackageManager.PERMISSION_GRANTED) {            ActivityCompat.requestPermissions(...);            return;        }        recordVideo();    }}private void recordVideo() {   try {        mVideoCapture.startRecording(                new VideoCapture.OutputFileOptions.Builder(getContentResolver(),                        MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentValues)                        .build(),                CameraXExecutors.mainThreadExecutor(),                new VideoCapture.OnVideoSavedCallback() {                    @Override                    public void onVideoSaved(@NonNull VideoCapture.OutputFileResults outputFileResults) {                        // Notify user...                    }                }        );    }     ...    toggleRecordingStatus();}private void toggleRecordingStatus() {    // Stop recording when toggle to false.    if (!isRecording && mVideoCapture != null) {        mVideoCapture.stopRecording();    }}
Android开发又一新挑战!CameraX 即将一统江湖?-7.jpg


  • 小插曲
实现视频录制功能的时候发现一个问题。

点击视频录制按钮的时候,如果此刻尚未获得audio权限,那么将申请该权限。即便此后获得了权限调用拍摄接口仍将发生异常。日志显示AudioRecorder实例为null引发了NPE。

仔细查看相关逻辑发现,demo现在的处理是在切换为视频录制模式的时候,就将VideoCapture绑定到了CameraProvider。这个时间点如果还未获得audio权限的话,那么将无法初始化AudioRecorder

其实日志里也会给出相应提示:AudioRecord object cannot initialized correctly。可是后面获得了权限再去调用VideoCapture的拍摄接口为何还是会发生NPE?

因为拍摄接口startRecording()的内部处理是AudioRecorder实例为null的话将直接终止请求。后面无论调用多少遍也无济于事。事实上该函数的后段存在再次获取AudioRecorder实例的逻辑,但因为前面发生了NPE而没有机会执行。

// VideoCapture.javapublic void startRecording(        @NonNull OutputFileOptions outputFileOptions, @NonNull Executor executor,        @NonNull OnVideoSavedCallback callback) {    ...    try {        // mAudioRecorder为null将引发NPE终止录制的请求        mAudioRecorder.startRecording();    } catch (IllegalStateException e) {        postListener.onError(ERROR_ENCODER, "AudioRecorder start fail", e);        return;    }    ...    mRecordingFuture.addListener(() -> {        ...        if (getCamera() != null) {            // 前面发生了NPE,那么将失去此处再次获得AudioRecorder实例的机会            setupEncoder(getCameraId(), getAttachedSurfaceResolution());            notifyReset();        }    }, CameraXExecutors.mainThreadExecutor());    ...}
不知道这是VideoCapture实现上的漏洞还是开发者有意为之。但是在明明已经获得了audio权限的情况下调用录制接口却依旧发生NPE貌似并不合理。

当下只能采取一些回避方案,或者说开发者本该就这么做?

现在是在获得了audio权限前执行了VideoCapture的绑定,这存在发生上述反复NPE的可能。所以改成获得audio权限后再绑定VideoCapture即可回避。

话说回来,在VideoCaptue的文档里加上需要获得audio的权限的说明是不是更好一些呢?

相机效果扩展
光有上述几个场景的使用并不能满足日益丰富的拍摄需求,人像,夜拍,美颜等相机效果是必不可少的。幸好CameraX是支持效果扩展的。但不是所有设备都能兼容这种扩展,具体可在官网的设备兼容列表里查询到。

可供扩展的效果主要分为两大类。

一个是用于图像预览时效果扩展的PreviewExtender,另一个是用于图像拍摄时效果扩展的ImageCaptureExtender

每个大类都包含几个典型的效果。

NightPreviewExtender 夜拍预览

BokehPreviewExtender 人像预览

BeautyPreviewExtender 美顔预览

HdrPreviewExtender HDR预览

AutoPreviewExtender 自动预览

开启这些效果的实现也非常简单。

private void bindPreview(@NonNull ProcessCameraProvider cameraProvider,                         PreviewView previewView, boolean isVideo) {    Preview.Builder previewBuilder = new Preview.Builder();    ImageCapture.Builder captureBuilder = new ImageCapture.Builder()            .setTargetRotation(previewView.getDisplay().getRotation());    ...    setPreviewExtender(previewBuilder, cameraSelector);    mPreview = previewBuilder.build();    setCaptureExtender(captureBuilder, cameraSelector);    mImageCapture =  captureBuilder.build();    ...}private void setPreviewExtender(Preview.Builder builder, CameraSelector cameraSelector) {    BeautyPreviewExtender beautyPreviewExtender = BeautyPreviewExtender.create(builder);    if (beautyPreviewExtender.isExtensionAvailable(cameraSelector)) {        // Enable the extension if available.        beautyPreviewExtender.enableExtension(cameraSelector);    }}private void setCaptureExtender(ImageCapture.Builder builder, CameraSelector cameraSelector) {    NightImageCaptureExtender nightImageCaptureExtender = NightImageCaptureExtender.create(builder);    if (nightImageCaptureExtender.isExtensionAvailable(cameraSelector)) {        // Enable the extension if available.        nightImageCaptureExtender.enableExtension(cameraSelector);    }}
遗憾的是笔者手中的Redmi 6A不在支持OEM效果扩展的设备列表里,无法给大家展示成功扩展效果的样图。

高阶用法
除了上述常见相机使用场景外还有其他可选的配置方法。篇幅限制不再详细展开,感兴趣者可参考官网进行尝试。

转换输出 CameraX支持将图像数据进行转换后输出,比如应用于人像识别后绘制人脸框图

https://developer.android.google.cn/training/camerax/transform-output?hl=zh-cn

用例旋转 图像拍摄和分析的过程中屏幕可能发生旋转,学习如何配置使得CameraX能够实时获取到屏幕方向和旋转角度,以抓取到正确的图像
https://developer.android.google.cn/training/camerax/orientation-rotation?hl=zh-cn

配置选项 控制分辨率,自动对焦,取景框形状设置等配置的指导
https://developer.android.google.cn/training/camerax/configuration?hl=zh-cn

使用注意

  • 调用CameraProviderbindToLifecycle()前记得先调用unbindAll(),否则可能发生重复绑定的exception
  • ImageAnalyzeranalyze()在分析完图片之后应立即调用ImageProxyclose()释放图像,以便后续图像能继续传送过来。否则将阻塞回调。因而也要注意分析图像的耗时问题。
  • 每个ImageProxy实例在关闭后不要存储它的引用,因为一旦调用close(),这些图像将变得不合法。
  • 图像分析结束后应当调用ImageAnalysisclearAnalyzer()以告知不用将图像流传输过来避免性能的浪费。
  • 视频录制场景一定不要忘记获得audio权限。
有趣的兼容性处理

实现图像拍摄功能的时候发现ImageCapturetakePicture()文档里写着这么一段有趣的注释。

Before triggering the image capture pipeline, if the save location is a File or MediaStore, it is first verified to ensure it’s valid and writable.

A File is verified by attempting to open a FileOutputStream to it, whereas a location in MediaStore is validated by ContentResolver#insert() creating a new row in the user defined table,



retrieving a Uri pointing to it, then attempting to open an OutputStream to it.
The newly created row is ContentResolver#delete() deleted at the end of the verification.

On Huawei devices, this deletion results in the system displaying a notification informing the user that a photo has been deleted.



In order to avoid this, validating the image capture save location in MediaStore is skipped on Huawei devices.



大意是拍摄保存的Uri为MediaStore的话,将插入一行以验证保存路径是否合法并可写。验证结束后会删除该测试行。

但是在Huawei设备上删除行记录的操作将触发一条删除照片的通知。所以为避免困扰用户,CameraX将会在Huawei设备上跳过路径的验证。

class ImageSaveLocationValidator {    // 将判断设备品牌是否为华为或荣耀,是则直接跳过验证    static boolean isValid(final @NonNull ImageCapture.OutputFileOptions outputFileOptions) {        ...        if (isSaveToMediaStore(outputFileOptions)) {            // Skip verification on Huawei devices            final HuaweiMediaStoreLocationValidationQuirk huaweiQuirk =                    DeviceQuirks.get(HuaweiMediaStoreLocationValidationQuirk.class);            if (huaweiQuirk != null) {                return huaweiQuirk.canSaveToMediaStore();            }            return canSaveToMediaStore(outputFileOptions.getContentResolver(),                    outputFileOptions.getSaveCollection(), outputFileOptions.getContentValues());        }        return true;    }    ...}public class HuaweiMediaStoreLocationValidationQuirk implements Quirk {    static boolean load() {        return "HUAWEI".equals(Build.BRAND.toUpperCase())                || "HONOR".equals(Build.BRAND.toUpperCase());    }    /**     * Always skip checking if the image capture save destination in     * {@link android.provider.MediaStore} is valid.     */    public boolean canSaveToMediaStore() {        return true;    }}CameraX的优势

源于CameraX在Camera2的基础上进行了高度的封装和对大量设备进行了兼容性的处理,使得CameraX拥有了很多优势。


  • 易用性 采用封装的API可以高效达到目标。
  • 设备一致性 不用在乎版本,忽略硬件差异,达到一致的开发体验。
  • 新的相机体验 通过效果扩展可以实现和原生相机一样的美颜等拍摄功能。
结语

CameraX发布于2019年8月7日,从alpha版到现在的beta版,一直在更新。

从上面有趣的Huawei设备兼容性处理可以看到CameraX一统江湖的决心。

最新仍是beta版,需要继续改进,但并非不能投入生产环境。

这么好用的框架,大家要多多使用并给出建议,这样才能越来越完善,才能给开发者给用户带来福音。
移动开发
20615 查看 0 0 反对

说说我的看法高级模式

您需要登录后才可以回帖 登录|立即注册

还没人评论此主题哦

相关阅读