基于zxing的二维码扫描

前言

现在的应用中二维码扫描已经成为一个应用必不可少的功能,现在大部分Android二维码扫描都是基于zxing和Zbar,这文章就来介绍一下基于zxing的二维码扫描。先看下效果图

效果图

虽然现在的关于二维码的文章有很多,但是很多都是交我们怎么使用,在使用的时候我也遇到了很多问题,比如。UI界面太丑,没有用的文件太多,扫描太慢,版本太老。本Demo使用的是3.x的是比较新的版本。我用的小米2A(api=19)亲测在正常情况下扫描时间和QQ微信差不多,我的界面是类防QQ的也是比较美观的。那么我们看看如何使用和文件的作用。


zxing

zxing官网 这是zxing官网。想了解更多的可以去官网,里面有文档不过在我看来。第三方库的使用我们没有必要完全了解,整体上我们了解所需要功能即可,这样减少了学习时间。


为什么选择zxing

  • google的开源项目,高可定制性
  • 可以识别多种码,不仅仅是二维码
  • 不依赖第三方库,使用起来简单

zxing的使用

  • zixng JAR
    我们可以去官网,如果是Android Studio的话也可以在线搜索zxing jar,导入完成后别忘了ADD library。

  • res文件

    res文件

    • drawable
      文件中主要放的是一些我们显示界面的图片和一些点击按钮的background

    • layout
      activity_qrcode_capture_layout.xml是zxing扫描的主界面,另外两个布局就是我们在开始图片中看到的,一个头部,一个脚部的布局

    • colors,dis,strings,styles,raw,xml
      这些是zxing中一些类的资源文件和我们自定义布局的一些资源文件,不导入会报错,raw是我们扫描完成后的音效,我们也可以根据需求改成自己的音效(但是要注意的是文件格式和名字尽量要相同,避免出错和资源找不到),xml就是zxing用到的资源文件,我们直接复制过来就行。

      PS:如果你是从官网拷贝,那么你自需要拷贝和我一样的就行。我这里只是多了一些drawwable中的布局图片和头部脚部2个布局

  • 关键类

    关键类
    • app
      CaptureActivity 主要是我们的扫码界面,在这里我们引入我们自己的头部脚部布局,并给控件点击事件,在这个类中我们重点看这几个方法:
    /**
     * 闪光灯点击事件
     */
    private OnClickListener click = new OnClickListener() {

        @Override
        public void onClick(View v) {
            int id = v.getId();
            if (id == R.id.button_back) {//返回按钮
                finish();
            } else if (id == R.id.flash_btn) {//打开关闭闪光灯
                if (!isFlash) {
                    CameraManager.get().turnLightOn();
                } else {
                    CameraManager.get().turnLightOff();
                }
                isFlash = !isFlash;
            } else if (id == R.id.photo_btn) {//扫描二维码图片
                // 打开手机中的相册
                Intent innerIntent = new Intent(Intent.ACTION_GET_CONTENT); // "android.intent.action.GET_CONTENT"
                innerIntent.setType("image/*");
                //封装intent
                Intent wrapperIntent = Intent.createChooser(innerIntent, "选择二维码图片");
                startActivityForResult(wrapperIntent, REQUEST_CODE);
            } else if (id == R.id.qrcode_btn) {
                // 跳转到生成二维码页面
                Bitmap b = createQRCode();
                Intent intent = getIntent();
                intent.putExtra("QR_CODE", b);
                setResult(200, intent);
                finish();
            }

        }
    };

所有扫码界面的点击事件都在这个Activity中,可以看到zxing给我做了比较好的封装,只需要2行代码我们就可以控制闪光灯的开关。扫描图片二维码的点击事件也比较简单我们自需要打开相册。并用startActivityForResult启动相册。

 @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode == RESULT_OK) {//图片选择返回
            uri = data.getData();//获取图片Uri
            //启动线程完成图片扫码
            new Thread(new Runnable() {
                @Override
                public void run() {

                    Result result = scanningImage(uri);
                    if (result == null) {
                        Looper.prepare();
                        Toast.makeText(getApplicationContext(), "图片格式有误", Toast.LENGTH_SHORT).show();
                        Looper.loop();
                    } else {
                        // 数据返回,在这里去处理扫码结果
                        String recode = (result.toString());
                        Intent data = new Intent();
                        data.putExtra("result", recode);
                        //返回启动扫码界面的Activity
                        setResult(300, data);
                        finish();
                    }
                }
            }).start();
        }
    }

这个方法主要是处理上个方法打开相册选取图片后结果返回的处理。可以看到,我们拿到结果将扫码扫码界面finish()掉,并通过setResult()方法将数据交给跳转我们的扫码界面的活动去处理。 下面我们再来看下生成二维码

  • 生成二维码:可以看到生成二维码主要是调用了createQRCode();这个方法,那我来看下这个方法:
private Bitmap createQRCode() {
        int QR_WIDTH = 100;//生成二维码的宽
        int QR_HEIGHT = 100;//生成二维码的高

        try {
            // 需要引入core包
            QRCodeWriter writer = new QRCodeWriter();
            String text = Util.getIMEI(this);
            if (text == null || "".equals(text) || text.length() < 1) {
                return null;
            }
            // 把输入的文本转为二维码
            BitMatrix martix = writer.encode(text, BarcodeFormat.QR_CODE, QR_WIDTH, QR_HEIGHT);

            System.out.println("w:" + martix.getWidth() + "h:" + martix.getHeight());

            Hashtable<EncodeHintType, String> hints = new Hashtable<EncodeHintType, String>();
            hints.put(EncodeHintType.CHARACTER_SET, "utf-8");
            BitMatrix bitMatrix = new QRCodeWriter().encode(text, BarcodeFormat.QR_CODE, QR_WIDTH, QR_HEIGHT, hints);
            int[] pixels = new int[QR_WIDTH * QR_HEIGHT];
            for (int y = 0; y < QR_HEIGHT; y++) {
                for (int x = 0; x < QR_WIDTH; x++) {
                    if (bitMatrix.get(x, y)) {
                        pixels[y * QR_WIDTH + x] = 0xff000000;
                    } else {
                        pixels[y * QR_WIDTH + x] = 0xffffffff;
                    }
                }
            }
            // 生成的二维码
            Bitmap bitmap = Bitmap.createBitmap(QR_WIDTH, QR_HEIGHT, Bitmap.Config.ARGB_8888);
            bitmap.setPixels(pixels, 0, QR_WIDTH, 0, 0, QR_WIDTH, QR_HEIGHT);

            return bitmap;
        } catch (WriterException e) {
            e.printStackTrace();
        }
        return null;
    }

它主要就是将一个文本生成了一个宽高为100*100的bitmap。那我们就把这个方法简单就修改下,宽高由我们传入。文本也是由我们自己来决定。因为生成二维码并不依赖扫描的Activity,所以不管在那里我们只要调用createQRcode,就能生成二维码。

  • camera 主要是一些相机的管理类,FlashlightManager闪光灯的管理类,CameraManager相机的管理类,比如刚才我们的开关闪光灯。
  • decode 这里面主要是解码,因为解码也是比较耗时的炒作,所以我们是放在线程中去执行并通过handle来进行消息传递。这里重要的类是CaptureActivityHandler。处理的结果都是通过这个类传给我们的Activity的。既然是handle,那么我们就来看下handleMessage()方法
 @Override
    public void handleMessage(Message message) {
        if (message.what == R.id.auto_focus) {
            // Log.d(TAG, "Got auto-focus message");
            // When one auto focus pass finishes, start another. This is the
            // closest thing to
            // continuous AF. It does seem to hunt a bit, but I'm not sure what
            // else to do.
            if (state == State.PREVIEW) {
                CameraManager.get().requestAutoFocus(this, R.id.auto_focus);
            }
        } else if (message.what == R.id.restart_preview) {
            Log.d(TAG, "Got restart preview message");
            restartPreviewAndDecode();
        } else if (message.what == R.id.decode_succeeded) {
            Log.d(TAG, "Got decode succeeded message");
            state = State.SUCCESS;
            Bundle bundle = message.getData();
            Bitmap barcode = bundle == null ? null : (Bitmap) bundle.getParcelable(DecodeThread.BARCODE_BITMAP);
            activity.handleDecode((Result) message.obj, barcode);
            Result result = (Result) message.obj;
            Intent mIntent = new Intent();
            mIntent.putExtra("SCAN_RESULT", result.getText());
            activity.setResult(Activity.RESULT_OK, mIntent);
            activity.finish();
        } else if (message.what == R.id.decode_failed) {
            // We're decoding as fast as possible, so when one decode fails,
            // start another.
            state = State.PREVIEW;
            CameraManager.get().requestPreviewFrame(decodeThread.getHandler(), R.id.decode);
        } else if (message.what == R.id.return_scan_result) {
            Log.d(TAG, "Got return scan result message");
            activity.setResult(Activity.RESULT_OK, (Intent) message.obj);
            activity.finish();
        } else if (message.what == R.id.launch_product_query) {
            Log.d(TAG, "Got product query message");
            String url = (String) message.obj;
            Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
            intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
            activity.startActivity(intent);
        }
    }

可以看到处理的结果都在这里。有成功失败和其他一些情况的处理,这里我们主要看成功时会返回resultCode=RESULT_OK,扫描的结果放在inent中key=SCAN_RESULT,这样我们就可以根据intent携带的key和value去做们相应的处理

BeepManager是控制我们的消息音。

BeepManager manager = new BeepManager(activity);//开启提示音
            manager.playBeepSoundAndVibrate();
  • encode,util,就是编码和工具类,这里我们就不看了
  • view中我们重点来看下ViewfinderView。这个就是我们扫码界面的主布局。他和我们自己添加的头部和脚部不同,它不是通过引入布局,而是自定义view画上去的。既然是画上去的那么就去看看onDraw()方法做了什么
 @Override
    public void onDraw(Canvas canvas) {
        /**
         * 想修改扫描框的位置修改CameraManager中的参数
         */
        Rect frame = CameraManager.get().getFramingRect();
        if (frame == null) {
            return;
        }

        if (!isFirst) {
            isFirst = true;
            slideTop = frame.top;
            slideBottom = frame.bottom;
        }
        int width = canvas.getWidth();
        int height = canvas.getHeight();
        /**
         * 画白线矩形
         */

        paint.setStyle(Paint.Style.STROKE);
        paint.setColor(Color.WHITE);
        path.moveTo(frame.left + CORNER_OFFEST, frame.top + CORNER_OFFEST);
        path.lineTo(frame.right - CORNER_OFFEST, frame.top + CORNER_OFFEST);
        path.lineTo(frame.right - CORNER_OFFEST, frame.bottom - CORNER_OFFEST);
        path.lineTo(frame.left + CORNER_OFFEST, frame.bottom - CORNER_OFFEST);
        path.close();
        canvas.drawPath(path, paint);

        // Draw the exterior (i.e. outside the framing rect) darkened
        paint.setColor(resultBitmap != null ? resultColor : maskColor);
        paint.setStyle(Paint.Style.FILL);
        /**
         * 画上下左右4个位置的半透明黑色布局
         */
        canvas.drawRect(0, 0, width, frame.top, paint);
        canvas.drawRect(0, frame.top, frame.left, frame.bottom + 1, paint);
        canvas.drawRect(frame.right + 1, frame.top, width, frame.bottom + 1,
                paint);
        canvas.drawRect(0, frame.bottom + 1, width, height, paint);

        if (resultBitmap != null) {
            // Draw the opaque result bitmap over the scanning rectangle
            paint.setAlpha(OPAQUE);
            canvas.drawBitmap(resultBitmap, null, frame, paint);
        } else {
            /**
             * 画4个角的小矩形,每个角2个矩形 共8个
             */
            paint.setColor(getResources().getColor(R.color.view_rect));
            canvas.drawRect(frame.left, frame.top, frame.left + ScreenRate,
                    frame.top + CORNER_WIDTH, paint);
            canvas.drawRect(frame.left, frame.top, frame.left + CORNER_WIDTH,
                    frame.top + ScreenRate, paint);
            canvas.drawRect(frame.right - ScreenRate, frame.top, frame.right,
                    frame.top + CORNER_WIDTH, paint);
            canvas.drawRect(frame.right - CORNER_WIDTH, frame.top, frame.right,
                    frame.top + ScreenRate, paint);
            canvas.drawRect(frame.left, frame.bottom - CORNER_WIDTH, frame.left
                    + ScreenRate, frame.bottom, paint);
            canvas.drawRect(frame.left, frame.bottom - ScreenRate, frame.left
                    + CORNER_WIDTH, frame.bottom, paint);
            canvas.drawRect(frame.right - ScreenRate, frame.bottom
                    - CORNER_WIDTH, frame.right, frame.bottom, paint);
            canvas.drawRect(frame.right - CORNER_WIDTH, frame.bottom
                    - ScreenRate, frame.right, frame.bottom, paint);

            /**
             * 定义每秒的移动速度
             */
            slideTop += SPEEN_DISTANCE;
            if (slideTop >= frame.bottom) {
                slideTop = frame.top;
            }
            lineRect.left = frame.left;
            lineRect.right = frame.right;
            lineRect.top = slideTop;
            lineRect.bottom = slideTop + 18;
            canvas.drawBitmap(((BitmapDrawable) (getResources()
                            .getDrawable(R.drawable.fle))).getBitmap(), null, lineRect,
                    paint);

            paint.setColor(Color.WHITE);
            paint.setTextSize(TEXT_SIZE * density);
            paint.setAlpha(0x40);
            paint.setTypeface(Typeface.create("System", Typeface.BOLD));
            String text = getResources().getString(R.string.scan_text);
            float textWidth = paint.measureText(text);

            canvas.drawText(
                    text,
                    (width - textWidth) / 2,
                    (float) (frame.bottom + (float) TEXT_PADDING_TOP * density),
                    paint);

            Collection<ResultPoint> currentPossible = possibleResultPoints;
            Collection<ResultPoint> currentLast = lastPossibleResultPoints;
            if (currentPossible.isEmpty()) {
                lastPossibleResultPoints = null;
            } else {
                possibleResultPoints = new HashSet<ResultPoint>(5);
                lastPossibleResultPoints = currentPossible;
                paint.setAlpha(OPAQUE);
                paint.setColor(resultPointColor);
                for (ResultPoint point : currentPossible) {
                    canvas.drawCircle(frame.left + point.getX(), frame.top
                            + point.getY(), 6.0f, paint);
                }
            }
            if (currentLast != null) {
                paint.setAlpha(OPAQUE / 2);
                paint.setColor(resultPointColor);
                for (ResultPoint point : currentLast) {
                    canvas.drawCircle(frame.left + point.getX(), frame.top
                            + point.getY(), 3.0f, paint);
                }
            }

            //不断重绘 完成光标移动
            postInvalidateDelayed(ANIMATION_DELAY, frame.left, frame.top,
                    frame.right, frame.bottom);
        }
    }

注释比较清晰,掌握基本的自定义View只是就可以画出来我们展示的效果。
最后我们在看下我们MainActivity的调用:

 @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == 200) {
            if (resultCode == Activity.RESULT_OK) {
                //还记得我们在讲CaptureActivityHandler时 成功就会发送SCAN_RESULT这个key值的intent嘛?
                String code = data.getStringExtra("SCAN_RESULT");
                Log.d(TAG, "onActivityResult:----> " + code);
                if (code.contains("http") || code.contains("https")) {
                    //二维码
                    mTextView.setText(code);
                } else if ((!code.contains("http") || !code.contains("https")) && code != null && TextUtils.isEmpty(code)) {
                    //条形码数字
                //这里注意:如果你扫描的是商品条形码。返回的条形码数字
                //这里如果你使用webview是无法解析的,一般我们需要查询
                //的api接口或是数据库查找才能展示我们想要的结果 
                    mTextView.setText(code);
                } else {
                    Toast.makeText(this, "error", Toast.LENGTH_SHORT).show();
                }
            }
            if (resultCode == 300) {
                //扫描图片
                String code = data.getStringExtra("result");
                Log.d(TAG, "onActivityResult:---->result " + code);
                mTextView.setText(code);
            }
            if (resultCode == 200) {
                //生成二维码回调
                Bitmap bitmap = Util.createQRCode(dip2px(this, 200), dip2px(this, 200), "https://www.baidu.com/");
                mImageView.setImageBitmap(bitmap);
            }
        }
    }

好了,zxing的使用和相关类的功能我们有了一个大致的了解。回头我们在看看zxing的优点:可以看到封装性比较好我们不需要多做什么处理,我们需要什么功能就在基础上加什么功能,源码都给我们了,我们也知道每个类是什么作用还不是想怎么改怎么改嘛(高可定制性),还有在使用第三方库的时候我们比较担心的就是我们需要一个库的功能,但是这个库却依赖很多其他的库,当其他库发生改动的时候我们需要的库也要改这就很烦。


结束

在这里我只是做一个抛砖引玉的作用,不管你想要什么样的UI还是什么样的布局,我在方法中加了注释不喜欢那里将代码删除写上自己想要的效果就好。毕竟我还是个小白菜鸟。如果你也在学习的路上,想在你自己应用加上扫码功能,希望这篇文章能给你一些帮助。如果像直接使用下面我给出了源码,可以直接放入项目中也可导入library。
写的不好大家多多谅解。如有错误真心希望大家提出来。最后希望大家一起进步。加油!!!


源码地址

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

推荐阅读更多精彩内容