本文多是當下最新最全的
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…