Android Camera使用總結(jié)與那些坑

寫在開頭

需求方:上傳試卷的時(shí)候忠烛,用戶自己拍的照片有很多問題属提。如:不清晰、圖片歪了美尸、錯(cuò)誤圖片等冤议。我們要是能夠?qū)ε臄z照片進(jìn)行識別處理就好了,能夠裁切矯正就更好了师坎,最好可以像二維碼掃描一樣恕酸,直接識別處理~

開發(fā):滿足你!

整體框架邏輯

試卷掃描模塊胯陋,最核心的邏輯就是數(shù)據(jù)采集蕊温、解碼識別、圖片裁切遏乔,再加上對識別結(jié)果和裁切結(jié)果的處理义矛,就構(gòu)成了整個(gè)模塊的主邏輯。整個(gè)邏輯的實(shí)現(xiàn)如下圖所示:

試卷掃描框圖
試卷掃描框圖

在模塊中盟萨,除了UI線程凉翻,還開啟了一個(gè)Deocde線程,用來處理圖片的解碼識別和裁切捻激。這么做的原因是因?yàn)閷τ趫D片數(shù)據(jù)的處理制轰,是比較耗時(shí)的前计,如果在UI線程處理,會(huì)有ANR的風(fēng)險(xiǎn)艇挨。同時(shí)采用這種處理方式残炮,整個(gè)模塊的流暢性也更加好,且模塊的結(jié)構(gòu)更加清晰缩滨。
那么線程之間是如何交互的呢势就?這里模塊中是采用了最常用的Handler消息傳遞機(jī)制。因?yàn)橥ㄟ^Handler的Message可以在線程間傳遞較大的圖片數(shù)據(jù)(注意如果在Intent的Bundle中傳遞較大的數(shù)據(jù)脉漏,會(huì)崩潰報(bào)錯(cuò))苞冯。請看下面這段代碼:

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

上面這個(gè)方法是DecodeThread的run方法,在方法中侧巨,我們初始化了當(dāng)前線程對應(yīng)的Handler對象DecodeHandler舅锄。而DecodeHandler初始化是需要傳入當(dāng)前主線程的上下文activity,通過activity我們可以拿到主線程的Handler對象司忱。這樣的話主線程和解碼線程就建立了聯(lián)系皇忿,它們之間就可以方便得進(jìn)行消息傳遞了。最終實(shí)現(xiàn)的模塊采集界面如下所示:

掃碼界面
掃碼界面

模塊開發(fā)相關(guān)實(shí)現(xiàn)

整個(gè)掃碼拍照模塊的邏輯比較瑣碎坦仍,就不一一說明了鳍烁。以下是整理的幾個(gè)開發(fā)中比較關(guān)鍵的點(diǎn)和Camera硬件開發(fā)一些經(jīng)驗(yàn),在這里做記錄繁扎,避免以后重復(fù)造輪子幔荒。

閃光燈設(shè)置

  • 開啟閃光燈
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();
            }
        }
    }
  • 關(guān)閉閃光燈
  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();
            }
        }
    }

預(yù)覽圖片分辨率選擇

預(yù)覽圖片的分辨率選擇邏輯是:有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);
        }
    }

預(yù)覽模式循環(huán)自動(dòng)對焦

預(yù)覽模式時(shí)姚垃,支持自動(dòng)對焦。當(dāng)前處理邏輯是在AutoFocusCallback的回調(diào)方法onAutoFocus中盼忌,延遲發(fā)送Message信息积糯。這樣在上一次聚焦完成后,固定時(shí)間的延遲后會(huì)發(fā)送下一次的自動(dòng)聚焦消息碴犬,如此達(dá)到循環(huán)聚焦的目的。

 @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");
        }
   }

預(yù)覽畫面不失真展示

如果預(yù)覽圖片的分辨率比例和手機(jī)畫面上展示拍攝畫面的區(qū)域比例不一致的話梆暮,就會(huì)出現(xiàn)畫面拉伸或者壓縮的現(xiàn)象服协。為了解決這個(gè)問題,取得更好的用戶體驗(yàn)啦粹。模塊在布局的時(shí)候偿荷,對屏幕展示區(qū)域是動(dòng)態(tài)計(jì)算的窘游,以保證預(yù)覽區(qū)域比例與圖片的分辨率比例是一致的。

模塊開發(fā)中的那些坑

掃碼模塊開發(fā)跳纳,因?yàn)槭歉謾C(jī)硬件Camera打交道忍饰,基于目前市場中Android手機(jī)眾多的型號和搭載的五花八門的ROM,沒坑那是不可能的K伦0丁!下面是本模塊開發(fā)過程中的相關(guān)坑斗塘。

部分機(jī)子拍攝照片分辨率不高

開發(fā)過程中碰到過這么一種情況赢织,在部分機(jī)子上,明明已經(jīng)聚焦馍盟,手機(jī)的分辨率也很高于置,但是拍出的照片分辨率卻很小。究其原因贞岭,就是不同的手機(jī)ROM八毯,獲取的默認(rèn)的照片分辨率是不同的。有的手機(jī)默認(rèn)照片分辨率高瞄桨,則照片就清晰话速;有的默認(rèn)分辨率是最低的一檔,則無論你手機(jī)分辨率多高讲婚,拍出來的照片還是很模糊的尿孔。解決方案就是需要顯示設(shè)置拍照的圖片分辨率:

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

部分機(jī)子拍攝照片發(fā)生了旋轉(zhuǎn)

還是由于Android手機(jī)碎片化的問題,每個(gè)手機(jī)默認(rèn)拍照的旋轉(zhuǎn)角度是不一樣的筹麸。剛開始模塊中是按照默認(rèn)旋轉(zhuǎn)90度處理活合,在大多數(shù)機(jī)子上是沒有問題的。但是在碰到Nexus 5X的時(shí)候就出問題了物赶,圖片上下導(dǎo)致了白指。查閱了相關(guān)資料,Google官方提供了下面的方法酵紫,解決了這個(gè)問題告嘲。

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;
        }
        // 記錄本機(jī)子相機(jī)的旋轉(zhuǎn)角度
        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;
    }

頻繁點(diǎn)擊屏幕應(yīng)用崩潰

因?yàn)閼?yīng)用支持點(diǎn)擊屏幕自動(dòng)聚焦功能,但在某些機(jī)子上奖地,用戶頻繁點(diǎn)擊屏幕進(jìn)行自動(dòng)聚焦橄唬,應(yīng)用發(fā)生了崩潰。究其原因是因?yàn)樵谀承㏑OM上参歹,當(dāng)上一次聚焦沒有完成時(shí)仰楚,就進(jìn)行下一次聚焦,就會(huì)發(fā)生崩潰。解決方案是通過設(shè)置標(biāo)志位僧界,只有在上一次聚焦完成后侨嘀,才能進(jìn)行下一次聚焦。

第三發(fā)ROM禁止了應(yīng)用的攝像頭權(quán)限

有些第三方ROM會(huì)有自己的權(quán)限管理機(jī)制捂襟,當(dāng)應(yīng)用的攝像頭權(quán)限被禁止了咬腕,進(jìn)入掃碼頁,會(huì)發(fā)生崩潰葬荷。這樣的交互體驗(yàn)肯定不是很好涨共,交互要求這邊權(quán)限被禁止以后,還是需要有一個(gè)溫和的提示闯狱,提醒用戶去設(shè)置頁面重新賦予應(yīng)用攝像頭權(quán)限煞赢。但是系統(tǒng)也沒有提供接口說當(dāng)前應(yīng)用這個(gè)權(quán)限被禁止了。因此模塊中采用了一個(gè)折中的方案哄孤,監(jiān)獄應(yīng)用沒有攝像頭權(quán)限時(shí)候照筑,開啟攝像頭會(huì)崩潰。因此我們捕獲開啟Camera的異常瘦陈,在捕獲異常時(shí)候彈框提醒用戶去開啟權(quán)限凝危。

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

Pad進(jìn)入掃碼頁應(yīng)用崩潰

實(shí)際上線時(shí)候,發(fā)現(xiàn)用戶使用pad的話晨逝,一進(jìn)入掃碼頁面就崩潰蛾默。因?yàn)槲覀儜?yīng)用首次進(jìn)入掃碼頁面默認(rèn)是開啟設(shè)備閃光燈的。但是pad沒有閃光燈捉貌,因此就崩潰了支鸡。剛開始用如下方式檢測設(shè)備是否支持閃光燈:

getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH)

但是失敗了。原因是好多pad的ROM是從手機(jī)ROM改過去的趁窃,有可能改得不是那么徹底牧挣。所以在Pad上調(diào)用如上代碼進(jìn)行判斷時(shí),還是會(huì)返回true醒陆。這是只能求助于try catch了瀑构。就是在開關(guān)閃光燈的時(shí)候進(jìn)行異常捕獲,這樣在Pad上開關(guān)閃光燈崩潰問題就解決了刨摩。

部分機(jī)子拍照后閃光燈自動(dòng)關(guān)閉

部分機(jī)子寺晌,在閃光燈開啟的狀態(tài)下,點(diǎn)擊拍照按鈕澡刹,閃光燈關(guān)閉了呻征。目前沒有找到原因,只能在模塊中加了特殊處理罢浇。針對當(dāng)前有此問題的手機(jī)陆赋,拍照完后主動(dòng)再去開關(guān)一次閃光燈边篮,這樣拍照完成后,閃光燈還是可以亮著奏甫。只是在拍照的過程中,會(huì)出現(xiàn)閃光燈閃爍的情況凌受。

部分機(jī)子拍照完后預(yù)覽畫面卡住了

部分機(jī)子阵子,當(dāng)點(diǎn)擊拍照完成一張照片的拍攝后,后面就停止不動(dòng)了胜蛉。出現(xiàn)這種現(xiàn)象是因?yàn)樵谂恼盏臅r(shí)候挠进,Camera會(huì)停止Preview,拍照完成后誊册,有的機(jī)子可以恢復(fù)回來重新Preview领突,有的則不會(huì)。因此只需在拍照完成后案怯,手動(dòng)調(diào)用一次Camera的startPreview()方法即可君旦。

結(jié)束語

最后,大家想看代碼的話嘲碱,可以看下我封裝的二維碼掃描庫金砍,實(shí)現(xiàn)原理是一樣的÷缶猓可以看我這篇文章:一款好用的二維碼掃描組件

二維碼掃描庫QrScan的GitHub:https://github.com/yushiwo/QrScan

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末恕稠,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子扶欣,更是在濱河造成了極大的恐慌鹅巍,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,348評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件料祠,死亡現(xiàn)場離奇詭異骆捧,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)术陶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,122評論 2 385
  • 文/潘曉璐 我一進(jìn)店門凑懂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人梧宫,你說我怎么就攤上這事接谨。” “怎么了塘匣?”我有些...
    開封第一講書人閱讀 156,936評論 0 347
  • 文/不壞的土叔 我叫張陵脓豪,是天一觀的道長。 經(jīng)常有香客問我忌卤,道長扫夜,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,427評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮笤闯,結(jié)果婚禮上堕阔,老公的妹妹穿的比我還像新娘。我一直安慰自己颗味,他們只是感情好超陆,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,467評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著浦马,像睡著了一般时呀。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上晶默,一...
    開封第一講書人閱讀 49,785評論 1 290
  • 那天谨娜,我揣著相機(jī)與錄音,去河邊找鬼磺陡。 笑死趴梢,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的币他。 我是一名探鬼主播垢油,決...
    沈念sama閱讀 38,931評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼圆丹!你這毒婦竟也來了滩愁?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,696評論 0 266
  • 序言:老撾萬榮一對情侶失蹤辫封,失蹤者是張志新(化名)和其女友劉穎硝枉,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體倦微,經(jīng)...
    沈念sama閱讀 44,141評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡妻味,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,483評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了欣福。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片责球。...
    茶點(diǎn)故事閱讀 38,625評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖拓劝,靈堂內(nèi)的尸體忽然破棺而出雏逾,到底是詐尸還是另有隱情,我是刑警寧澤郑临,帶...
    沈念sama閱讀 34,291評論 4 329
  • 正文 年R本政府宣布栖博,位于F島的核電站,受9級特大地震影響厢洞,放射性物質(zhì)發(fā)生泄漏仇让。R本人自食惡果不足惜典奉,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,892評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望丧叽。 院中可真熱鬧卫玖,春花似錦、人聲如沸踊淳。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽嚣崭。三九已至,卻和暖如春懦傍,著一層夾襖步出監(jiān)牢的瞬間雹舀,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工粗俱, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留说榆,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,324評論 2 360
  • 正文 我出身青樓寸认,卻偏偏與公主長得像签财,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子偏塞,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,492評論 2 348

推薦閱讀更多精彩內(nèi)容