最近又被需求了一波,改了个扫码闪光灯无效的问题,让我好一顿查资料,然后从github上找了一版基于Zxing实现扫码功能的demo,从头到尾地屡了一遍扫码解码逻辑,所以想写一下学到的东西,主要是扫码实现的逻辑,后续我会把我自己写的上传到github供大家参考使用。话不多说,上酸菜!
扫码逻辑实现概括:
使用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,则扫码时无法根据开关控制闪光灯;
其他参数读者可自行根据需求查找设置;
这一步主要是打开摄像头预览,设置预览数据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进行设置,还有其他几种方式进行设置回调:
回调数据返回的原则:设置一次回调,出一次预览数据;
实现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时的宽高。
这一步就比较简单了,调用解码库进行解码,如果解析成功就退出,否则接着进行扫描,部分代码如下:代码块
/**
* 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上供大家参考~