1. 程式人生 > >Android Camera使用總結與那些坑

Android Camera使用總結與那些坑

寫在開頭

需求方:上傳試卷的時候,使用者自己拍的照片有很多問題。如:不清晰、圖片歪了、錯誤圖片等。我們要是能夠對拍攝照片進行識別處理就好了,能夠裁切矯正就更好了,最好可以像二維碼掃描一樣,直接識別處理~

開發:滿足你!

整體框架邏輯

試卷掃描模組,最核心的邏輯就是資料採集、解碼識別、圖片裁切,再加上對識別結果和裁切結果的處理,就構成了整個模組的主邏輯。整個邏輯的實現如下圖所示:

試卷掃描框圖

在模組中,除了UI執行緒,還開啟了一個Deocde執行緒,用來處理圖片的解碼識別和裁切。這麼做的原因是因為對於圖片資料的處理,是比較耗時的,如果在UI執行緒處理,會有ANR的風險。同時採用這種處理方式,整個模組的流暢性也更加好,且模組的結構更加清晰。
那麼執行緒之間是如何互動的呢?這裡模組中是採用了最常用的Handler訊息傳遞機制。因為通過Handler的Message可以線上程間傳遞較大的圖片資料(注意如果在Intent的Bundle中傳遞較大的資料,會崩潰報錯)。請看下面這段程式碼:

  @Override
  public void run() {
    Looper.prepare();
    handler = new DecodeHandler(activity);
    handlerInitLatch.countDown();
    Looper.loop();
  }

上面這個方法是DecodeThread的run方法,在方法中,我們初始化了當前執行緒對應的Handler物件DecodeHandler。而DecodeHandler初始化是需要傳入當前主執行緒的上下文activity,通過activity我們可以拿到主執行緒的Handler物件。這樣的話主執行緒和解碼執行緒就建立了聯絡,它們之間就可以方便得進行訊息傳遞了。最終實現的模組採集介面如下所示:

掃碼介面

模組開發相關實現

整個掃碼拍照模組的邏輯比較瑣碎,就不一一說明了。以下是整理的幾個開發中比較關鍵的點和Camera硬體開發一些經驗,在這裡做記錄,避免以後重複造輪子。

閃光燈設定

  • 開啟閃光燈
public void turnOnFlash(){
        if(camera != null){
            try {
                Camera.Parameters parameters = camera.getParameters();
                parameters.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);
                camera.setParameters(parameters);
            } catch
(Exception e) { e.printStackTrace(); } } }
  • 關閉閃光燈
  public void turnOffFlash(){
        if(camera != null){
            try {
                Camera.Parameters parameters = camera.getParameters();
                parameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
                camera.setParameters(parameters);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

預覽圖片解析度選擇

預覽圖片的解析度選擇邏輯是:有1920*1080則選之,否則選硬體支援的最大的解析度,且滿足圖片比例為16:9

private static Point findBestPreviewSizeValue(List<Camera.Size> sizeList, Point screenResolution) {
      int bestX = 0;
      int bestY = 0;
      int size = 0;
      for(int i = 0; i < sizeList.size(); i ++){
          // 如果有符合的解析度,則直接返回
          if(sizeList.get(i).width == DEFAULT_WIDTH && sizeList.get(i).height == DEFAULT_HEIGHT){
              Log.d(TAG, "get default preview size!!!");
              return new Point(DEFAULT_WIDTH, DEFAULT_HEIGHT);
          }

          int newX = sizeList.get(i).width;
          int newY = sizeList.get(i).height;
          int newSize = Math.abs(newX * newX) + Math.abs(newY * newY);
          float ratio = (float)newY / (float)newX;
          Log.d(TAG, newX + ":" + newY + ":" + ratio);
          if (newSize >= size && ratio != 0.75) {  // 確保圖片是16:9的
              bestX = newX;
              bestY = newY;
              size = newSize;
          } else if (newSize < size) {
              continue;
          }
      }

      if (bestX > 0 && bestY > 0) {
          return new Point(bestX, bestY);
      }
      return null;
  }

拍照圖片解析度選擇

在硬體支援的拍照圖片解析度列表中,拍照圖片解析度選擇邏輯:

  1. 有1920*1080則選之
  2. 選擇大於螢幕解析度且圖片比例為16:9的
  3. 選擇圖片解析度儘可能大且圖片比例為16:9的
 private static Point findBestPictureSizeValue(List<Camera.Size> sizeList, Point screenResolution){
        List<Camera.Size> tempList = new ArrayList<>();

        for(int i = 0; i < sizeList.size(); i ++){
            // 如果有符合的解析度,則直接返回
            if(sizeList.get(i).width == DEFAULT_WIDTH && sizeList.get(i).height == DEFAULT_HEIGHT){
                Log.d(TAG, "get default picture size!!!");
                return new Point(DEFAULT_WIDTH, DEFAULT_HEIGHT);
            }
            if(sizeList.get(i).width >= screenResolution.x && sizeList.get(i).height >= screenResolution.y){
                tempList.add(sizeList.get(i));
            }
        }

        int bestX = 0;
        int bestY = 0;
        int diff = Integer.MAX_VALUE;
        if(tempList != null && tempList.size() > 0){
            for(int i = 0; i < tempList.size(); i ++){
                int newDiff = Math.abs(tempList.get(i).width - screenResolution.x) + Math.abs(tempList.get(i).height - screenResolution.y);
                float ratio = (float)tempList.get(i).height / tempList.get(i).width;
                Log.d(TAG, "ratio = " + ratio);
                if(newDiff < diff && ratio != 0.75){  // 確保圖片是16:9的
                    bestX = tempList.get(i).width;
                    bestY = tempList.get(i).height;
                    diff = newDiff;
                }
            }
        }

        if (bestX > 0 && bestY > 0) {
            return new Point(bestX, bestY);
        }else {
            return findMaxPictureSizeValue(sizeList);
        }
    }

預覽模式迴圈自動對焦

預覽模式時,支援自動對焦。當前處理邏輯是在AutoFocusCallback的回撥方法onAutoFocus中,延遲傳送Message資訊。這樣在上一次聚焦完成後,固定時間的延遲後會傳送下一次的自動聚焦訊息,如此達到迴圈聚焦的目的。

 @Override
    public void onAutoFocus(boolean success, Camera camera) {
        Log.d(TAG, "onAutoFocus");
        PaperScanConstant.isAutoFocusSuccess = true;
        if (autoFocusHandler != null) {
            Message message = autoFocusHandler.obtainMessage(autoFocusMessage, success);
            autoFocusHandler.sendMessageDelayed(message, AUTOFOCUS_INTERVAL_MS);
            autoFocusHandler = null;
        } else {
            Log.d(TAG, "Got auto-focus callback, but no handler for it");
        }
   }

預覽畫面不失真展示

如果預覽圖片的解析度比例和手機畫面上展示拍攝畫面的區域比例不一致的話,就會出現畫面拉伸或者壓縮的現象。為了解決這個問題,取得更好的使用者體驗。模組在佈局的時候,對螢幕展示區域是動態計算的,以保證預覽區域比例與圖片的解析度比例是一致的。

模組開發中的那些坑

掃碼模組開發,因為是跟手機硬體Camera打交道,基於目前市場中Android手機眾多的型號和搭載的五花八門的ROM,沒坑那是不可能的!!!下面是本模組開發過程中的相關坑。

部分機子拍攝照片解析度不高

開發過程中碰到過這麼一種情況,在部分機子上,明明已經聚焦,手機的解析度也很高,但是拍出的照片解析度卻很小。究其原因,就是不同的手機ROM,獲取的預設的照片解析度是不同的。有的手機預設照片解析度高,則照片就清晰;有的預設解析度是最低的一檔,則無論你手機解析度多高,拍出來的照片還是很模糊的。解決方案就是需要顯示設定拍照的圖片解析度:

parameters.setPreviewSize(cameraResolution.x, cameraResolution.y);
parameters.setPictureSize(pictureResolution.x, pictureResolution.y);

部分機子拍攝照片發生了旋轉

還是由於Android手機碎片化的問題,每個手機預設拍照的旋轉角度是不一樣的。剛開始模組中是按照預設旋轉90度處理,在大多數機子上是沒有問題的。但是在碰到Nexus 5X的時候就出問題了,圖片上下導致了。查閱了相關資料,Google官方提供了下面的方法,解決了這個問題。

public void setCameraDisplayOrientation(int cameraId, android.hardware.Camera camera) {
        android.hardware.Camera.CameraInfo info = new android.hardware.Camera.CameraInfo();
        android.hardware.Camera.getCameraInfo(cameraId, info);
        int rotation = BaseApplication.getInstance().getCurrentActivity().getWindowManager().getDefaultDisplay()
                .getRotation();
        int degrees = 0;
        switch (rotation) {
            case Surface.ROTATION_0: degrees = 0; break;
            case Surface.ROTATION_90: degrees = 90; break;
            case Surface.ROTATION_180: degrees = 180; break;
            case Surface.ROTATION_270: degrees = 270; break;
        }

        int result;
        if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
            result = (info.orientation + degrees) % 360;
            result = (360 - result) % 360;  // compensate the mirror
        } else {  // back-facing
            result = (info.orientation - degrees + 360) % 360;
        }
        // 記錄本機子相機的旋轉角度
        PaperScanConstant.cameraRotation = result;
        camera.setDisplayOrientation(result);
    }

    private int findFrontFacingCameraID() {
        int cameraId = -1;
        // Search for the back facing camera
        int numberOfCameras = Camera.getNumberOfCameras();
        for (int i = 0; i < numberOfCameras; i++) {
            Camera.CameraInfo info = new Camera.CameraInfo();
            Camera.getCameraInfo(i, info);
            if (info.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
                Log.d(TAG, "Camera found");
                cameraId = i;
                break;
            }
        }
        return cameraId;
    }

頻繁點選螢幕應用崩潰

因為應用支援點選螢幕自動聚焦功能,但在某些機子上,使用者頻繁點選螢幕進行自動聚焦,應用發生了崩潰。究其原因是因為在某些ROM上,當上一次聚焦沒有完成時,就進行下一次聚焦,就會發生崩潰。解決方案是通過設定標誌位,只有在上一次聚焦完成後,才能進行下一次聚焦。

第三發ROM禁止了應用的攝像頭許可權

有些第三方ROM會有自己的許可權管理機制,當應用的攝像頭許可權被禁止了,進入掃碼頁,會發生崩潰。這樣的互動體驗肯定不是很好,互動要求這邊許可權被禁止以後,還是需要有一個溫和的提示,提醒使用者去設定頁面重新賦予應用攝像頭許可權。但是系統也沒有提供介面說當前應用這個許可權被禁止了。因此模組中採用了一個折中的方案,監獄應用沒有攝像頭許可權時候,開啟攝像頭會崩潰。因此我們捕獲開啟Camera的異常,在捕獲異常時候彈框提醒使用者去開啟許可權。

  try {
        CameraManager.get().openDriver(surfaceHolder);
  } catch (Throwable tr){
        showOpenCameraErrorDialog();
        return; 
  }

Pad進入掃碼頁應用崩潰

實際上線時候,發現使用者使用pad的話,一進入掃碼頁面就崩潰。因為我們應用首次進入掃碼頁面預設是開啟裝置閃光燈的。但是pad沒有閃光燈,因此就崩潰了。剛開始用如下方式檢測裝置是否支援閃光燈:

getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH)

但是失敗了。原因是好多pad的ROM是從手機ROM改過去的,有可能改得不是那麼徹底。所以在Pad上呼叫如上程式碼進行判斷時,還是會返回true。這是隻能求助於try catch了。就是在開關閃光燈的時候進行異常捕獲,這樣在Pad上開關閃光燈崩潰問題就解決了。

部分機子拍照後閃光燈自動關閉

部分機子,在閃光燈開啟的狀態下,點選拍照按鈕,閃光燈關閉了。目前沒有找到原因,只能在模組中加了特殊處理。針對當前有此問題的手機,拍照完後主動再去開關一次閃光燈,這樣拍照完成後,閃光燈還是可以亮著。只是在拍照的過程中,會出現閃光燈閃爍的情況。

部分機子拍照完後預覽畫面卡住了

部分機子,當點選拍照完成一張照片的拍攝後,後面就停止不動了。出現這種現象是因為在拍照的時候,Camera會停止Preview,拍照完成後,有的機子可以恢復回來重新Preview,有的則不會。因此只需在拍照完成後,手動呼叫一次Camera的startPreview()方法即可。

結束語

最後,大家想看程式碼的話,可以看下我封裝的二維碼掃描庫,實現原理是一樣的。 可以看我這篇文章:一款好用的二維碼掃描元件