美文网首页Android Android进阶小功能android
Android扫一扫:zxing的集成与优化

Android扫一扫:zxing的集成与优化

作者: a49f87ef5d4f | 来源:发表于2018-04-21 15:21 被阅读2086次

    0.

    最近项目里需要实现二维码的扫描功能,扫描两个二维码然后得到数据进行绑定。目前比较常见的二维码扫描库就是zxing和zbar了,zxing是google官方的开源项目,有专门的维护,java编写。zbar使用C语言写的,而且github上多年没有代码提交了,所以我决定选用zxing。

    1.

    附上zxing的项目地址:
    zxing
    打开zxing的github地址,发现似乎没有如何接入的文档。没关心,没有文档,但是有demo,我们要做的就是修改demo,移除无用的功能,只保留二维码的扫描和识别。

    2.

    下载项目后,里面很多东西我们是不需要的,我们需要的就是这个,如图所示 screenshot.png

    这个就是刚才所说的android的demo,新建一个android项目,将这个module导入工程并命名为zxinglib,在这个module里的gradle文件里添加依赖。

    dependencies{
        api 'com.google.zxing:android-core:3.3.0'
        api 'com.google.zxing:core:3.3.2'
    }
    

    运行这个module,你会发现这就是一个已经集成好zxing二维码扫描的app,同时还有一些不需要的功能,比如创建二维码,历史记录等等,而且还是相机预览还是横屏。下面我们就分析一下这个demo的二维码识别流程

    3.

    首先打开AndroidManifest文件,找到含有

        <intent-filter>
          <action android:name="android.intent.action.MAIN"/>
          <category     android:name="android.intent.category.LAUNCHER"/>
        </intent-filter>
    

    的Activity,发现是CaptureActivity,发现他还有好多其他intent-filter,CaptureActivity是可以被其他应用打开的,既然找到了入口,那就进去分析吧。
    先看onCreate方法:

    @Override
      public void onCreate(Bundle icicle) {
       super.onCreate(icicle);
    Window window = getWindow();   window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
    setContentView(R.layout.capture);
    hasSurface = false;
    inactivityTimer = new InactivityTimer(this);
    beepManager = new BeepManager(this);
    ambientLightManager = new AmbientLightManager(this);
    
    PreferenceManager.setDefaultValues(this, R.xml.preferences, false);}
    

    window设置标志位,保证屏幕常亮不会黑屏,inactivityTimer保证在电量较低的时候且一段时间没有激活的时候,关闭CaptureActivity,
    beepManager是用来扫码时发出声音和震动的,ambientLightManager是用来控制感光的,以此来控制闪光灯的开闭。然后我们再来看布局文件:

    <SurfaceView android:id="@+id/preview_view"
               android:layout_width="fill_parent"
               android:layout_height="fill_parent"/>
    
      <com.google.zxing.client.android.ViewfinderView
      android:id="@+id/viewfinder_view"
      android:layout_width="fill_parent"
      android:layout_height="fill_parent"/>
    

    布局文件里比较重要的就是这两个,一个surfaceview和一个viewfinderView,一个是照相机用来预览的界面,一个是取景框的界面,剩下的控价都是用来展示扫描结果的,和我们的需求没有太大关系,这里就不说了。

    4.

    扫描二维码自然不能少的就是相机的调用了,在AndroidManifest文件里,我们也看到了相关权限的声明

    <uses-permission android:name="android.permission.CAMERA"/>
      <uses-permission    android:name="android.permission.INTERNET"/>
      <uses-permission android:name="android.permission.VIBRATE"/>
      <uses-permission android:name="android.permission.FLASHLIGHT"/>
    

    有相机,震动和闪光灯的权限。
    接着看一下是在哪里初始化相机并调用预览的,在onReumse里
    有一段代码

    SurfaceView surfaceView = (SurfaceView) findViewById(R.id.preview_view);
    SurfaceHolder surfaceHolder = surfaceView.getHolder();
    if (hasSurface) {
      // The activity was paused but not stopped, so the surface still exists. Therefore
      // surfaceCreated() won't be called, so init the camera here.
      initCamera(surfaceHolder);
    } else {
      // Install the callback and wait for surfaceCreated() to init the camera.
      surfaceHolder.addCallback(this);
    }
    

    这里初始化了surfaceview,同时判断surface是否为true,true就调用initcamera,否在就为surfaceholder添加回调。刚才已经看到了,hasSurface在onCreate的时候赋值为false,之后在onResume也没有进行true的赋值,所以这里在第一次打开的时候,hasSurface=false。
    那我们就要关注surfaceHolder的回调了

     @Override
     public void surfaceCreated(SurfaceHolder holder) {
    if (holder == null) {
      Log.e(TAG, "*** WARNING *** surfaceCreated() gave us a null surface!");
    }
    if (!hasSurface) {
      hasSurface = true;
      initCamera(holder);
    }
      }
    

    在这里我们发现在surface创建后还是调用了initCamera,进入initCamera看看里面有什么

    private void initCamera(SurfaceHolder surfaceHolder) {
    if (surfaceHolder == null) {
      throw new IllegalStateException("No SurfaceHolder provided");
    }
    if (cameraManager.isOpen()) {
      Log.w(TAG, "initCamera() while already open -- late SurfaceView callback?");
      return;
    }
    try {
      cameraManager.openDriver(surfaceHolder);
      // 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();
     }
      }
    

    这里我们关心两个点,cameraManager.openDriver(surfaceHolder)和CaptureActivityHandler,进入openDriver这个方法

    public synchronized void openDriver(SurfaceHolder holder) throws IOException {
    OpenCamera theCamera = camera;
    if (theCamera == null) {
      theCamera = OpenCameraInterface.open(requestedCameraId);
      if (theCamera == null) {
        throw new IOException("Camera.open() failed to return object from driver");
      }
      camera = theCamera;
    }
    
    if (!initialized) {
      initialized = true;
      configManager.initFromCameraParameters(theCamera);
      if (requestedFramingRectWidth > 0 && requestedFramingRectHeight > 0) {
        setManualFramingRect(requestedFramingRectWidth, requestedFramingRectHeight);
        requestedFramingRectWidth = 0;
        requestedFramingRectHeight = 0;
      }
    }
    
    Camera cameraObject = theCamera.getCamera();
    Camera.Parameters parameters = cameraObject.getParameters();
    String parametersFlattened = parameters == null ? null : parameters.flatten(); // Save these, temporarily
    try {
      configManager.setDesiredCameraParameters(theCamera, false);
    } catch (RuntimeException re) {
      // Driver failed
      Log.w(TAG, "Camera rejected parameters. Setting only minimal safe-mode parameters");
      Log.i(TAG, "Resetting to saved camera params: " + parametersFlattened);
      // Reset:
      if (parametersFlattened != null) {
        parameters = cameraObject.getParameters();
        parameters.unflatten(parametersFlattened);
        try {
          cameraObject.setParameters(parameters);
          configManager.setDesiredCameraParameters(theCamera, true);
        } catch (RuntimeException re2) {
          // Well, darn. Give up
          Log.w(TAG, "Camera rejected even safe-mode parameters! No configuration");
        }
      }
    }
    cameraObject.setPreviewDisplay(holder);
      }
    

    这里可以发现,主要是针对camera的一些参数的设定,另外还要说明的一点是sdk21以后,以前的camera类已经废弃了,google又给出了camera2来替他他们,但是目前zxing这个库里还没有使用camera2,关于camera的相关问题,等以后有时间单独来写一篇文章,这里我们主要针对的是流程的分析。这个方法里我们发现调用了CameraConfigurationManager的initFromCameraParameters和setDesiredCameraParameters,这两个方法里找出了相机预览的最佳大小和根据屏幕进行camera方向的旋转,感兴趣的话可以看一下这两个方法。
    接下来我们来看CaptureActivityHandler

    CaptureActivityHandler(CaptureActivity activity,
                         Collection<BarcodeFormat> decodeFormats,
                         Map<DecodeHintType,?> baseHints,
                         String characterSet,
                         CameraManager cameraManager) {
    this.activity = activity;
    decodeThread = new DecodeThread(activity, decodeFormats, baseHints, characterSet,
        new ViewfinderResultPointCallback(activity.getViewfinderView()));
    decodeThread.start();
    state = State.SUCCESS;
    
    // Start ourselves capturing previews and decoding.
    this.cameraManager = cameraManager;
    cameraManager.startPreview();
    restartPreviewAndDecode();
    }
    

    在它的构造函数里我们发现它开启了相机的预览,同时启动了DecodeThread,再看看DeocodeThread的run方法

     @Override
      public void run() {
        Looper.prepare();
        handler = new DecodeHandler(activity, hints);
        handlerInitLatch.countDown();
        Looper.loop();
      }
    

    这里又实例化了一个DeoceHandler,好,那就进入DeoceHandler,看看这货又是什么

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

    这个decode方法,就是我们心心念念的用来解析二维码的地方,multiFormatReader.decodeWithState(bitmap);得到结果后,返回给CaptureActivityHandler,captureActivityHandler在接到R.id.decode_succeededde message后,会调用CaptureActivity的handleDecode方法,在这里会调用

     // Put up our own UI for how to handle the decoded contents.
    

    private void handleDecodeInternally(Result rawResult, ResultHandler resultHandler, Bitmap barcode) {

    maybeSetClipboard(resultHandler);
    
    SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
    
    if (resultHandler.getDefaultButtonID() != null && prefs.getBoolean(PreferencesActivity.KEY_AUTO_OPEN_WEB, false)) {
      resultHandler.handleButtonPress(resultHandler.getDefaultButtonID());
      return;
    }
    
    statusView.setVisibility(View.GONE);
    viewfinderView.setVisibility(View.GONE);
    resultView.setVisibility(View.VISIBLE);
    
    ImageView barcodeImageView = (ImageView) findViewById(R.id.barcode_image_view);
    if (barcode == null) {
      barcodeImageView.setImageBitmap(BitmapFactory.decodeResource(getResources(),
          R.drawable.launcher_icon));
    } else {
      barcodeImageView.setImageBitmap(barcode);
    }
    
    TextView formatTextView = (TextView) findViewById(R.id.format_text_view);
    formatTextView.setText(rawResult.getBarcodeFormat().toString());
    
    TextView typeTextView = (TextView) findViewById(R.id.type_text_view);
    typeTextView.setText(resultHandler.getType().toString());
    
    DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT);
    TextView timeTextView = (TextView) findViewById(R.id.time_text_view);
    timeTextView.setText(formatter.format(rawResult.getTimestamp()));
    
    
    TextView metaTextView = (TextView) findViewById(R.id.meta_text_view);
    View metaTextViewLabel = findViewById(R.id.meta_text_view_label);
    metaTextView.setVisibility(View.GONE);
    metaTextViewLabel.setVisibility(View.GONE);
    Map<ResultMetadataType,Object> metadata = rawResult.getResultMetadata();
    if (metadata != null) {
      StringBuilder metadataText = new StringBuilder(20);
      for (Map.Entry<ResultMetadataType,Object> entry : metadata.entrySet()) {
        if (DISPLAYABLE_METADATA_TYPES.contains(entry.getKey())) {
          metadataText.append(entry.getValue()).append('\n');
        }
      }
      if (metadataText.length() > 0) {
        metadataText.setLength(metadataText.length() - 1);
        metaTextView.setText(metadataText);
        metaTextView.setVisibility(View.VISIBLE);
        metaTextViewLabel.setVisibility(View.VISIBLE);
      }
    }
    
    CharSequence displayContents = resultHandler.getDisplayContents();
    TextView contentsTextView = (TextView) findViewById(R.id.contents_text_view);
    contentsTextView.setText(displayContents);
    int scaledSize = Math.max(22, 32 - displayContents.length() / 4);
    contentsTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, scaledSize);
    
    TextView supplementTextView = (TextView) findViewById(R.id.contents_supplement_text_view);
    supplementTextView.setText("");
    supplementTextView.setOnClickListener(null);
    if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean(
        PreferencesActivity.KEY_SUPPLEMENTAL, true)) {
      SupplementalInfoRetriever.maybeInvokeRetrieval(supplementTextView,
                                                     resultHandler.getResult(),
                                                     historyManager,
                                                     this);
    }
    
    int buttonCount = resultHandler.getButtonCount();
    ViewGroup buttonView = (ViewGroup) findViewById(R.id.result_button_view);
    buttonView.requestFocus();
    for (int x = 0; x < ResultHandler.MAX_BUTTON_COUNT; x++) {
      TextView button = (TextView) buttonView.getChildAt(x);
      if (x < buttonCount) {
        button.setVisibility(View.VISIBLE);
        button.setText(resultHandler.getButtonText(x));
        button.setOnClickListener(new ResultButtonListener(resultHandler, x));
      } else {
        button.setVisibility(View.GONE);
      }
    }
    }
    

    用来展示最终的结果。
    那么DeoceHandler又是什么时候调用deocode方法的呢?

    @Override
      public void handleMessage(Message message) {
       if (message == null || !running) {
      return;
    }
    switch (message.what) {
      case R.id.decode:
        decode((byte[]) message.obj, message.arg1, message.arg2);
        break;
      case R.id.quit:
        running = false;
        Looper.myLooper().quit();
        break;
    }
      }
    

    DeoceHandler在收到what==R.id.deocde的message时会调用decode方法,那么是谁发送的这个message呢?还记得CaptureActivityHandler的构造函数里,调用了restartPreviewAndDeocode方法

    private void restartPreviewAndDecode() {
    if (state == State.SUCCESS) {
      state = State.PREVIEW;
      cameraManager.requestPreviewFrame(decodeThread.getHandler(), R.id.decode);
      activity.drawViewfinder();
    }
    

    这个方法里不仅调用了drawViewFinder,绘制了取景框,还调用了
    cameraManager.requestPreviewFrame(decodeThread.getHandler(), R.id.decode);这里传入了decodehandler,它又做了什么?

    public synchronized void requestPreviewFrame(Handler handler, int message) {
    OpenCamera theCamera = camera;
    if (theCamera != null && previewing) {
      previewCallback.setHandler(handler, message);
      theCamera.getCamera().setOneShotPreviewCallback(previewCallback);
    }
      }
    

    原来这个方法为camera设置了previewcallback,同时previewcallback还持有decodehandler的引用,这个previewcallback的

    public void onPreviewFrame(byte[] data, Camera camera) {
    Point cameraResolution = configManager.getCameraResolution();
    Handler thePreviewHandler = previewHandler;
    if (cameraResolution != null && thePreviewHandler != null) {
      Message message = thePreviewHandler.obtainMessage(previewMessage, cameraResolution.x,
          cameraResolution.y, data);
      message.sendToTarget();
      previewHandler = null;
    } else {
      Log.d(TAG, "Got preview callback, but no handler or resolution available");
    }
    

    }
    在这里Message message = thePreviewHandler.obtainMessage(previewMessage, cameraResolution.x,
    cameraResolution.y, data);
    message.sendToTarget();
    发送了what=previewMessage,而这个previewMessage就是之前CaptureActivityHandler传入的R.id.decode。
    那么camera的setOneShotPreviewCallback这个方法是用来干什么的?查看源码看注释
    单个预览帧将被返回给提供的处理程序。 数据将以byte []形式到达在message.obj字段中,宽度和高度编码为message.arg1message.arg2。
    至此一个从预览到识别解析的流程差不多就分析完了,围绕这些,那些demo里的不需要的东西就可以删除了。

    最后附上git地址:
    github

    相关文章

      网友评论

      本文标题:Android扫一扫:zxing的集成与优化

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