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

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);

}

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

它有四个子类,分别为Preview,ImageCapture,ImageAnalysis和VideoCapture。接下来介绍下它们如何使用。

图像拍摄


借助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);

}

图像分析


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

继续对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);

}

}

视频录制


依托VideoCapture的startRecording()可以进行视频录制。

在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();

}

}

小插曲

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

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

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

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

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

// VideoCapture.java

public 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

使用注意


  1. 调用CameraProvider的bindToLifecycle()前记得先调用unbindAll(),否则可能发生重复绑定的exception。

  2. ImageAnalyzer的analyze()在分析完图片之后应立即调用ImageProxy的close()释放图像,以便后续图像能继续传送过来。否则将阻塞回调。因而也要注意分析图像的耗时问题。

  3. 每个ImageProxy实例在关闭后不要存储它的引用,因为一旦调用close(),这些图像将变得不合法。

  4. 图像分析结束后应当调用ImageAnalysis的clearAnalyzer()以告知不用将图像流传输过来避免性能的浪费。

  5. 视频录制场景一定不要忘记获得audio权限。

有趣的兼容性处理

=================================================================

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

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的话,将插入一行以验证保存路径是否合法并可写。验证结束后会删除该测试行。

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助

因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
验证保存路径是否合法并可写。验证结束后会删除该测试行。

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助

因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-YirShEm1-1715413745666)]

[外链图片转存中…(img-KFeWVrIP-1715413745667)]

[外链图片转存中…(img-HlQ7Kk7R-1715413745668)]

[外链图片转存中…(img-HDP7Rnw3-1715413745669)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

  • 25
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值