graph TD
布局里展示TextureView控件 --> 事先启动HandlerThread并取得其Handler实例供相机回调 --> 确保app拥有camera权限 --> 监听TextureView的可用的时机并配置相机 --> 获取并保存目标镜头的ID和参数 --> 将尺寸/缩放比率/屏幕方向等参数反映给TextureView --> 通过CameraManager启动相机 --> 在相机启动成功的回调里将其CameraDevice和Surface建立连接
同样是图像预览采用CameraX
的话,实现就非常简洁。
CameraX
图像预览
可以说十几行就可以完成。和Camera2
一样需要展示预览的控件PreviewView
到布局上,并确保获得了camera
权限。差异的地方主要体现在相机的配置步骤上。
private void setupCamera(PreviewView previewView) {
ListenableFuture 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
。接下来介绍下它们如何使用。
图像拍摄
借助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
。
每个大类都包含几个典型的效果。
- NightPreviewExtender 夜拍预览
- BokehPreviewExtender 人像预览
- BeautyPreviewExtender 美顔预览
- HdrPreviewExtender HDR预览
- AutoPreviewExtender 自动预览
开启这些效果的实现也非常简单。
private void bindPreview(@NonNull ProcessCameraProvider cameraProvider,
PreviewView previewView, boolean isVideo) {
Preview.Builder previewBuilder = new Preview.Builder();
总结
首先是感觉自己的基础还是不够吧,大厂好像都喜欢问这些底层原理。
另外一部分原因在于资料也还没有看完,一面时凭借那份资料考前突击恶补个几天居然也能轻松应对(在这里还是要感谢那份资料,真的牛),于是自我感觉良好,资料就没有怎么深究下去了。
之前的准备只涉及了Java、Android、计网、数据结构与算法这些方面,面对面试官对其他基础课程的考察显得捉襟见肘。
下一步还是要查漏补缺,进行针对性复习。
最后的最后,那套资料这次一定要全部看完,是真的太全面了,各个知识点都涵盖了,几乎我面试遇到的所有问题的知识点这里面都有!希望大家不要犯和我一样的错误呀!!!一定要看完!
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!
础课程的考察显得捉襟见肘。
下一步还是要查漏补缺,进行针对性复习。
最后的最后,那套资料这次一定要全部看完,是真的太全面了,各个知识点都涵盖了,几乎我面试遇到的所有问题的知识点这里面都有!希望大家不要犯和我一样的错误呀!!!一定要看完!
[外链图片转存中…(img-mPHL1jVR-1715174436179)]
[外链图片转存中…(img-cTsuS2Nl-1715174436179)]
[外链图片转存中…(img-ZsHxajRz-1715174436180)]
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!