本文多是当下最新最全的
CameraX
解读,篇幅较长,慢慢享用。java
咱们的生活已经愈来愈离不开相机,从自拍
到直播
,扫码
再到VR
等等。相机的优劣天然就成为了厂商竞相追逐的赛场。对于app开发者来讲,如何快速驱动相机,提供优秀的拍摄体验,优化相机的使用功耗,是一直以来追求的目标。android
Android 5.0 时期Camera
接口便已弃用,因此通常的作法是使用其替代者Camera2
接口。但随着CameraX
的出现,这个选择变得再也不惟一。git
咱们先来回顾下图像预览这一简单的需求,使用Camera2
接口是如何实现的。github
抛开回调,异常等附加处理,仍然须要多个步骤才能实现,比较繁琐。※篇幅缘由省略代码只归纳步骤※web
graph TD 布局里展现TextureView控件 --> 事先启动HandlerThread并取得其Handler实例供相机回调 --> 确保app拥有camera权限 --> 监听TextureView的可用的时机并配置相机 --> 获取并保存目标镜头的ID和参数 --> 将尺寸/缩放比率/屏幕方向等参数反映给TextureView --> 经过CameraManager启动相机 --> 在相机启动成功的回调里将其CameraDevice和Surface创建链接
一样是图像预览采用CameraX
的话,实现就很是简洁。markdown
能够说十几行就能够完成。和Camera2
同样须要展现预览的控件PreviewView
到布局上,并确保得到了camera
权限。差别的地方主要体如今相机的配置步骤上。app
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());
}
复制代码
若是想要切换镜头,只要将目标镜头的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);
...
}
复制代码
没法聚焦的拍摄是不完整的,咱们监听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);
}
复制代码
除了图像预览之外还有不少其余使用场景,好比图像拍摄,图像分析和视频录制。
CameraX
将这些使用场景统一抽象为UseCase
,它有四个子类,分别为Preview
,ImageCapture
,ImageAnalysis
和VideoCapture
。接下来介绍下它们如何使用。ide
借助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
。其实日志里也会给出相应提示:VideoCapture: 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
。
每一个大类都包含几个典型的效果。
开启这些效果的实现也很是简单。
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
支持将图像数据进行转换后输出,好比应用于人像识别
后绘制人脸框图developer.android.google.cn/training/ca…
CameraX
可以实时获取到屏幕方向和旋转角度,以抓取到正确的图像developer.android.google.cn/training/ca…
developer.android.google.cn/training/ca…
调用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
的话,将插入一行以验证保存路径是否合法并可写。验证结束后会删除该测试行。
可是在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
在Camera2
的基础上进行了高度的封裝和对大量设备进行了兼容性的处理,使得CameraX
拥有了不少优点。
demo的源码已经开源至Github
,你们能够查阅参考。
CameraX
发布于2019年8月7日,从alpha版到如今的beta版,一直在更新。从上面有趣的Huawei设备兼容性处理能够看到CameraX
一统江湖的决心。
最新还是beta版,须要继续改进,但并不是不能投入生产环境。
这么好用的框架,你们要多多使用并给出建议,这样才能愈来愈完善,才能给开发者给用户带来福音。
CameraX
使用指南:developer.android.google.cn/training/ca…CameraX
的历史版本:developer.android.google.cn/jetpack/and…CameraX
的兼容和效果扩展支持的设备:developer.android.google.cn/training/ca…CameraX
的官方示例:github.com/android/cam…