Android 詳解使用 Zxing實現(xiàn)前置攝像頭掃描二維碼、生成二維碼

本文同步到CSDN

現(xiàn)在二維碼使用越來越廣泛了酸员,幾乎處處可見,并且 公司相關(guān)的項目中幾乎全部都和二維碼掃描有關(guān)讳嘱,所以總結(jié)一下自己的使用心路歷程幔嗦,總覺得要做點什么來記錄自己的成長,讓自己的成長有跡可循沥潭,如果恰好能夠幫助到你邀泉,我當(dāng)然會很開心啦,如果沒幫到钝鸽,請忽略汇恤。。

廢話結(jié)束拔恰,正文開始

小白之旅因谎,如有問題 望指正,萬分感謝 ????

首先推薦幾篇

Android 二維碼的掃碼功能實現(xiàn)

二維碼ZXING源碼分析(一)

zxing掃描二維碼和識別圖片二維碼及其優(yōu)化策略

Android中常用的就是 zxing 仁连,開源項目地址:https://github.com/zxing/zxing蓝角。首先我們下載項目到本地,然后加載到自己的工程中饭冬,可參考 我的上一片博文 AndroidStudio 導(dǎo)入 Zxing Android 項目使鹅,這個是作為庫文件導(dǎo)入的,當(dāng)然我們也可以單獨在工程中分出一個包昌抠,來實現(xiàn)掃描二維碼的功能患朱。

特別提醒: 如果作為 module 完整導(dǎo)入項目則配置好后就可以運行,如果單獨作為一個包炊苫,獨立出自己需要的內(nèi)容裁厅,需要復(fù)制 layout 文件冰沙,和資源文件 res-->values 里面的 ids.xml 和 res-->raw 里面的 beep.ogg 文件

一、了解 ZXing

image.png

ZXing 導(dǎo)入后执虹,所有的內(nèi)容如上圖所示拓挥,運行示例代碼,發(fā)現(xiàn)是橫屏用來掃描條形碼的袋励,包括識別相冊中的二維碼侥啤,掃描記錄,剪切板茬故,生成二維碼等功能盖灸,我們可以根據(jù)需要,分離出自己需要的那一部分磺芭,首先了解 ZXing 這個項目中各個部分的作用赁炎,然后開始 DELETE ??操作。

把 CaptureActivity 作為入口開始分析.....
(PS:不一定正確钾腺,是我自己的理解徙垫,不過 大概是這樣子的,如有失誤垮庐,后續(xù)會修正)

CaptureActivity: 打開相機并在后臺線程進行實際掃描松邪,繪制取景框,并進行圖像掃描反饋哨查。

CameraManager:相機管理類,調(diào)用相機 預(yù)覽剧辐,繪制掃描框的具體內(nèi)容都在這里寒亥。相關(guān)的相機的配置也在這里設(shè)置,例如前后攝像頭切換荧关,是否自動聚焦等溉奕。在 CaptureActivity 中,CameraManager 在 onResume() 中獲取對象忍啤,openDriver() 用來打開相機

CaptureActivityHandler: 處理所有的捕獲的狀態(tài)消息加勤,在 initCamera()中,打開相機后同波,創(chuàng)建該對象鳄梅,根據(jù)描述,應(yīng)該是傳遞消息的, 把需要解碼的內(nèi)容 傳遞給 然后 把結(jié)果返回到 CaptureActivity

DecodeThread: 處理最困難圖像解析工作未檩,包括解析和生成二維碼的內(nèi)容戴尸,和 DecodeHandler 搭檔

DecodeHandler: 把掃碼結(jié)果返回給 CaptureActivityHandler。

ViewfinderView:自定義的掃描界面冤狡,如果想繪制自己想要的掃描效果孙蒙,可以在這里動手??

大致是這樣子的项棠,從 CaptureActivity 入手,一點點看挎峦,就會明白是怎么回事香追,寫不明白的感覺。大家 也可以看 我這里實現(xiàn)的 DEMO 坦胶, 里面相應(yīng)的都給注釋了下透典。Github:

二、修改UI, 修改掃描頁面

大致的流程清楚后迁央,首先來最簡單的掷匠,把掃描的界面繪制成我們想要的樣子.

首先,把屏幕方向改為豎屏岖圈,相應(yīng)地 相機掃描方向也要旋轉(zhuǎn)讹语,
在 CameraManager 中 getFramingRectInPreview() 修改:

      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;

然后,在 DecodeHandler 里面 decode() 中添加:

// 旋轉(zhuǎn)攝像頭掃描方向后 處理 可以掃描二維碼 也可以掃描條形碼
    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;
    data = rotatedData;

    PlanarYUVLuminanceSource source = activity.getCameraManager().buildLuminanceSource(data, width, height);

我們想要的其實是一個正方形的掃描框蜂科,然后顽决,調(diào)整 ViewfinderView 繪制掃描框預(yù)覽界面,具體的可以在 ViewfinderView 的 onDraw() 方法中實現(xiàn)导匣,

Rect frame = cameraManager.getFramingRect();

想要繪制正方形才菠,getFramingRect() 獲取的預(yù)覽界面寬高,可以設(shè)置成一樣的贡定,均使用屏幕的寬來設(shè)置

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

繪制四個角

 //畫掃描框邊上的角赋访,總共8個部分
            paint.setColor(getResources().getColor(R.color.result_view));
            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);

            //繪制中間的線,每次刷新界面,中間的線往下移動SPEEN_DISTANCE
            slideTop += SPEEN_DISTANCE;
            if (slideTop >= frame.bottom) {
                slideTop = frame.top;
            }
            canvas.drawRect(frame.left + MIDDLE_LINE_PADDING, slideTop - MIDDLE_LINE_WIDTH / 2, frame.right - MIDDLE_LINE_PADDING, slideTop + MIDDLE_LINE_WIDTH / 2, paint);


            //畫掃描框下面的字
            paint.setColor(getResources().getColor(R.color.white));
            paint.setTextSize(TEXT_SIZE * density);
            paint.setAlpha(225);
            paint.setTypeface(Typeface.DEFAULT);
            String text = getResources().getString(R.string.msg_default_status);
            float textWidth = paint.measureText(text);
            canvas.drawText(text, (width - textWidth) / 2, (float) (frame.bottom + (float) TEXT_PADDING_TOP * density), paint);

實現(xiàn)效果圖:


image.png

三缓待、實現(xiàn)掃一掃

UI 繪制好后蚓耽,啟動 CaptureActivity 調(diào)用掃一掃,CaptureActivity 中的 handleDecode() 處理掃描后的結(jié)果旋炒,把處理好的結(jié)果返回步悠,

    public void handleDecode(Result rawResult, Bitmap barcode, float scaleFactor) {
        inactivityTimer.onActivity();
        String result = rawResult.getText();

        if (result.equals("")) {
            Toast.makeText(CaptureActivity.this, "Scan Failed!", Toast.LENGTH_SHORT).show();
        } else {
            Log.e(TAG, "掃描的結(jié)果" + result);
            // 把掃描結(jié)果返回到掃描的頁面
            Intent intent = new Intent();
            Bundle bundle = new Bundle();
            bundle.putString("result", result);
            intent.putExtras(bundle);
            setResult(RESULT_OK, intent);
        }
        CaptureActivity.this.finish();
    }

四、添加切換前后攝像頭

掃描二維碼調(diào)用的相機是系統(tǒng)相機瘫镇,如果手機本身支持前后攝像頭的話(廢話鼎兽,現(xiàn)在還有不支持前置攝像頭的手機嗎,我要不能自拍的手機干嘛铣除。谚咬。。)通孽,應(yīng)該都沒有問題序宦,主要是切換下前后攝像頭就可以了。所以呢,主要看調(diào)用相機部分互捌,camera -> open 下 OpenCameraInterface 主要是用來處理相機相關(guān)的潘明,所以看下,發(fā)現(xiàn)

  while (cameraId < numCameras) {
        Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
        Camera.getCameraInfo(cameraId, cameraInfo);
        if (CameraFacing.values()[cameraInfo.facing] == CameraFacing.BACK) {
          break;
        }
        cameraId++;
      }

CameraFacing.BACK秕噪,當(dāng)為后置攝像頭時返回了當(dāng)前相機钳降,也就是,默認掃一掃僅僅支持后置掃一掃腌巾,這里我們改為支持前后攝像頭遂填,根據(jù)攝像頭傳遞過來的參數(shù)進行修改,open() 打開相機的方法中添加一個參數(shù)澈蝙,用來判斷是前置攝像頭還是后置攝像頭

public static OpenCamera open(int cameraId, CameraFacing cf) {

        int numCameras = Camera.getNumberOfCameras();
        if (numCameras == 0) {
            Log.w(TAG, "No cameras!");
            return null;
        }
        if (cameraId >= numCameras) {
            Log.w(TAG, "Requested camera does not exist: " + cameraId);
            return null;
        }

        if (cameraId <= NO_REQUESTED_CAMERA) {
            cameraId = 0;

            while (cameraId < numCameras) {
                Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
                Camera.getCameraInfo(cameraId, cameraInfo);

                if(cf == CameraFacing.BACK){
                    if (CameraFacing.values()[cameraInfo.facing] == cf.BACK) {
                        break;
                    }
                }

                if(cf == CameraFacing.FRONT){
                    if (CameraFacing.values()[cameraInfo.facing] == cf.FRONT) {
                        break;
                    }
                }
                cameraId++;
            }

            if (cameraId == numCameras) {
                Log.i(TAG, "No camera facing " + CameraFacing.BACK + "; returning camera #0");
                cameraId = 0;
            }
        }

        Log.i(TAG, "Opening camera #" + cameraId);
        Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
        Camera.getCameraInfo(cameraId, cameraInfo);
        Camera camera = Camera.open(cameraId);
        if (camera == null) {
            return null;
        }
        return new OpenCamera(cameraId,
                camera,
                CameraFacing.values()[cameraInfo.facing],
                cameraInfo.orientation);
    }

然后發(fā)現(xiàn) 其實調(diào)用 OpenCameraInterface里面 open()方法的是 CameraManageropenDriver()吓坚,也就是說需要在 openDriver()里面?zhèn)鬟f參數(shù),再往外找灯荧,發(fā)現(xiàn)CaptureActivityinitCamera()是打開相機礁击,設(shè)置參數(shù)的地方,所以在這里把設(shè)置前后攝像頭的參數(shù)傳遞過去逗载,

 private void initCamera(SurfaceHolder surfaceHolder) {
        if (surfaceHolder == null) {
            throw new IllegalStateException("No SurfaceHolder provide");
        }
        if (cameraManager.isOpen()) {
            Log.w(TAG, "initCamera() while already open -- late SurfaceView callback?");

            // 如果相機已經(jīng)打開 則關(guān)閉當(dāng)前相機 重建一個 切換攝像頭哆窿,,如果不需要切換前置攝像頭 則這里直接return
            handler = null;
            cameraManager.closeDriver();

//            return;
        }
        try {
            cameraManager.openDriver(surfaceHolder, cfbf);
            // Creating the handler starts the preview, which can also throw a RuntimeException.
            if (handler == null) {
                handler = new CaptureActivityHandler(this, decodeFormats, decodeHints, characterSet, cameraManager);
            }
//            decodeOrStoreSavedBitmap(null, null);
        } catch (IOException ioe) {
            Log.w(TAG, ioe);
            displayFrameworkBugMessageAndExit();
        } catch (RuntimeException e) {
            // Barcode Scanner has seen crashes in the wild of this variety:
            // java.?lang.?RuntimeException: Fail to connect to camera service
            Log.w(TAG, "Unexpected error initializing camera", e);
            displayFrameworkBugMessageAndExit();
        }
    }

這樣就會調(diào)用相應(yīng)的前置或后置攝像頭厉斟。

BUT 修改后挚躯,發(fā)現(xiàn) 報錯了,前后攝像頭不能切換 直接卡死 2粱唷码荔!

Error:

 Unexpected exception while focusing
 Camera is being used after Camera.release() was called

看報錯信息大致是說,相機對象已經(jīng)被釋放了感挥,但是還在使用目胡,嗯,想想自己干了什么會這樣链快,對,cameraManager.closeDriver();眉尸,在切換前后攝像頭時域蜗,當(dāng)相機是打開的時候就先釋放,重新創(chuàng)建一個對象噪猾,所以在釋放的時候霉祸,不能繼續(xù)使用相機,在closeDriver()添加袱蜡,相機釋放之前丝蹭,先停止預(yù)覽。

 camera.getCamera().setPreviewCallback(null);
 camera.getCamera().lock();
 stopPreview();

在capture.xml中添加一個組件坪蚁,用來切換相機前后攝像頭奔穿。

實現(xiàn)效果:


image.png

五镜沽、生成二維碼

生成二維碼部分的功能主要在QRCodeEncoder.java里面,這里僅僅生成二維碼贱田,對于二維碼內(nèi)容的格式使用默認的缅茉,重寫構(gòu)造函數(shù)只傳入我們需要的參數(shù)

 public QRCodeEncoder(Context activity, int dimension, String contnt) {
    this.activity = activity;
    this.dimension = dimension; // 生成二維碼圖片的尺寸
    this.contents = contnt; // 生成的二維碼的內(nèi)容
  }
 public Bitmap encodeAsBitmap() throws WriterException {

    Log.e("二維碼圖片參數(shù)",String.valueOf(dimension));

    String contentsToEncode = contents;
    if (contentsToEncode == null) {
      return null;
    }
    
    Map<EncodeHintType,Object> hints = new HashMap<>();
//    String encoding = guessAppropriateEncoding(contentsToEncode);
//    if (encoding != null) {
//      hints = new EnumMap<>(EncodeHintType.class);
//      hints.put(EncodeHintType.CHARACTER_SET, encoding);
//    }

    hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");

    BitMatrix result;
    try {
      format = BarcodeFormat.QR_CODE;

      Log.e(TAG,"contentsToEncode == " + contentsToEncode);
      result = new MultiFormatWriter().encode(contentsToEncode, format, dimension, dimension, hints);

      int width = result.getWidth();
      int height = result.getHeight();
      int[] pixels = new int[width * height];
      for (int y = 0; y < height; y++) {
        int offset = y * width;
        for (int x = 0; x < width; x++) {
          pixels[offset + x] = result.get(x, y) ? BLACK : WHITE;
        }
      }

      Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
      bitmap.setPixels(pixels, 0, width, 0, 0, width, height);

      return bitmap;

    } catch (IllegalArgumentException iae) {
      Log.e(TAG,"Error == " + iae.toString());
      // Unsupported format
      return null;
    }

  }

調(diào)用encodeAsBitmap(),就可以生成Bitmap對象男摧。

實現(xiàn)效果:輸入內(nèi)容 點擊生成二維碼即可生成二維碼


image.png

這里僅僅把zxing項目中自己需要的功能提取出來蔬墩,主要還是要有耐心,看不懂就多看幾遍對我來說還是有效果的啊耗拓,哈哈哈拇颅,當(dāng)然,自己也去找了很多的資料幫助自己理解乔询,感謝你們樟插。

慣例,最后送給自己一句話:凡事往簡單處想哥谷,往認真處行

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末岸夯,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子们妥,更是在濱河造成了極大的恐慌猜扮,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,509評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件监婶,死亡現(xiàn)場離奇詭異旅赢,居然都是意外死亡,警方通過查閱死者的電腦和手機惑惶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評論 3 394
  • 文/潘曉璐 我一進店門煮盼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人带污,你說我怎么就攤上這事僵控。” “怎么了鱼冀?”我有些...
    開封第一講書人閱讀 163,875評論 0 354
  • 文/不壞的土叔 我叫張陵报破,是天一觀的道長。 經(jīng)常有香客問我千绪,道長充易,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,441評論 1 293
  • 正文 為了忘掉前任荸型,我火速辦了婚禮盹靴,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己稿静,他們只是感情好梭冠,可當(dāng)我...
    茶點故事閱讀 67,488評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著自赔,像睡著了一般妈嘹。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上绍妨,一...
    開封第一講書人閱讀 51,365評論 1 302
  • 那天润脸,我揣著相機與錄音,去河邊找鬼他去。 笑死毙驯,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的灾测。 我是一名探鬼主播爆价,決...
    沈念sama閱讀 40,190評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼媳搪!你這毒婦竟也來了铭段?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,062評論 0 276
  • 序言:老撾萬榮一對情侶失蹤秦爆,失蹤者是張志新(化名)和其女友劉穎序愚,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體等限,經(jīng)...
    沈念sama閱讀 45,500評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡爸吮,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,706評論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了望门。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片形娇。...
    茶點故事閱讀 39,834評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖筹误,靈堂內(nèi)的尸體忽然破棺而出桐早,到底是詐尸還是另有隱情,我是刑警寧澤厨剪,帶...
    沈念sama閱讀 35,559評論 5 345
  • 正文 年R本政府宣布勘畔,位于F島的核電站,受9級特大地震影響丽惶,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜爬立,卻給世界環(huán)境...
    茶點故事閱讀 41,167評論 3 328
  • 文/蒙蒙 一钾唬、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦抡秆、人聲如沸奕巍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽的止。三九已至,卻和暖如春着撩,著一層夾襖步出監(jiān)牢的瞬間诅福,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評論 1 269
  • 我被黑心中介騙來泰國打工拖叙, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留氓润,地道東北人。 一個月前我還...
    沈念sama閱讀 47,958評論 2 370
  • 正文 我出身青樓薯鳍,卻偏偏與公主長得像咖气,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子挖滤,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,779評論 2 354

推薦閱讀更多精彩內(nèi)容