一款好用的二維碼掃描組件

簡介

之前項目中使用到掃描功能,那時邏輯業務和UI是完全耦合在一起,不好維護,也難移植。趁著這次新項目中要使用掃一掃的功能,就將二維碼掃描單獨提出作為一個組件庫,與業務完全分離。最終掃描結果通過回調的方式提供給調用者,用戶可以在自己的app中處理掃描結果。庫仿造Universal-Image-Loader進行封裝,提供一個配置文件,可簡單配置掃一掃界面的樣式,實現用戶UI定制。提供一個控制類,用戶通過其提供的接口與組件進行交互,內部實現相對于用戶都是透明的。實現效果如下圖所示:

具體實現

組件是調用zxing進行二維碼的編解碼計算,這部分不是本文研究的重點。本文主要關注以下幾點:

  • 掃描界面的繪制
  • 掃描結果如何回調給用戶
  • 組件是如何封裝的
  • 如何使用該組件

界面繪制

ViewfinderView是我們的掃描界面,實在onDraw方法中繪制。這里我直接上代碼,關鍵部分會有注釋

@Override
    public void onDraw(Canvas canvas) {
        //中間的掃描框,你要修改掃描框的大小,去CameraManager里面修改
        CameraManager.init(this.getContext().getApplicationContext());

        Rect frame = null;
        try {
            frame = CameraManager.get().getFramingRect();
        } catch (Exception e) {
            e.printStackTrace();
            return;
        }


        if (frame == null) {
            return;
        }

        //獲取屏幕的寬和高
        int width = canvas.getWidth();
        int height = canvas.getHeight();

        paint.setColor(maskColor);

        //畫出掃描框外面的陰影部分,共四個部分,掃描框的上面到屏幕上面,掃描框的下面到屏幕下面
        //掃描框的左邊面到屏幕左邊,掃描框的右邊到屏幕右邊
        canvas.drawRect(0, 0, width, frame.top, paint); //上
        canvas.drawRect(0, frame.top, frame.left, frame.bottom - 1, paint);  //左
        canvas.drawRect(frame.right, frame.top, width, frame.bottom - 1, paint); //右
        canvas.drawRect(0, frame.bottom - 1, width, height, paint);

        paint.setColor(0xffffffff);
        canvas.drawLine(frame.left + 1, frame.top + 1, frame.right - 1, frame.top + 1, paint);
        canvas.drawLine(frame.left + 1,frame.top + 1,frame.left + 1,frame.bottom - 1, paint);
        canvas.drawLine(frame.left + 1,frame.bottom - 1,frame.right -1,frame.bottom - 1,paint);
        canvas.drawLine(frame.right -1,frame.top + 1,frame.right - 1,frame.bottom - 1,paint);

        if (resultBitmap != null) {
            // Draw the opaque result bitmap over the scanning rectangle
            paint.setAlpha(OPAQUE);
            canvas.drawBitmap(resultBitmap, frame.left, frame.top, paint);
        } else {

            //畫掃描框邊上的角,總共8個部分
            paint.setColor(angleColor);
            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);

            // 如果設置了slideIcon,則顯示
            if(mSlideIcon != null){
//              mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.capture_add_scanning);

                BitmapDrawable bd = (BitmapDrawable) mSlideIcon;
                mBitmap = bd.getBitmap();

                //繪制中間的線,每次刷新界面,中間的線往下移動SPEEN_DISTANCE
                if (mBitmap != null){
                    mBitmap = Bitmap.createScaledBitmap(mBitmap, frame.right - frame.left, mBitmap.getHeight(), true);
                }


                //初始化中間線滑動的最上邊和最下邊
                if(!isFirst){
                    isFirst = true;
                    slideTop = frame.top + mBitmap.getHeight();
                    slideBottom = frame.bottom;
                }

                slideTop += SPEEN_DISTANCE;
                if(slideTop >= frame.bottom){
                    slideTop = frame.top + mBitmap.getHeight();
                }

                canvas.drawBitmap(mBitmap, frame.left, slideTop - mBitmap.getHeight(), paint);
            }else{
                //初始化中間線滑動的最上邊和最下邊
                if(!isFirst){
                    isFirst = true;
                    slideTop = frame.top + MIDDLE_LINE_WIDTH;
                    slideBottom = frame.bottom;
                }

                slideTop += SPEEN_DISTANCE;
                if(slideTop >= frame.bottom){
                    slideTop = frame.top + MIDDLE_LINE_WIDTH;
                }

                canvas.drawRect(frame.left + MIDDLE_LINE_PADDING, slideTop - MIDDLE_LINE_WIDTH,frame.right - MIDDLE_LINE_PADDING, slideTop, paint);

            }

            // 畫掃描框下面的字
            paint.setColor(mTipColor);
            paint.setTextSize(TEXT_SIZE * density);
            paint.setTextAlign(Paint.Align.CENTER);
            paint.setTypeface(Typeface.create("System", Typeface.NORMAL));
            canvas.drawText(scanTip, width/2, (float) (frame.bottom + (float)mTipmMargin * density), paint);
            
            //只刷新掃描框的內容,其他地方不刷新
            postInvalidateDelayed(ANIMATION_DELAY, frame.left, frame.top, frame.right, frame.bottom);

        }
    }

其中中間的掃描框,是在CameraManager中實現。

public Rect getFramingRect() {
    Point screenResolution = configManager.getScreenResolution();
    if (framingRect == null) {
      if (camera == null) {
        return null;
      }

      if (screenResolution == null){
        return null;
      }

      int width;
      int height;
      int topOffset;
      int leftOffset;
      int rightOffset;

      mFrameRate = QrScanProxy.getInstance().getScanFrameRectRate();

      width = (int) (screenResolution.x * mFrameRate);
      if (width < MIN_FRAME_WIDTH) {
        width = MIN_FRAME_WIDTH;
      }
      height = width;
      leftOffset = (screenResolution.x - width) / 2;
      topOffset = DeviceUtil.dip2px(DEFAULT_FRAME_MARGIN_TOP);
      if((height + topOffset) > screenResolution.y){
        topOffset = screenResolution.y - height;
      }

      framingRect = new Rect(leftOffset, topOffset, leftOffset + width, topOffset + height);
      Log.d(TAG, "Calculated framing rect: " + framingRect);
    }
    return framingRect;
  }

掃描結果回調

在CaptureActivityHandler中,有一個handleMessage方法。掃描的二維碼信息會在這邊進行分發。

 @Override
  public void handleMessage(Message message) {
    ...

    } 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);
      ...

組件封裝

對外部調用者,提供QrScan.java和QrScanConfiguration.java,前者是組件提供給外部用于和組件交互的方法,后者用于配置組件的相關屬性。在組件內部,有一個代理類QrScanProxy.java,所有外部設置的屬性作用于內部,都是通過這個類進行分發。具體實現大家可以參考源碼

使用

  • build.gradle配置
compile 'com.netease.scan:lib-qr-scan:1.0.0'
  • AndroidManifest配置
    // 設置權限
     <uses-permission android:name="android.permission.VIBRATE"/>
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-feature android:name="android.hardware.camera" />
    <uses-feature android:name="android.hardware.camera.autofocus" />
    
    // 注冊activity
    <activity android:name="com.netease.scan.ui.CaptureActivity"
            android:screenOrientation="portrait"
            android:theme="@style/Theme.AppCompat.NoActionBar"/>
  • 初始化
    在需要使用此組件的Activity的onCreate方法中,或者在自定義Application的onCreate方法中初始化。
/**
 * @author hzzhengrui
 * @Date 16/10/27
 * @Description
 */
public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();

//        // 默認配置
//        QrScanConfiguration configuration = QrScanConfiguration.createDefault(this);

        // 自定義配置
        QrScanConfiguration configuration = new QrScanConfiguration.Builder(this)
                .setTitleHeight(53)
                .setTitleText("來掃一掃")
                .setTitleTextSize(18)
                .setTitleTextColor(R.color.white)
                .setTipText("將二維碼放入框內掃描~")
                .setTipTextSize(14)
                .setTipMarginTop(40)
                .setTipTextColor(R.color.white)
                .setSlideIcon(R.mipmap.capture_add_scanning)
                .setAngleColor(R.color.white)
                .setMaskColor(R.color.black_80)
                .setScanFrameRectRate((float) 0.8)
                .build();
        QrScan.getInstance().init(configuration);
    }
}
  • 啟動掃描并處理掃描結果
QrScan.getInstance().launchScan(MainActivity.this, new IScanModuleCallBack() {
                    @Override
                    public void OnReceiveDecodeResult(final Context context, String result) {
                        mCaptureContext = (CaptureActivity)context;

                        AlertDialog dialog = new AlertDialog.Builder(mCaptureContext)
                                .setMessage(result)
                                .setCancelable(false)
                                .setNegativeButton("取消", new DialogInterface.OnClickListener() {
                                    @Override
                                    public void onClick(DialogInterface dialog, int which) {
                                        dialog.dismiss();
                                        QrScan.getInstance().restartScan(mCaptureContext);
                                    }
                                })
                                .setPositiveButton("關閉", new DialogInterface.OnClickListener() {
                                    @Override
                                    public void onClick(DialogInterface dialog, int which) {
                                        dialog.dismiss();
                                        QrScan.getInstance().finishScan(mCaptureContext);
                                    }
                                })
                                .create();
                        dialog.show();
                    }
                });

最后附上源碼地址:https://github.com/yushiwo/QrScan

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容