<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          為什么推薦使用Jetpack CameraX?

          共 32514字,需瀏覽 66分鐘

           ·

          2023-03-04 02:02

           安卓進階漲薪訓練營,讓一部分人先進大廠


          大家好,我是皇叔,最近開了一個安卓進階漲薪訓練營,可以幫助大家突破技術(shù)&職場瓶頸,從而度過難關(guān),進入心儀的公司。


          詳情見文章:沒錯!皇叔開了個訓練營

          前言

          我們的生活已經(jīng)越來越離不開相機,從自拍到直播,掃碼再到VR等等。相機的優(yōu)劣自然就成為了廠商競相追逐的賽場。對于app開發(fā)者來說,如何快速驅(qū)動相機,提供優(yōu)秀的拍攝體驗,優(yōu)化相機的使用功耗,是一直以來追求的目標。

          Android 5.0 時期Camera接口便已棄用,所以一般的做法是使用其替代者Camera2接口。但隨著CameraX的出現(xiàn),這個選擇變得不再唯一。我們先來回顧下圖像預覽這一簡單的需求,使用Camera2接口是如何實現(xiàn)的。

          Camera2

          拋開回調(diào),異常等附加處理,仍然需要多個步驟才能實現(xiàn),比較繁瑣?!蚴÷源a只概括步驟※


          同樣是圖像預覽采用CameraX的話,實現(xiàn)就非常簡潔。

          CameraX

          圖像預覽

          可以說十幾行就可以完成。和Camera2一樣需要展示預覽的控件PreviewView到布局上,并確保獲得了camera權(quán)限。差異的地方主要體現(xiàn)在相機的配置步驟上。

              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發(fā)生異常
                  cameraProvider.unbindAll(); 
                  mCamera = cameraProvider.bindToLifecycle(this, cameraSelector, mPreview);
                  ...
              }

          鏡頭聚焦

          無法聚焦的拍攝是不完整的,我們監(jiān)聽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將這些使用場景統(tǒng)一抽象為UseCase,它有四個子類,分別為Preview,ImageCapture,ImageAnalysis和VideoCapture。接下來介紹下它們?nèi)绾问褂谩?/span>

          圖像拍攝

          借助ImageCapture提供的takePicture()可以將圖像拍攝下來。支持保存到外部存儲空間,當然需要獲得external storage的讀寫權(quán)限。

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

          圖像分析

          圖像分析指的是對預覽的圖像實時分析,將色彩,內(nèi)容等信息識別出來,應(yīng)用在機器學習,二維碼識別等業(yè)務(wù)場景。繼續(xù)對demo做些改造,添加掃描二維碼的按鈕。點擊按鈕后進入掃碼模式,并在二維碼解析成功后彈出解析結(jié)果。

              public void onAnalyzeGo(View view) {
                  if (!isAnalyzing) {
                      mImageAnalysis.setAnalyzer(CameraXExecutors.mainThreadExecutor(), image -> {
                         analyzeQRCode(image);
                      });
                  }
                  ...
              }

              // 從ImageProxy取出圖像數(shù)據(jù),交由二維碼框架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上添加一個圖像拍攝和視頻錄制模式的切換按鈕,切換到視頻錄制模式的時候?qū)⒁曨l拍攝的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權(quán)限,之后再開始視頻的錄制。

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

          小插曲

          實現(xiàn)視頻錄制功能的時候發(fā)現(xiàn)一個問題。

          點擊視頻錄制按鈕的時候,如果此刻尚未獲得audio權(quán)限,那么將申請該權(quán)限。即便此后獲得了權(quán)限調(diào)用拍攝接口仍將發(fā)生異常。日志顯示AudioRecorder實例為null引發(fā)了NPE。

          仔細查看相關(guān)邏輯發(fā)現(xiàn),demo現(xiàn)在的處理是在切換為視頻錄制模式的時候,就將VideoCapture綁定到了CameraProvider。這個時間點如果還未獲得audio權(quán)限的話,那么將無法初始化AudioRecorder。其實日志里也會給出相應(yīng)提示:VideoCapture: AudioRecord object cannot initialized correctly。

          可是后面獲得了權(quán)限再去調(diào)用VideoCapture的拍攝接口為何還是會發(fā)生NPE?

          因為拍攝接口startRecording()的內(nèi)部處理是AudioRecorder實例為null的話將直接終止請求。后面無論調(diào)用多少遍也無濟于事。事實上該函數(shù)的后段存在再次獲取AudioRecorder實例的邏輯,但因為前面發(fā)生了NPE而沒有機會執(zhí)行。

              // VideoCapture.java
              public void startRecording(
                      @NonNull OutputFileOptions outputFileOptions, @NonNull Executor executor,
                      @NonNull OnVideoSavedCallback callback)
           
          {
                  ...
                  try {
                      // mAudioRecorder為null將引發(fā)NPE終止錄制的請求
                      mAudioRecorder.startRecording();
                  } catch (IllegalStateException e) {
                      postListener.onError(ERROR_ENCODER, "AudioRecorder start fail", e);
                      return;
                  }

                  ...
                  mRecordingFuture.addListener(() -> {
                      ...
                      if (getCamera() != null) {
                          // 前面發(fā)生了NPE,那么將失去此處再次獲得AudioRecorder實例的機會
                          setupEncoder(getCameraId(), getAttachedSurfaceResolution());
                          notifyReset();
                      }
                  }, CameraXExecutors.mainThreadExecutor());
                  ...
              }
          不知道這是VideoCapture實現(xiàn)上的漏洞還是開發(fā)者有意為之。但是在明明已經(jīng)獲得了audio權(quán)限的情況下調(diào)用錄製接口卻仍然發(fā)生NPE貌似并不合理。

          當下只能采取一些回避方案,或者說開發(fā)者本該就這么做?

          現(xiàn)在是在獲得了audio權(quán)限前執(zhí)行了VideoCapture的綁定,這存在發(fā)生上述反復NPE的可能。所以改成獲得audio權(quán)限后再綁定VideoCapture即可回避。

          話說回來,在VideoCaptue的文檔里加上需要獲得audio的權(quán)限的說明是不是更好一些呢?

          相機效果擴展

          光有上述幾個場景的使用并不能滿足日益豐富的拍攝需求,人像,夜拍,美顏等相機效果是必不可少的。幸好CameraX是支持效果擴展的。但不是所有設(shè)備都能兼容這種擴展,具體可在官網(wǎng)的設(shè)備兼容列表里查詢到。

          可供擴展的效果主要分為兩大類,一個是用于圖像預覽時效果擴展的PreviewExtender,另一個是用于圖像拍攝時效果擴展的ImageCaptureExtender。

          每個大類都包含幾個典型的效果。


          • NightPreviewExtender 夜拍預覽
          • BokehPreviewExtender 人像預覽
          • BeautyPreviewExtender 美顔預覽
          • HdrPreviewExtender HDR預覽
          • AutoPreviewExtender 自動預覽



          開啟這些效果的實現(xiàn)也非常簡單。

              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效果擴展的設(shè)備列表里,無法給大家展示成功擴展效果的樣圖。

          高階用法

          除了上述常見相機使用場景外還有其他可選的配置方法。篇幅限制不再詳細展開,感興趣者可參考官網(wǎng)進行嘗試。

          使用注意


          1. 調(diào)用CameraProvider的bindToLifecycle()前記得先調(diào)用unbindAll(),否則可能發(fā)生重復綁定的exception
          2. ImageAnalyzer的analyze()在分析完圖片之后應(yīng)立即調(diào)用ImageProxy的close()釋放圖像,以便后續(xù)圖像能繼續(xù)傳送過來。否則將阻塞回調(diào)。因而也要注意分析圖像的耗時問題
          3. 每個ImageProxy實例在關(guān)閉后不要存儲它的引用,因為一旦調(diào)用close(),這些圖像將變得不合法
          4. 圖像分析結(jié)束后應(yīng)當調(diào)用ImageAnalysis的clearAnalyzer()以告知不用將圖像流傳輸過來避免性能的浪費
          5. 視頻錄制場景一定不要忘記獲得audio權(quán)限

          有趣的兼容性處理

          實現(xiàn)圖像拍攝功能的時候發(fā)現(xiàn)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的話,將插入一行以驗證保存路徑是否合法并可寫。驗證結(jié)束后會刪除該測試行。

          但是在Huawei設(shè)備上刪除行的操作將觸發(fā)一條刪除照片的通知。所以為避免困擾用戶,CameraX將會在Huawei設(shè)備上跳過路徑的驗證。

          class ImageSaveLocationValidator {
              // 將判斷設(shè)備品牌是否為華為或榮耀,是則直接跳過驗證
              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的優(yōu)勢

          源于CameraX在Camera2的基礎(chǔ)上進行了高度的封裝和對大量設(shè)備進行了兼容性的處理,使得CameraX擁有了很多優(yōu)勢。


          • 易用性,采用封裝的API可以高效達到目標
          • 設(shè)備一致性,不用在乎版本,忽略設(shè)備硬件差異帶來的開發(fā)區(qū)別,達到一致的開發(fā)體驗
          • 新的相機體驗,通過效果擴展可以實現(xiàn)和原生相機一樣的美顏等拍攝功能

          本文demo

          demo的源碼已經(jīng)開源至Github,大家可以查閱參考。


          https://github.com/ellisonchan/JetpackDemo

          結(jié)語

          CameraX發(fā)布于2019年8月7日,從alpha版到現(xiàn)在的beta版,一直在更新。從上面有趣的Huawei設(shè)備兼容性處理可以看到CameraX一統(tǒng)江湖的決心。最新仍是beta版,需要繼續(xù)改進,但并非不能投入生產(chǎn)環(huán)境。這么好用的框架,大家要多多使用并給出建議,這樣才能越來越完善,才能給開發(fā)者給用戶帶來福音。



          為了防止失聯(lián),歡迎關(guān)注我防備的小號

           

                         微信改了推送機制,真愛請星標本公號??

          瀏覽 83
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  久久久久久久视频 | 99影视成人片 | 波多野结衣国产在线 | A片www | 99色国产 |