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()方法即可。

结束语

最后,大家想看代码的话,可以看下我封装的二维码扫描库,实现原理是一样的。可以看我这篇文章:一款好用的二维码扫描组件

二维码扫描库QrScan的GitHub:https://github.com/yushiwo/QrScan

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容