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
-
调用CameraProvider的bindToLifecycle()前记得先调用unbindAll(),否则可能发生重复绑定的exception。
-
ImageAnalyzer的analyze()在分析完图片之后应立即调用ImageProxy的close()释放图像,以便后续图像能继续传送过来。否则将阻塞回调。因而也要注意分析图像的耗时问题。
-
每个ImageProxy实例在关闭后不要存储它的引用,因为一旦调用close(),这些图像将变得不合法。
-
图像分析结束后应当调用ImageAnalysis的clearAnalyzer()以告知不用将图像流传输过来避免性能的浪费。
-
视频录制场景一定不要忘记获得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的话,将插入一行以验证保存路径是否合法并可写。验证结束后会删除该测试行。
分享读者
作者2013年java转到Android开发,在小厂待过,也去过华为,OPPO等大厂待过,18年四月份进了阿里一直到现在。
被人面试过,也面试过很多人。深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长,而且极易碰到天花板技术停滞不前!
我们整理了一份阿里P7级别的Android架构师全套学习资料,特别适合有3-5年以上经验的小伙伴深入学习提升。
主要包括腾讯,以及字节跳动,阿里,华为,小米,等一线互联网公司主流架构技术。
如果你觉得自己学习效率低,缺乏正确的指导,可以一起学习交流!
我们致力打造一个平等,高质量的Android交流圈子,不一定能短期就让每个人的技术突飞猛进,但从长远来说,眼光,格局,长远发展的方向才是最重要的。
35岁中年危机大多是因为被短期的利益牵着走,过早压榨掉了价值,如果能一开始就树立一个正确的长远的职业规划。35岁后的你只会比周围的人更值钱。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!
ediaStore is skipped on Huawei devices.
大意是拍摄保存的Uri为MediaStore的话,将插入一行以验证保存路径是否合法并可写。验证结束后会删除该测试行。
分享读者
作者2013年java转到Android开发,在小厂待过,也去过华为,OPPO等大厂待过,18年四月份进了阿里一直到现在。
被人面试过,也面试过很多人。深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长,而且极易碰到天花板技术停滞不前!
我们整理了一份阿里P7级别的Android架构师全套学习资料,特别适合有3-5年以上经验的小伙伴深入学习提升。
主要包括腾讯,以及字节跳动,阿里,华为,小米,等一线互联网公司主流架构技术。
[外链图片转存中…(img-mVsdTUJM-1714733390870)]
如果你觉得自己学习效率低,缺乏正确的指导,可以一起学习交流!
我们致力打造一个平等,高质量的Android交流圈子,不一定能短期就让每个人的技术突飞猛进,但从长远来说,眼光,格局,长远发展的方向才是最重要的。
35岁中年危机大多是因为被短期的利益牵着走,过早压榨掉了价值,如果能一开始就树立一个正确的长远的职业规划。35岁后的你只会比周围的人更值钱。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!