Android 自定义相机 识别身份证(上)

前言

最近在忙着港股券商上线,在做港股的过程中,用户开户功能涉及到拍照识别身份证的功能,如图:

调用相机的两种方法

一.使用相机应用程序进行拍照

利用一个描述了执行目的Intent对象,Android可以将某些执行任务委托给其他应用,比如调用相机。整个过程包含三部分: Intent 本身,一个函数调用来启动外部的 Activity,当焦点返回到我们的Activity时,处理返回图像数据的代码。
下面的函数通过发送一个Intent来捕获照片:

static final int REQUEST_IMAGE_CAPTURE = 1;

private void dispatchTakePictureIntent() {
    Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
        startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE);
    }
}

注意在调用startActivityForResult()方法之前,先调用resolveActivity(),这个方法会返回能处理该Intent的第一个Activity(即检查有没有能处理这个Intent的Activity)。执行这个检查非常重要,因为如果在调用startActivityForResult()时,没有应用能处理你的Intent,应用将会崩溃。所以只要返回结果不为null,使用该Intent就是安全的。(因为这种方法并不能实现我们的需求,因此不做过多解释,需要的童鞋可以去搜索一下具体用法)

二.通过使用Android框架所提供的API来直接控制相机硬件,实现自定义相机模块。

1.打开相机对象

获取一个 Camera 实例是直接控制相机的第一步。Camera是最主要的类,用于管理和操作camera资源。它提供了完整的相机底层接口,支持相机资源切换,设置预览/拍摄尺寸,设定光圈、曝光、聚焦等相关参数,获取预览/拍摄帧数据等功能,我会在下面介绍一下他的主要方法.
编写一个打开相机的方法,在onResume()方法里面去调用执行,单独的方法使得代码更容易重用,也便于保持控制流程更加简单。

private int mCameraId = 0;

 /**
 * 获取Camera实例
 *
 * @return
 */
private void safeOpenCamera() {
    try {
        releaseCamera();
        mCamera = Camera.open(mCameraId);
    } catch (Exception e) {
        Log.e(getString(R.string.app_name), "failed to open Camera");
        e.printStackTrace();
    }
}

/**
 * 释放相机资源
 */
private void releaseCamera() {
    if (mCamera != null) {
        mCamera.setPreviewCallback(null);
        mCamera.stopPreview();
        mCamera.release();
        mCamera = null;
    }
}

需要注意的是:由于Android机制的缘故,Android相机只能够被一个APP线程所绑定,也就是说如果你正在使用相机进行拍照摄像的时候,另一个程序便不能使用Camera类来启用相机,而且会报出Can not release的错误,因为我们需要使用try语句块进行捕获一下.那么如何判定你是否使用了相机呢,其实只要使用了Camera.open()方法获得了相机的示例,系统就认为你使用了相机,所以当你使用了完了相机一定记得要释放相机的资源,不然别的应用程序用不了呀,我们可以使用 Camera.release()来释放相机资源,Google官方的意见是重写Activity的onPause()方法来Camera.release().
另外从API level 9开始,相机框架可以支持多个摄像头的打开操作。如果使用旧的API,在调用open()时不传入参数指定打开哪个摄像头,默认情况下会使用后置摄像头。0是后置摄像头,1是前置摄像头.

2.创建相机预览界面

拍照通常需要向用户提供一个预览界面来显示待拍摄的画面内容。我们可以使用SurfaceView来呈现相机采集到的图像画面。SurfaceView这个类是用于绘制相机预览图像的,提供给用户实时的预览图像。普通的view以及派生类都是共享同一个surface的,所有的绘制都必须在UI线程中进行。而surfaceview是一种比较特殊的view,它并不与其他普通view共享surface,而是在内部持有了一个独立的surface,surfaceview负责管理这个surface的格式、尺寸以及显示位置。由于UI线程还要同时处理其他交互逻辑,因此对view的更新速度和帧率无法保证,而surfaceview由于持有一个独立的surface,因而可以在独立的线程中进行绘制,因此可以提供更高的帧率。自定义相机的预览图像由于对更新速度和帧率要求比较高,所以比较适合用surfaceview来显示。

SurfaceHolder :surfaceholder是控制surface的一个抽象接口,它能够控制surface的尺寸和格式,修改surface的像素,监视surface的变化等等,surfaceholder的典型应用就是用于surfaceview中。surfaceview通过getHolder()方法获得surfaceholder 实例,通过后者管理监听surface 的状态。

SurfaceHolder.Callback 接口 :负责监听surface状态变化的接口,有三个方法:

surfaceCreated(SurfaceHolder holder):在surface创建后立即被调用。在开发自定义相机时,可以通过重载这个函数调用camera.open()、camera.setPreviewDisplay(),来实现获取相机资源、连接camera和surface等操作。

surfaceChanged(SurfaceHolder holder, int format, int width, int height):在surface发生format或size变化时调用。在开发自定义相机时,可以通过重载这个函数调用camera.startPreview来开启相机预览,使得camera预览帧数据可以传递给surface,从而实时显示相机预览图像。

surfaceDestroyed(SurfaceHolder holder):在surface销毁之前被调用。在开发自定义相机时,可以通过重载这个函数调用camera.stopPreview(),camera.release()来实现停止相机预览及释放相机资源等操作。

在这里你可以直接在activity中直接implements SurfaceHolder.Callback 的接口,也可以自定义一个surfaceview然后把camera这个实例传给她来使用.

3.设置和启动Preview

一个Camera实例与它相关的Preview必须按照特定的顺序来创建,通常来说Camera对象优先被创建,而Preview对象必须在surfaceChanged()这一回调方法里面重新启用(restart),写一个方法用来设置相机的参数和执行相机的预览Camera.startPreview()方法.

/**
 * 预览相机
 */
private void startPreview(Camera camera, SurfaceHolder holder) {
    try {
        setupCamera(camera);
        camera.setPreviewDisplay(holder);
        CameraUtil.getInstance().setCameraDisplayOrientation(this, mCameraId, camera);
        camera.startPreview();
    } catch (IOException e) {
        e.printStackTrace();
    }
}
4.修改相机设置

相机参数的修改可以改变拍照的成像效果,例如缩放大小,曝光补偿值等等。

/**
 * 设置
 */
private void setupCamera(Camera camera) {
    Camera.Parameters parameters = camera.getParameters();
    if (parameters.getSupportedFocusModes().contains(
            Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
        parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
    }

    //这里第三个参数为最小尺寸 getPropPreviewSize方法会对从最小尺寸开始升序排列 取出所有支持尺寸的最小尺寸
    Camera.Size previewSize = CameraUtil.getInstance().getPropPreviewSize(parameters.getSupportedPreviewSizes(), 800);
    parameters.setPreviewSize(previewSize.width, previewSize.height);

    Camera.Size pictureSize = CameraUtil.getInstance().getPropPictureSize(parameters.getSupportedPictureSizes(), 800);
    parameters.setPictureSize(pictureSize.width, pictureSize.height);

    camera.setParameters(parameters);
}
5.设置预览方向

借用官方文档的说明:

Most camera applications lock the display into landscape mode because that is the natural orientation of the camera sensor. This setting does not prevent you from taking portrait-mode photos, because the orientation of the device is recorded in the EXIF header. The setCameraDisplayOrientation() method lets you change how the preview is displayed without affecting how the image is recorded. However, in Android prior to API level 14, you must stop your preview before changing the orientation and then restart it.

大多数相机程序会锁定预览方向为横屏状态,因为该方向是相机传感器的自然放置方向。当然这一设定并不妨碍我们去拍竖屏的照片,这个时候设备的方向角度信息会被记录在EXIF文件头中。setCameraDisplayOrientation()方法可以让你在不影响照片拍摄过程的情况下,改变预览的方向。然而,对于Android API level 14及更旧版本的系统,在改变方向之前,我们必须先停止相机预览,设置方向之后,然后再重启预览。

6.拍摄照片

一旦预览启动成功之后,可以使用Camera.takePicture()方法拍摄照片。我们可以创建Camera.PictureCallback与Camera.ShutterCallback对象并将他们传递到Camera.takePicture()中。下面的代码创建了一个Camera.PictureCallback.

Camera.PictureCallback mRectJpegPictureCallback = new Camera.PictureCallback() {
    public void onPictureTaken(byte[] data, Camera camera) {
        // TODO Auto-generated method stub
        Bitmap b = null;
        if (null != data) {
            b = BitmapFactory.decodeByteArray(data, 0, data.length);
            mCamera.stopPreview();
            isPreviewing = false;
        }
        if (null != b) {
            Bitmap rotaBitmap = ImageUtil.getRotateBitmap(b, 90.0f);
            int x = rotaBitmap.getWidth() / 2 - DST_RECT_WIDTH / 2;
            int y = rotaBitmap.getHeight() / 2 - DST_RECT_HEIGHT / 2;

            Bitmap rectBitmap = Bitmap.createBitmap(rotaBitmap, x, y, DST_RECT_WIDTH, DST_RECT_HEIGHT);
            FileUtil.saveBitmap(rectBitmap);
            Intent intent = new Intent();
            intent.putExtra(AppConstant.KEY.IMG_PATH, FileUtil.getImgPath());
            intent.putExtra(AppConstant.KEY.PIC_WIDTH, rectBitmap.getWidth());
            intent.putExtra(AppConstant.KEY.PIC_HEIGHT, rectBitmap.getHeight());
            setResult(AppConstant.RESULT_CODE.RESULT_OK, intent);
            finish();
            if (rotaBitmap.isRecycled()) {
                rotaBitmap.recycle();
                rotaBitmap = null;
            }
            if (rectBitmap.isRecycled()) {
                rectBitmap.recycle();
                rectBitmap = null;
            }
        }
        mCamera.startPreview();
        isPreviewing = true;
        if (!b.isRecycled()) {
            b.recycle();
            b = null;
        }
    }
};
8.停止预览并释放相机

当应用使用完相机之后,我们有必要进行清理释放资源的操作。尤其是,我们必须释放Camera对象,不然的话可能会引起其他应用程序使用Camera实例的时候发生崩溃,包括我们自己应用也同样会遇到这个问题。

那么何时应该停止预览并释放相机呢?在预览SurfaceView组件被销毁之后,调用释放相机的方法.
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
releaseCamera();
}

9.遇到的问题 SurfaceView预览图像拉伸变形,拍摄照片尺寸不对

说明这个问题之前,同样先说一下几个跟相机有关的尺寸。

SurfaceView尺寸 :即自定义相机应用中用于显示相机预览图像的View的尺寸,当它铺满全屏时就是屏幕的大小。这里surfaceview显示的预览图像暂且称作手机预览图像。

Previewsize :相机硬件提供的预览帧数据尺寸。预览帧数据传递给SurfaceView,实现预览图像的显示。这里预览帧数据对应的预览图像暂且称作相机预览图像。

Picturesize :相机硬件提供的拍摄帧数据尺寸。拍摄帧数据可以生成位图文件,最终保存成.jpg或者.png等格式的图片。这里拍摄帧数据对应的图像称作相机拍摄图像。图4说明了以上几种图像及照片之间的关系。手机预览图像是直接提供给用户看的图像,它由相机预览图像生成,拍摄照片的数据则来自于相机拍摄图像。

原因是没有正确设置比例 parameter.setPictureSize(width,height),这个比例不是你决定的,要先通过camera.getParameters().getSupportedPictureSizes()获得手机支持的尺寸。

自定义相机其实还是比较容易的,按着步骤来都可以一点一点实现.项目中的自定义相机部分首先是写了一个遮罩view,预览的时候是四周暗中间亮,其次就是对相机拍摄出来的照片的处理,在拍摄的时候会选取适合屏幕大小,但是我们想拍摄出中间亮的部分区域的照片,所以我们需要对拍摄出来的照片做一些处理,实现和源码会在下一篇文章中展现Android 自定义相机 识别身份证(下).

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