Android使用zbar库实现扫码功能(实践篇)

前言

最近又被需求了一波,改了个扫码闪光灯无效的问题,让我好一顿查资料,然后从github上找了一版基于Zxing实现扫码功能的demo,从头到尾地屡了一遍扫码解码逻辑,所以想写一下学到的东西,主要是扫码实现的逻辑,后续我会把我自己写的上传到github供大家参考使用。话不多说,上酸菜!

正文

扫码逻辑实现概括:

  1. 初始化Camera:主要是打开摄像头,配置Camera的相关参数信息,包括预览尺寸、曝光度、自动对焦等数据,设置SurfaceHolder;
  2. 开始预览,设置预览CallBack:这一步是需要获取到扫码数据;
  3. 调用解密库进行数据解析:如果解析成功,则关闭Camera,释放资源;如果解析失败,则继续设置预览回调,重新获取预览数据;

Step one:初始化Camera

使用Camera需要Camera权限,小伙伴儿们千万不要忘记,首先添加权限:代码块

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

//动态申请权限
/**
 * 检查相机权限
 * @return 是否获取权限
 */
private boolean checkPermission() {
    if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) !=
            PackageManager.PERMISSION_GRANTED) {
        if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) {
            Toast.makeText(this, "此应用需要使用相机权限", Toast.LENGTH_SHORT).show();
        } else {
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, CAMERA_REQUESTCODE);
        }
        Log.i("xk", "permission denied!");
        return false;
    } else {
        Log.i("xk", "permit successfully!");
        return true;
    }
}

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    Log.i("xk", "onRequestPermissionsResult");
    if (requestCode == CAMERA_REQUESTCODE) {
        if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            startScan();
        } else {
            Toast.makeText(this, "相机权限未允许", Toast.LENGTH_SHORT).show();
        }
    }
}

权限申请完成后,接下来使用Camera离不开SurfaceView进行实时更新预览界面,所以layout文件需要定义:代码块

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <SurfaceView
        android:id="@+id/qr_code_preview_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="center"
        android:visibility="visible" />

    <com.test.xukun.scansimpletest.qrcode.view.QrCodeFinderView
        android:id="@+id/qr_code_view_finder"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_centerInParent="true"
        android:visibility="gone" />

    <View
        android:id="@+id/qr_code_view_background"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/black"
        android:visibility="gone" />

</RelativeLayout>

SerfaceView相关介绍可参考博文SerfaceView简单介绍
QrCodeFinderView是自定义实现的一个布局(继承RelativeLayout),用来绘制扫码框的动画效果(类似微信扫码界面上有个框和扫描动画);View这一块先不多讲,后续也会出一篇关于自定义View的文章供大家参考;
接下来需要获取Camera对象,设置相关参数,这一部分需要在SerfaceView加载成功后进行,所以需要用到SurfaceHolder.Callback,载入SerfaceView的Activity中实现SurfaceHolder.Callback接口,在其surfaceCreated方法中进行初始化操作,部分代码如下:代码块

// CameraManager.class

public void openDriver(SurfaceHolder holder) throws IOException {
    SDKLog.d(TAG,"-->openDriver");
    if (mCamera == null) {
        // 入参为摄像头id:0为后置摄像头
        mCamera = Camera.open(0);
        if (mCamera == null) {
            throw new IOException();
        }

        if (!mInitialized) {
            mInitialized = true;
            // 获取预览界面的尺寸
            mConfigManager.initFromCameraParameters(mCamera);
        }
        // 设置Camera参数
        mConfigManager.setDesiredCameraParameters(mCamera);
        // 设置SerfaceHolder
        mCamera.setPreviewDisplay(holder);
    }
}

// CameraConfigurationManager.class

void initFromCameraParameters(Camera camera) {
    Camera.Parameters parameters = camera.getParameters();
    int sWidth = ScreenUtils.getScreenWidth(mContext);
    int sHeight = ScreenUtils.getScreenHeight(mContext);
    SDKLog.d(TAG, "init preview begin size: " + sWidth + "-" + sHeight);
    if (sWidth > sHeight) {
        sHeight = sWidth * 3 / 4;
    } else {
        sWidth = sHeight * 3 / 4;
    }
    SDKLog.d(TAG, "init preview size: " + sWidth + "-" + sHeight);
    mCameraResolution = findCloselySize(sWidth, sHeight,
            parameters.getSupportedPreviewSizes());
    SDKLog.d(TAG, "Setting preview size: " + mCameraResolution.width + "-" + mCameraResolution.height);
    mPictureResolution = findCloselySize(ScreenUtils.getScreenWidth(mContext),
            ScreenUtils.getScreenHeight(mContext), parameters.getSupportedPictureSizes());
    SDKLog.d(TAG, "Setting picture size: " + mPictureResolution.width + "-" + mPictureResolution.height);
}

void setDesiredCameraParameters(Camera camera) {
    Camera.Parameters parameters = camera.getParameters();
    // 预览大小
    parameters.setPreviewSize(mCameraResolution.width, mCameraResolution.height);
    // 图片大小 拍摄图片使用
    parameters.setPictureSize(mPictureResolution.width, mPictureResolution.height);
    // flash模式
    parameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
    // 对焦模式
    parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
    // 提高MediaRecorder录制摄像头视频性能
    parameters.setRecordingHint(true);
    // 设置摄像头捕获数据方向
    camera.setDisplayOrientation(90);
    camera.setParameters(parameters);
}

// ScreenUtils.class
/**
 * 获取屏幕宽度
 *
 * @return
 */
public static int getScreenWidth(Context context) {
    DisplayMetrics dm = context.getResources().getDisplayMetrics();
    return dm.widthPixels;
}

/**
 * 获取屏幕高度
 *
 * @return
 */
public static int getScreenHeight(Context context) {
    DisplayMetrics dm = context.getResources().getDisplayMetrics();
    return dm.heightPixels;
}

Camera.Parameters类可设置Camera相关设置参数,像代码中注释说明的那些基本都是常用的属性,其中:

setFlashMode:闪光灯模式,共有五种模式:
public static final String FLASH_MODE_OFF = “off” //关闭
public static final String FLASH_MODE_AUTO = “auto”; //自动
public static final String FLASH_MODE_ON = “on”; //打开(拍照时)
public static final String FLASH_MODE_RED_EYE = “red-eye”; //红眼
public static final String FLASH_MODE_TORCH = “torch”; //始终开启

setRecordingHint:这个方法就是导致打开Camera后设置闪光灯无效的原因,默认是false,如果不进行设置或者置为false,则扫码时无法根据开关控制闪光灯;

其他参数读者可自行根据需求查找设置;

Step two:开始预览

这一步主要是打开摄像头预览,设置预览数据callback,设置自动对焦,同时开始解码线程,等待数据返回后进行解码;部分代码如下:代码块

// CaptureActivityHandler.class

public void restartPreviewAndDecode() {
    if (mState != State.PREVIEW) {
        SDKLog.d(TAG, "start preview!");
        CameraManager.get().startPreview();
        mState = State.PREVIEW;
        // 设置自动对焦
        CameraManager.get().requestAutoFocus(this, R.id.auto_focus);
        // 设置预览数据回调
        CameraManager.get().requestPreviewFrame(mDecodeThread.getHandler(), R.id.decode);
    }
}

// CameraManager.class
/**
 * 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 void requestPreviewFrame(Handler handler, int message) {
    if (mCamera != null && mPreviewing) {
        mPreviewCallback.setHandler(handler, message);
        //set一次 回调一次预览数据
        mCamera.setOneShotPreviewCallback(mPreviewCallback);
    }
}

/**
 * Asks the mCamera hardware to perform an autofocus.
 *
 * @param handler The Handler to notify when the autofocus completes.
 * @param message The message to deliver.
 */
public void requestAutoFocus(Handler handler, int message) {
    if (mCamera != null && mPreviewing) {
        mAutoFocusCallback.setHandler(handler, message);
        mCamera.autoFocus(mAutoFocusCallback);
    }
}

关于Camera.PreviewCallback的设置方法,这里用到了setOneShotPreviewCallback进行设置,还有其他几种方式进行设置回调:

  1. setPreviewCallback(Camera.PreviewCallback):使用此方法注册一个Camera. PreviewCallback,这将确保在屏幕上显示一个新的预览帧时调用onPreviewFrame方法。传递到onPreviewFrame方法中的数据字节数组最有可能采用YUV格式。但是,Android 2.2是第一个包含了YUV格式解码器(YuvImage)的版本;在以前的版本中,必须手动完成解码。
  2. setOneShotPreviewCallback(Camera.PreviewCallback):利用Camera对象上的这个方法注册Camera.PreviewCallback,从而当下一幅预览图像可用时调用一次onPreviewFrame。同样,传递到onPreviewFrame方法的预览图像数据最有可能采用YUV格式。可以通过使用ImageFormat中的常量检查Camera. getParameters(). getPreviewFormat()返回的结果来确定这一点。
  3. setPreviewCallbackWithBuffer(Camera.PreviewCallback):在Android 2.2中引入了该方法,其与setPreviewCallback的工作方式相同,但要求指定一个字节数组作为缓冲区,用于预览图像数据。这是为了能够更好地管理处理预览图像时使用的内存。

回调数据返回的原则:设置一次回调,出一次预览数据

实现Camera.PreviewCallback的类代码如下:代码块

final class PreviewCallback implements Camera.PreviewCallback {
    private static final String TAG = PreviewCallback.class.getSimpleName();
    private final CameraConfigurationManager mConfigManager;
    private Handler mPreviewHandler;
    private int mPreviewMessage;

    PreviewCallback(CameraConfigurationManager configManager) {
        this.mConfigManager = configManager;
    }

    void setHandler(Handler previewHandler, int previewMessage) {
        this.mPreviewHandler = previewHandler;
        this.mPreviewMessage = previewMessage;
    }
    
    /**
     * 预览数据回调方法
     * @param data 预览数据
     * @param camera camera对象
     */
    @Override
    public void onPreviewFrame(byte[] data, Camera camera) {
        Camera.Size cameraResolution = mConfigManager.getCameraResolution();
        if (mPreviewHandler != null) {
            Message message =
                mPreviewHandler.obtainMessage(mPreviewMessage, cameraResolution.width, cameraResolution.height, data);
            message.sendToTarget();
        } else {
            Log.v(TAG, "no handler callback.");
        }
    }
}

在onPreviewFrame方法中可获取到预览数据,将预览数据送到解码库即可进行解码操作;其中这里Camera对象获取的宽高就是你之前设置预览setPreviewSize时的宽高。

Step three:解码

这一步就比较简单了,调用解码库进行解码,如果解析成功就退出,否则接着进行扫描,部分代码如下:代码块

   /**
     * Decode the data within the viewfinder rectangle, and time how long it took. For efficiency, reuse the same reader
     * objects from one decode to the next.
     *
     * @param data   The YUV  SP420 preview frame.
     * @param width  The width of the preview frame.
     * @param height The height of the preview frame.
     */
    private void decode(byte[] data, int width, int height) {
        SDKLog.d(TAG, "scanning  start (width,height) = " + "(" + width + "," + height + ")");
        long timebegin = System.currentTimeMillis();
        // transfer 90'
        /*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;*/
        
        ZbarManager manager = new ZbarManager();
        String result = manager.decode(data, width, height, false, 0, 0, width,
                height);
        long end = System.currentTimeMillis();
        decodetime = (int) (end - timebegin);

        if (!TextUtils.isEmpty(result)) {
            SDKLog.d(TAG, "decode code result: " + result);
            SDKLog.d(TAG, "scan success decodetime = " + decodetime);
            Message message = Message.obtain(mActivity.getCaptureActivityHandler(), R.id.decode_succeeded, result);
            message.sendToTarget();
        } else {
            SDKLog.d(TAG, "scan failed decodetime = " + decodetime);
            Message message = Message.obtain(mActivity.getCaptureActivityHandler(), R.id.decode_failed);
            message.sendToTarget();
        }
    }

其中manager.decode方法就是调用zbar解码库的jni方法,参数说明:

public native String decode(byte[] data, int width, int height, boolean isCrop, int x, int y, int cwidth, int cheight);
data:待解码数据 width:预览数据宽 height:预览数据高
isCrop:是否裁剪 x:裁剪区起始点x y:裁剪区起始点y cwidth:裁剪区宽 cheight:裁剪区高

使用zbar库需注意,调用native方法的类包名必须是com.zbar.lib,不然使用时会报zbar库链接失败;
另这里我注释了一段代码,这是我之前调试其他公司的扫码时发现将预览原数据直接扔到解码库,解码会失败,后面我猜测是因为他们在兼容前置时Camera模块对数据做了修改,所以才将数据进行90度旋转后再扔到解码库,就成功解码了!

成功解码后不要忘记释放Camera资源哟,相关代码如下:代码块

    /**
     * Closes the camera driver if still in use.
     */
    public void closeDriver() {
        if (mCamera != null) {
            mCamera.setPreviewCallback(null);
            mCamera.stopPreview();
            mCamera.release();
            mCamera = null;
        }
    }

到这里,使用zbar库进行扫码的简单实现就结束了!

拓展功能:闪光灯开关

    /**
     * 打开或关闭闪光灯
     *
     * @param open 控制是否打开
     * @return 打开或关闭失败,则返回false。
     */
    public boolean setFlashLight(boolean open) {
        SDKLog.d(TAG,"-->setFlashLight("+open+")");
        if (mCamera == null) {
            return false;
        }
        Camera.Parameters parameters = mCamera.getParameters();
        if (parameters == null) {
            return false;
        }
        List<String> flashModes = parameters.getSupportedFlashModes();
        // Check if camera flash exists
        if (null == flashModes || 0 == flashModes.size()) {
            // Use the screen as a flashlight (next best thing)
            return false;
        }
        String flashMode = parameters.getFlashMode();
        SDKLog.d(TAG,"闪光灯模式 getFlashMode() = " + flashMode);
        if (open) {
            SDKLog.d(TAG,"进行 open 操作");
            if (Camera.Parameters.FLASH_MODE_TORCH.equals(flashMode)) {
                SDKLog.d(TAG,"flashMode 已经处于 TORCH(手电筒) 模式");
                return true;
            }
            // Turn on the flash
            if (flashModes.contains(Camera.Parameters.FLASH_MODE_TORCH)) {
                parameters.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);
                mCamera.setParameters(parameters);
                SDKLog.d(TAG,"设置为 TORCH(手电筒) 模式");
                return true;
            } else {
                SDKLog.d(TAG,"flashModes不包含FLASH_MODE_TORCH");
                return false;
            }
        } else {
            SDKLog.d(TAG,"进行 close 操作");
            if (Camera.Parameters.FLASH_MODE_OFF.equals(flashMode)) {
                SDKLog.d(TAG,"flashMode 已经处于 OFF(关闭) 模式");
                return true;
            }
            // Turn on the flash
            if (flashModes.contains(Camera.Parameters.FLASH_MODE_OFF)) {
                parameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
                mCamera.setParameters(parameters);
                SDKLog.d(TAG,"设置为 OFF(关闭) 模式");
                return true;
            } else
                SDKLog.d(TAG,"flashModes不包含FLASH_MODE_OFF");
            return false;
        }
    }

后记

这篇文章本来应该上周完成,结果拖延症到今天,不过最近加班也比较严重,感觉自己看起来已经成为一名程序猿了(加班+稀发)…不想多说,小伙伴儿们欢迎你们指导交流,工程代码会上传到GitHub上供大家参考~

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值