资讯专栏INFORMATION COLUMN

Android 扫描二维码的实现(简化zxing)

bingchen / 1636人阅读

摘要:哎呀呀,在杭州的现场,因为没有二维码签到功能,被吐槽这是我近期最丢脸的事啦于是回来就开始着手开发二维码相关的东西了。对获取的一帧图片进行解析。对解析的结果进行处理。

zxing

哎呀呀,在杭州2015 Hackthon的现场,因为没有二维码签到功能,被吐槽low!这是我近期最丢脸的事啦~
于是回来就开始着手开发二维码相关的东西了。

一搜索Google和我们SegmentFault,发现在Android上,Googlezxing这个二维码的库比较受欢迎,好,那就是它了(我就是这么任性= =)

OK,看下zxing这个项目https://github.com/zxing/zxing
好像非常大啊,如何快速用起来呢?

答案就是

简化!

简化的话,带来的副作用就是适用性降低,比如在这个场景里面,我们不考虑横屏的情况,不考虑对摄像头进行过多的配置,不存在截图。

简化过程

我们看下这个项目有一个android目录,它里面其实就是条码扫描器这个app的开源代码,因为我们暂时不需要深究它是如何进行图像识别,如何帮我们从图像里解析出二维码的,所以我们就不研究core相关的内容(但是里面都是干货啊!图像识别的干货啊!)


我们最主要就是参考这个里面的源码啦。

先把整个项目clone下来,然后把android这个源码导入到Android Studio里面去,经过小小的配置,就能搞定啦,直接运行我们就能看见这个App了。

先看下我们要做的工作是啥:

对摄像头进行管理(为了兼容老设备,我们要用即将被Google舍弃的android.hardware.camera类)。

对获取的一帧图片进行解析。

对解析的结果进行处理。

如果不带着问题或者目的去看源码的话,会非常没头绪,所以我们一定要记得我们想要做什么。

OK,先看第一个问题,我们需要对摄像头进行管理。我们可以看到目录中有三个文件和摄像机有关


就是这三个文件啦。

用途
CameraConfigurationManager 这个类主要用于对摄像头进行一些配置,包括旋转角度、预览图尺寸等等
CameraConfigurationUtils 工具类,在CameraConfigurationManager调用,
CameraManager 控制摄像头的生命周期,获取预览尺寸,生成原始数据发送给解析器

在这里,我把CameraConfigurationUtils拷贝过来,另外两个类合成到CameraManager里面,在初始化的时候,做好配置。

摄像头的管理(横屏改竖屏)

因为原先zxing给的demo是横屏的,这里要改成竖屏,就需要做几个配置。

getFramingRectInPreview

getFramingRectInPreview这个函数中,cameraResolutionscreenResolution方向不对,所以

rect.left = rect.left * cameraResolution.y / screenResolution.x;
rect.right = rect.right * cameraResolution.y / screenResolution.x;
rect.top = rect.top * cameraResolution.x / screenResolution.y;
rect.bottom = rect.bottom * cameraResolution.x / screenResolution.y;

这里需要把方向换一下。

binary data

还需要更改的地方,就是在获取帧预览中的原始数据,需要进行一个旋转,因为zxing原先是对横向的图片一行一行读取的,如果我们给予纵向的数据,就必须旋转数据,或者更改读取算法。 这里更改数据可能会更加方便,
buildLuminanceSource中,更改:

byte[] rotatedData = new byte[data.length];
for (int y = 0; y < height; y++) {
    for (int x = 0; x < width; x++)
        rotatedData[x * height + height - y - 1] = data[x + y * width];
}
int tmp = width;
width = height;
height = tmp;

最后记得把摄像头的预览旋转改成90°即可。

 camera.setDisplayOrientation(90);

附上这部分代码(比较长)

public class CameraManager {

    private static final int MIN_FRAME_WIDTH = 240;
    private static final int MIN_FRAME_HEIGHT = 240;

    // 这里修正扫描大小
    private static final int MAX_FRAME_WIDTH = 675; // = 5/8 * 1920
    private static final int MAX_FRAME_HEIGHT = 1200; // = 5/8 * 1080

    private Context mContext;
    private Point mScreenResolution;
    private Point mCameraResolution;
    private Point mBestPreviewSize;
    private Point mPreviewSizeOnScreen;

    private Rect mFramingRectInPreview;
    private Rect mFramingRect;

    private Camera mCamera;
    private PreviewCallback mPreviewCallback;
    private AutoFocusManager mAutoFocusManager;

    public CameraManager(Context context) {
        mContext = context;
        mPreviewCallback = new PreviewCallback(this);
    }

    public void openDriver(Camera camera, SurfaceHolder holder) {
        mCamera = camera;

        Camera.Parameters parameters = camera.getParameters();

        CameraConfigurationUtils.setBarcodeSceneMode(parameters);
        CameraConfigurationUtils.setFocus(parameters,
                true,
                true,
                false);

        Point theScreenResolution = new Point();
        Rect rect = holder.getSurfaceFrame();
        theScreenResolution.set(rect.height(), rect.width());
        mScreenResolution = theScreenResolution;
        mCameraResolution = CameraConfigurationUtils.findBestPreviewSizeValue(parameters, mScreenResolution);
        mBestPreviewSize = CameraConfigurationUtils.findBestPreviewSizeValue(parameters, mScreenResolution);

        boolean isScreenPortrait = mScreenResolution.x < mScreenResolution.y;
        boolean isPreviewSizePortrait = mBestPreviewSize.x < mBestPreviewSize.y;

        if (isScreenPortrait == isPreviewSizePortrait) {
            mPreviewSizeOnScreen = mBestPreviewSize;
        } else {
            mPreviewSizeOnScreen = new Point(mBestPreviewSize.y, mBestPreviewSize.x);
        }

        parameters.setPreviewSize(mBestPreviewSize.x, mBestPreviewSize.y);
        camera.setParameters(parameters);
        camera.setDisplayOrientation(90);

        Camera.Parameters afterParameters = camera.getParameters();
        Camera.Size afterSize = afterParameters.getPreviewSize();
        if (afterSize != null && (mBestPreviewSize.x != afterSize.width || mBestPreviewSize.y != afterSize.height)) {
            mBestPreviewSize.x = afterSize.width;
            mBestPreviewSize.y = afterSize.height;
        }

    }

    public synchronized Rect getFramingRect() {
        if (mFramingRect == null) {
            Point screenResolution = mScreenResolution;
            if (screenResolution == null) {
                // Called early, before init even finished
                return null;
            }

            int width = findDesiredDimensionInRange(screenResolution.x, MIN_FRAME_WIDTH, MAX_FRAME_WIDTH);
            int height = findDesiredDimensionInRange(screenResolution.y, MIN_FRAME_HEIGHT, MAX_FRAME_HEIGHT);

            int leftOffset = (screenResolution.x - width) / 2;
            int topOffset = (screenResolution.y - height) / 2;
            mFramingRect = new Rect(leftOffset, topOffset, leftOffset + width, topOffset + height);
        }
        return mFramingRect;
    }

    public Point getCameraResolution() {
        return mCameraResolution;
    }

    public synchronized Rect getFramingRectInPreview() {
        if (mFramingRectInPreview == null) {
            Rect framingRect = getFramingRect();
            if (framingRect == null) {
                return null;
            }
            Rect rect = new Rect(framingRect);
            Point cameraResolution = mCameraResolution;
            Point screenResolution = mScreenResolution;
            if (cameraResolution == null || screenResolution == null) {
                // Called early, before init even finished
                return null;
            }
            rect.left = rect.left * cameraResolution.y / screenResolution.x;
            rect.right = rect.right * cameraResolution.y / screenResolution.x;
            rect.top = rect.top * cameraResolution.x / screenResolution.y;
            rect.bottom = rect.bottom * cameraResolution.x / screenResolution.y;
            mFramingRectInPreview = rect;
        }
        return mFramingRectInPreview;
    }

    private static int findDesiredDimensionInRange(int resolution, int hardMin, int hardMax) {
        int dim = 5 * resolution / 8; // Target 5/8 of each dimension
        if (dim < hardMin) {
            return hardMin;
        }
        if (dim > hardMax) {
            return hardMax;
        }
        return dim;
    }

    /**
     * A factory method to build the appropriate LuminanceSource object based on the format
     * of the preview buffers, as described by Camera.Parameters.
     *
     * @param data A preview frame.
     * @param width The width of the image.
     * @param height The height of the image.
     * @return A PlanarYUVLuminanceSource instance.
     */
    public PlanarYUVLuminanceSource buildLuminanceSource(byte[] data, int width, int height) {
        Rect rect = getFramingRectInPreview();
        if (rect == null) {
            return null;
        }

        byte[] rotatedData = new byte[data.length];
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++)
                rotatedData[x * height + height - y - 1] = data[x + y * width];
        }
        int tmp = width;
        width = height;
        height = tmp;

        // Go ahead and assume it"s YUV rather than die.
        return new PlanarYUVLuminanceSource(rotatedData, width, height, rect.left, rect.top,
                rect.width(), rect.height(), false);
    }

    /**
     * A single preview frame will be returned to the handler supplied. The data will arrive as byte[]
     * in the message.obj field, with width and height encoded as message.arg1 and message.arg2,
     * respectively.
     *
     * @param handler The handler to send the message to.
     * @param message The what field of the message to be sent.
     */
    public synchronized void requestPreviewFrame(Handler handler, int message) {
        if (mCamera != null) {
            mPreviewCallback.setHandler(handler, message);
            mCamera.setOneShotPreviewCallback(mPreviewCallback);
        }
    }

    public synchronized void startPreview() {
        mCamera.startPreview();
        mAutoFocusManager = new AutoFocusManager(mContext, mCamera);
    }

    public synchronized void stopPreview() {
        if (mAutoFocusManager != null) {
            mAutoFocusManager.stop();
            mAutoFocusManager = null;
        }
        if (mCamera != null) {
            mCamera.stopPreview();
        }
        mPreviewCallback.setHandler(null, 0);

    }

    public synchronized void closeDriver() {
        mCamera.release();
        mCamera = null;
    }
}
获取图像数据

Camera提供这么一个函数:setOneShotPreviewCallback,看名字就知道,它是会在某个时间点,回调你给的接口,然后传入一个二进制的图像数组,让你去解析,这正是我们想要的东西,所以我们看下这个PreviewCallback,它只有一个函数,

@Override
public void onPreviewFrame(byte[] data, Camera camera) {
}

很显然,第一个参数就是图像数组了,我们看看zxing里面是怎么处理这个数据的。

解析数据
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
    Point cameraResolution = mCameraManager.getCameraResolution();
    Handler thePreviewHandler = mPreviewHandler;
    if (cameraResolution != null && thePreviewHandler != null) {
        Message message = thePreviewHandler.obtainMessage(mPreviewMessage, cameraResolution.x,
                cameraResolution.y, data);
        message.sendToTarget();
        mPreviewHandler = null;
    } else {
    }
}

zxing是发送到另外一个handler,也就是说,这个解析数据的过程比较浪费时间,所以要开线程来解决这个事。
我们跟踪这个previewHanlder可以发现,它是一个DecodeHandler,其中有个重要的decode函数

private void decode(byte[] data, int width, int height) {
    long start = System.currentTimeMillis();
    Result rawResult = null;
    PlanarYUVLuminanceSource source = activity.getCameraManager().buildLuminanceSource(data, width, height);
    if (source != null) {
        BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
        try {
            rawResult = multiFormatReader.decodeWithState(bitmap);
        } catch (ReaderException re) {
            // continue
        } finally {
            multiFormatReader.reset();
        }
    }

    Handler handler = activity.getHandler();
    if (rawResult != null) {
        // Don"t log the barcode contents for security.
        long end = System.currentTimeMillis();
        Log.d(TAG, "Found barcode in " + (end - start) + " ms");
        if (handler != null) {
            Message message = Message.obtain(handler, R.id.decode_succeeded, rawResult);
            Bundle bundle = new Bundle();
            bundleThumbnail(source, bundle);
            message.setData(bundle);
            message.sendToTarget();
        }
    } else {
        if (handler != null) {
            Message message = Message.obtain(handler, R.id.decode_failed);
            message.sendToTarget();
        }
    }
}

这里用到我们刚刚重写的buildLuminanceSource这个函数了,可以理解这个函数是对我们的原始数据做一个包装,来给解析器读取。

解析器的配置我们可以看看DecodeThread这个类(专门用于解析图像的线程)

...
 decodeFormats = EnumSet.noneOf(BarcodeFormat.class);
decodeFormats.addAll(DecodeFormatManager.PRODUCT_FORMATS);
decodeFormats.addAll(DecodeFormatManager.INDUSTRIAL_FORMATS);
decodeFormats.addAll(DecodeFormatManager.QR_CODE_FORMATS);
decodeFormats.addAll(DecodeFormatManager.DATA_MATRIX_FORMATS);
mHints.put(DecodeHintType.POSSIBLE_FORMATS, decodeFormats);

这里我们可以为我们的扫描器加更多格式的支持(比如条形码、二维码、XX码)

zxing是使用List来管理这些解析器,然后每次读取数据,都经过这些解析器过滤一遍,如果解析成功就有结果,否则就没有结果。

最后我们跟踪R.id.decode_success找到CaptureActivityHandler
这里的handleMessage中有

  case R.id.decode_succeeded:
        state = State.SUCCESS;
        Bundle bundle = message.getData();
        Bitmap barcode = null;
        float scaleFactor = 1.0f;
        if (bundle != null) {
            byte[] compressedBitmap = bundle.getByteArray(DecodeThread.BARCODE_BITMAP);
            if (compressedBitmap != null) {
                barcode = BitmapFactory.decodeByteArray(compressedBitmap, 0, compressedBitmap.length, null);
                // Mutable copy:
                barcode = barcode.copy(Bitmap.Config.ARGB_8888, true);
            }
            scaleFactor = bundle.getFloat(DecodeThread.BARCODE_SCALED_FACTOR);
        }
        activity.handleDecode((Result) message.obj, barcode, scaleFactor);
        break;

我们读取Result对象的text成员变量就是我们想要的二维码信息了!最后的工作当然是解析我们的二维码中的URI,然后进行后续的工作啦~

PS: 附带二维码扫描的SegmentFault for Android版本 即将上线!!

欢迎关注我Github 以及 weibo、@Gemini

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/12413.html

相关文章

  • Android最佳实践(六)之扫描维码模块

    摘要:工厂模式该模块使用工厂模式,将实现和接口分离,同时为未来提供多样化的产品带来可能,用户可定制扫描二维码界面。 这是一个系列,我们将其命名为android最佳实践,如果你还没有看之前的文章: Android最佳实践(一) android最佳实践(二) android最佳实践(三) android最佳实践(四) android最佳实践(五) 上一节,我们编写了contact模块,极大的简化...

    王笑朝 评论0 收藏0
  • android基于ZXing和ZBar维码扫描项目

    摘要:更多库项目,提供基于和易于使用,可扩展的条码扫描器的用法。安装添加下面的依赖关系到你的文件。简单的用法添加摄像头权限进文件请看看一个完整的工作示例项目。 更多: http://www.codefrom.com/p/Android Android库项目,提供基于ZXing和ZBar易于使用,可扩展的条码扫描器的用法。 showImg(http://codefrom.oss-c...

    Airmusic 评论0 收藏0
  • zxing开源库基本使用

    摘要:如果你的项目中有模块跟二维码相关的话,那你一定听过或者用过大名鼎鼎的开源库。什么是是一个开源的,用实现的多种格式的条码图像处理库,它包含了联系到其他语言的端口。可以实现使用手机的内置的摄像头完成条形码的扫描及解码。如果你的项目中有模块跟二维码相关的话,那你一定听过或者用过大名鼎鼎的zxing开源库。 什么是zxing? ZXing是一个开源的,用Java实现的多种格式的1D/2D条码图像处理...

    trilever 评论0 收藏0
  • zxing 如何识别反转维码

    摘要:说起二维码扫描,估计很多人用的是吧。毕竟有些场景就需要用到反转二维码。所以本篇说的的坑就是无法识别反转二维码。通过交替识别可能会降低识别速度,因此次数设置为多少需要自己调试把控。计数时注意避免次数溢出。 说起二维码扫描,估计很多人用的是 zxing 吧。 然而 zxing 虽然好用,但是却有一些坑。 这边分析一下自己实际项目遇到的一个坑。 什么坑呢? 下面举个栗子你就懂了。 这边生成二维码使...

    Darkgel 评论0 收藏0
  • google zxing android扫描优化&解析

    摘要:下面代码是在中的,该方法参数中即取得的全部图像数据分别是图像的宽与高,就是从整张图片中取出所返回的矩形大小的图片数据。最后在对数据进行解析。解析说明完,我们就来说说简化修改后的情况。更多解析,这里不再缀述,具体过程看实现代码,点击下载。   这里先给出zxing包的源码地址      zip包:https://codeload.github.com/zxing/zxing/zip/maste...

    番茄西红柿 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<