美文网首页camera
使用SurfaceView自定义相机预览控件

使用SurfaceView自定义相机预览控件

作者: mandypig | 来源:发表于2018-02-07 14:26 被阅读0次

之前公司的一个开发需求,相信有不少人应该也有遇到类似需求所以就特地总结了下放到网上供大家参考。

大致需求如下:扫描身份证然后将图片信息上传服务端,识别出身份证上的号码,名字等关键信息,预期做成二维码扫描类似的功能,将身份证放置到预设的区域后自动完成拍照,然后上传服务端进行识别,如果识别失败继续拍照。该需求实现过程中参考了二维码扫描zxing的部分源码,其实照理说完全可以在zxing的基础上修改下也能实现上述需求,但是考虑到我之前很少使用相机api加上zxing预览源码比较简单就索性自己实现一下,毕竟使用轮子人人都会talk is cheap show me the code,在show代码之前先给大家看下最终实现的UI效果。

QQ截图20180206171602.png

可能是公司太破了,也可能是手机摄像头比较给力,墙上的裂缝一不小心就给拍到了,但这不是重点,重点是如何实现。

预览布局实现

这个就比较简单了,连里面的蓝框背景还有中间的蓝色扫描线我都是直接拿zxing中的图片直接使用,直接上布局

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <businesshall.com.photodemo.CameraPreview
        android:id="@+id/cameraPreview"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <View
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:layout_gravity="top"
            android:background="#44000000" />

        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="210dp"
            android:orientation="horizontal">

            <View
                android:layout_gravity="start"
                android:layout_width="50dp"
                android:layout_height="match_parent"
                android:background="#44000000" />

            <FrameLayout
                android:background="@drawable/capture"
                android:layout_marginLeft="50dp"
                android:layout_marginRight="50dp"
                android:layout_width="match_parent"
                android:layout_height="match_parent">

                <View
                    android:id="@+id/line"
                    android:background="@drawable/scan_line"
                    android:layout_width="match_parent"
                    android:layout_height="3dp"/>

            </FrameLayout>

            <View
                android:layout_gravity="end"
                android:layout_width="50dp"
                android:layout_height="match_parent"
                android:background="#44000000" />

        </FrameLayout>

        <TextView
            android:layout_weight="1"
            android:paddingTop="40dp"
            android:textColor="#ffffff"
            android:textSize="20sp"
            android:gravity="center_horizontal"
            android:text="请将身份证放置到扫描区"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_gravity="bottom"
            android:background="#44000000" />
    </LinearLayout>
</FrameLayout>

这里需要说明下的是中间的横线上下移动就是一个简单的动画效果

TranslateAnimation mAnimation = new TranslateAnimation(
                        TranslateAnimation.ABSOLUTE, 0f, TranslateAnimation.ABSOLUTE,
                        0f, TranslateAnimation.RELATIVE_TO_PARENT, 0f,
                        TranslateAnimation.RELATIVE_TO_PARENT, 1f);
                mAnimation.setDuration(1500);
                mAnimation.setRepeatCount(-1);
                mAnimation.setRepeatMode(Animation.REVERSE);
                mAnimation.setInterpolator(new LinearInterpolator());
                line.setAnimation(mAnimation);

XML中的CameraPreview,它是一个自定义相机预览类,来看一下它的继承关系

public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback 

关于SurfaceView这里简单说下,它就类似一个View,但是和View不同的点在于,它允许在子线程中进行绘制,而普通的View是只能在UI线程中执行绘制工作,所以SurfaceView比较适合进行一些繁重的绘制工作而不会造成app的卡顿,这里因为需要不断将摄像头的每一帧数据都展示出来所以使用SurfaceView是非常合适的, SurfaceHolder.Callback 就是一个接口内部有三个生命周期方法用来控制SurfaceView

        public void surfaceCreated(SurfaceHolder holder);
        public void surfaceChanged(SurfaceHolder holder, int format, int width,
                int height);
        public void surfaceDestroyed(SurfaceHolder holder);

大概看方法名字大家也能猜到这些方法的调用场景,具体的使用在代码中等下可以看见,现在就让我们具体看一下代码的实现部分

CameraPreview构造函数

public CameraPreview(Context context) {
        this(context, null);
    }

    public CameraPreview(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    @SuppressWarnings("deprecation")
    public CameraPreview(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        holder = getHolder();
        holder.addCallback(this);
        holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
    }

通过holder来注册Callback生命周期回调方法,holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);的作用就是将摄像头中的数据放置到
SurfaceView中去呈现

surfaceCreated

 @SuppressWarnings("deprecation")
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        try {
            Log.e("mandy", "surfaceCreated");
            camera = Camera.open(0);//打开后置摄像头
            autoFocusHandler = new AutoFocusHandler(Looper.getMainLooper());
            decodeHandlerThread = new HandlerThread("decodePic");
            decodeHandlerThread.start();
            decodePicHandler = new DecodePicHandler(decodeHandlerThread.getLooper());
            oneShotPreviewEnable = true;//每次获取预览数据设置为true,获取完毕置为false
            init(camera);
            autoFocusHandler.sendEmptyMessageDelayed(AUTO_FOCUS, AUTO_FOCUS_INTERVAL);
        } catch (Exception e) {
            e.printStackTrace();
            Log.e("mandy", "崩溃");
        }
    }

执行完构造函数后,当SurfaceView最终展示出来的时候就会调用到surfaceCreated方法,我们可以在这里做一些初始化的工作,AutoFocusHandler主要是为了自动获取聚焦效果,因为当我们在预览的时候摄像头是不会自动聚焦的,这样造成的问题就是预览效果模糊需要不停的聚焦才能预览到清晰的画面,这就是这个类的工作,自动聚焦的代码很简单来看下如何实现

@SuppressWarnings("deprecation")
    private class AutoFocusCallBack implements Camera.AutoFocusCallback {

        @Override
        public void onAutoFocus(boolean success, Camera camera) {
            Log.e("mandy", "onAutoFocus==" + success);
            autoFocusHandler.sendEmptyMessageDelayed(AUTO_FOCUS, AUTO_FOCUS_INTERVAL);
        }
    }

当摄像头每次聚焦都会调用到onAutoFocus方法,在这个方法内部发送一个延时请求,一定延时后就再次聚焦保证预览画面不会出现糊的感觉。AutoFocusCallBack分析完毕,回到created方法中

 decodeHandlerThread = new HandlerThread("decodePic");
            decodeHandlerThread.start();
            decodePicHandler = new DecodePicHandler(decodeHandlerThread.getLooper());

因为需要将预览的画面数据获取到经过处理后上传给服务端,这本身就是一个耗时的操作,所以使用HandlerThread新起一个线程完成这项任务,DecodePicHandler就是用来获取到的预览画面

private class DecodePicHandler extends Handler {

        private DecodePicHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case DECODE_PIC:
                    try {
                        Log.e("mandy", "DECODE_PIC");
                        byte[] data = (byte[]) msg.obj;

                        YuvImage yuvimage = new YuvImage(data, ImageFormat.NV21, camera.getParameters().getPreviewSize().width, camera.getParameters().getPreviewSize().height, null);
                        ByteArrayOutputStream baos = new ByteArrayOutputStream();
                        yuvimage.compressToJpeg(new Rect(0, 0, camera.getParameters().getPreviewSize().width, camera.getParameters().getPreviewSize().height), 80, baos);

                        if (resultProcessor != null) {
                            resultProcessor.resultProcess(baos.toByteArray());
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                        Log.e("mandy", "崩溃");
                    }
                    break;
                default:

                    break;
            }
        }
    }

byte[] data = (byte[]) msg.obj;就是获取到的预览画面数据,如果直接将data经过BitmapFactory的decode方法处理,会发现无法获取到bitmap,因为摄像头获取的data数据格式和BitmapFactory需要的格式不一致,所以这里就需要使用YuvImage进行一次转换,resultProcessor就一个简单的回调接口供使用CarmaPreview的activity使用,至于拿到这些数据做什么就看需求了。

            init(camera);
            autoFocusHandler.sendEmptyMessageDelayed(AUTO_FOCUS, AUTO_FOCUS_INTERVAL);

init方法作用就是对相机的参数进行设置,获取最佳的预览效果,这部分的代码参考zxing中的写法,代码略多就不贴出来了,主要说下关键的部分

this.camera.setDisplayOrientation(90);
...
parameters.setPreviewSize(cameraResolution.x, cameraResolution.y);
setZoom(parameters);

摄像头旋转90度,不然预览画面是横着的。parameters.setPreviewSize(cameraResolution.x, cameraResolution.y);设置预览的分辨率吧,如果不进行预览分辨率的设置你会发现有些手机的预览画面会产生形变,具体有哪些预览分辨率可以选择调用下parameters.getSupportedPreviewSizes();就知道了,关于如何处理形变网上有现有的解决思路大家可以参考下http://blog.csdn.net/u013560890/article/details/49338031,实际上zxing的处理就是类似的思想。
setZoom用来处理摄像头放大倍数,具体怎么设置直接看zxing源码吧,这样created方法就分析完毕了。

surfaceChanged

调用完created方法后紧接着会调用到该方法,个人理解有点类似于activity的onresume方法,都是紧接在create方法之后调用的,里面逻辑简单,关键部分就是开启预览模式,没什么好说的

           if (camera != null) {
                camera.stopPreview();
                camera.setPreviewDisplay(this.holder);
                camera.startPreview();
            }

surfaceDestroyed

这个方法调用需要稍微说一下的是并不是只有在activiity销毁的时候才会调用,在点击手机home键时该方法也会调用到,在这里需要做好资源的释放工作及时释放掉相机,关键代码如下

if (camera != null) {
            camera.autoFocus(null);
            camera.setPreviewCallback(null);
            camera.setOneShotPreviewCallback(null);
            camera.stopPreview();
            camera.release();
            camera = null;
        }
        if (autoFocusHandler != null) {
            autoFocusHandler.removeCallbacksAndMessages(null);
        }
        if (decodeHandlerThread != null) {
            decodeHandlerThread.quit();
        }
        if (decodePicHandler != null) {
            decodePicHandler.removeCallbacksAndMessages(null);
        }

上述的分析大致就是自定义相机预览类的关键代码了,剩下的修修补补自己调试下基本都能解决掉,个人在实现自定义相机预览控件的时候遇到的比较麻烦的就是预览画面模糊问题解决,预览画面形变问题解决,这两个问题在上述代码都能找到解决方法。

相关文章

网友评论

    本文标题:使用SurfaceView自定义相机预览控件

    本文链接:https://www.haomeiwen.com/subject/otggzxtx.html