在正文之前说点题外话,加上这篇我已经写了3篇博客了,其实我写博客的初衷不是想证明自己有多牛,并且我也只是从事安卓开发只有半年时间的小渣,但是不想成为大牛的渣不是好渣,所以我想通过博客把工作学习中遇到的问题进行研究总结,从而提高自己,与此同时如果能给广大从事安卓开发的朋友们提供帮助或者是提供一点点思路我也是很心满意足了!~~好了,废话不多少进入正题吧!
先上界面图。由于目前不会录屏,所以直接上截图吧!~另外扫描框内本身有一个从上之下循环的绿线,但是不知道为毛手机截屏没显示。。。汗。。
最近公司需要一个二维码扫描的需求,于是我便在网上下载了一个应用谷歌zxing的开源项目,但是这个项目界面不符合公司需求,并且不灵活,我便研究了下这个项目,在这个项目的基础上进行了修改,编写了ZxingScannerViewNew这个类。先看下目录结构。
整个项目是在Android studio上进行开发的,zxinglib是一个Android Lib modul,libs文件夹中便是谷歌zxing的jar包,主要解码等方法都是这个包提供的。
BarcodeScannerView,CameraPreview,CameraUtils,DisplayUtils,ViewFinderView,这几个文件都是第三方项目提供的。我结合了BarcodeScannerView和ViewFinderView主要功能,并且进行了修改,使ZxingScannerViewNew更方便自定义扫描界面。CameraPreview是Camera的预览界面。下面先看下ZxingScannerViewNew这个类。
ZxingScannerViewNew继承FrameLayout并且实现Camera.PreviewCallback接口(后面有介绍)。看下成员变量。
private Camera mCamera;
private CameraPreview mPreview;//相机预览界面
private View showPanel;//扫描界面
private QrSize qrSize;//自己定义的接口
其中QrSize是自己定义的接口,代码如下。
public interface QrSize {
public Rect getDetectRect();
}
这个接口主要是获取扫描区域的Rect矩形坐标等信息。接下来看下几个重要的方法。
private void init() {//初始化方法
addView(mPreview = new CameraPreview(getContext()));
//加载默认扫描界面
showPanel = View.inflate(getContext(), R.layout.default_scan, null);
addView(showPanel);
initMultiFormatReader();//初始化解码的类
}
public void setContentView(int res) {
try {
View panelView = View.inflate(getContext(), res, null);
removeView(showPanel);
showPanel = panelView;
addView(showPanel);
} catch (Exception e) {
return;
}
}
在init方法中会初始化相机预览界面CameraPreview并且加入到ZxingScannerViewNew中作为底层。接着初始化默认扫描界面,并且加入到ZxingScannerViewNew中。因为ZxingScannerViewNew是framelayout所以showPanel 会覆盖到相机预览界面之上。如果调用setContentView方法,会移除默认的扫描界面,并且把传递进来的layout覆盖到相机预览界面之上,所以通过这个方法可以轻松实现自定义扫面界面。
接下来介绍下Camera.PreviewCallback接口。
很多时候,android摄像头模块不仅预览拍照这么简单,而是需要在预览的同时,能够做出一些检测,比如最常见的人脸检测。在未按下拍照按钮前,就检测出人脸然后显示矩形提示框,再按拍照。那么如何获得预览帧视频呢?只需要继承PreviewCallback这个接口就行了。继承这个方法后,会自动重载这个函数:
public void onPreviewFrame(byte[] data, Camera camera) {}。
这个函数里的data就是实时预览帧视频数据。一旦程序调用PreviewCallback接口,就会自动调用onPreviewFrame这个函数。调用PreviewCallback的方法有三种
1,setPreviewCallback。
2,setOneShotPreviewCallback。
3, setPreviewCallbackWithBuffer,。
一般是使用第二种方式。
什么时候触发onPreviewFrame()这个函数呢?可以是按一个按键触发一次,就在按键的监听里执行setOneShotPreviewCallback,便会自动触发一次。但大多数希望程序自动每隔多长时间,自动进行一次检测预览帧。所以我会在程序中设置一个timer每隔1.5秒便执行一次。
private void startFreshThread(final Camera camera, final Camera.PreviewCallback callback) {
//初始化每timer每隔1.5秒设置一次 setOneShotPreviewCallback
timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
Log.e("Run", "Running");
if (camera == null || !cameraAvailable()) {
cancelTask();
return;
}
camera.setOneShotPreviewCallback(callback);
}
}, 0, 1500);
}
private byte[] rotatedData;
private boolean onlyOnce;
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
if (onlyOnce) {//只开启timer一次
startFreshThread(camera, this);
onlyOnce = false;
}
Camera.Parameters parameters = camera.getParameters();
//获取帧视频size
Camera.Size size = parameters.getPreviewSize();
int width = size.width;//获取帧视频宽度
int height = size.height;//获取帧视频高度
if (DisplayUtils.getScreenOrientation(getContext()) == Configuration.ORIENTATION_PORTRAIT) {//判断是否是竖屏
if (rotatedData == null) {
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;
}
Result rawResult = null;
//调用zxing jar包中的方法 生成解码所需的YUV类型数据
PlanarYUVLuminanceSource source = buildLuminanceSource(data, width, height);
if (source != null) {
//调用zxing jar包中的方法将source转为bitmap
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
try {
//调用zxing jar包中的方法进行解码
rawResult = mMultiFormatReader.decodeWithState(bitmap);
} catch (NullPointerException npe) {
} catch (ArrayIndexOutOfBoundsException aoe) {
} finally {
mMultiFormatReader.reset();
}
}
if (rawResult != null) {
if (mResultHandler != null) {
//将结果返回给回调方法handleResult
mResultHandler.handleResult(rawResult);
}
}
}
需要说明的是默认情况下获取的帧视频是倒着的,所以如果手机是竖屏我们需要交换宽和高的值。上面代码中通过buildLuminanceSource方法形成解码所需数据,所以我们看下这个方法。
public PlanarYUVLuminanceSource buildLuminanceSource(byte[] data, int width, int height) {
Rect rect = getFramingRectInPreview(width, height);
if (rect == null) {
return null;
}
// Go ahead and assume it's YUV rather than die.
PlanarYUVLuminanceSource source = null;
try {
//新建PlanarYUVLuminanceSource对象
source = new PlanarYUVLuminanceSource(data, width, height, rect.left, rect.top,
rect.width(), rect.height(), false);
} catch (Exception e) {
}
return source;
}
这个方法有3个参数,第一个是帧视频数据,第二个第三个为帧视频的宽和高,通过getFramingRectInPreview获得扫描区的rect ,接着把rect的左边界坐标,上边界坐标、高度、宽度还有帧视频的高宽交给zxing jar包提供的方法来new一个PlanarYUVLuminanceSource对象并把这个对象返回。
接着看下getFramingRectInPreview方法的代码。
public synchronized Rect getFramingRectInPreview(int previewWidth, int previewHeight) {
if (qrSize == null || qrSize.getDetectRect() == null) {
return null;
}
//通过回调方法获取扫描区的rect
Rect rect = qrSize.getDetectRect();
int width=showPanel.getWidth();
int height=showPanel.getHeight();
if ((rect.right - rect.left) != 0 && (rect.top - rect.bottom) != 0) {
rect.left = rect.left * previewWidth / width;
rect.right = rect.right * previewWidth / width;
rect.top = rect.top * previewHeight / height;
rect.bottom = rect.bottom * previewHeight / height;
return rect;
} else {
return null;
}
}
代码中主要通过回调方法getDetectRect获取扫描区的rect,另外有一个计算公式。
rect.left = rect.left * previewWidth / width;
rect.right = rect.right * previewWidth / width;
rect.top = rect.top * previewHeight / height;
rect.bottom = rect.bottom * previewHeight /height;
这部分代码根据视频帧的高度、宽度和扫描界面高宽对扫描区进行缩放,主要是为了防止相机预览界面不是全屏导致rect数据错误问题。
好了ZxingScannerViewNew这个类我们已经介绍的差不多了。接下来我们只需在自己的activity中通过setContentView方法把自己定制的扫描layout传递给ZxingScannerViewNew,然后把扫描区域的rect计算好,通过QrSize接口传递给ZxingScannerViewNew,最后在回调方法handleResult中接收扫描结果即可~~~
微信扫一扫中会与一根绿线自上而下循环显示,我的解决方案是在扫描区域中设置一根绿线imageview起始状态设置为gone,界面初始化后让它visiable,同时添加自上而下的循环移动动画。
最后贴上activity代码。
package com.afun.zxingcore;
import android.app.Activity;
import android.graphics.Rect;
import android.os.Bundle;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.TranslateAnimation;
import android.widget.ImageView;
import android.widget.TextView;
import com.afun.zxinglib.ScanView.ZXingScannerViewNew;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.Result;
import java.util.ArrayList;
import java.util.List;
public class QrScanActivity extends Activity implements ZXingScannerViewNew.ResultHandler, ZXingScannerViewNew.QrSize, View.OnClickListener {
ZXingScannerViewNew scanView;
private TextView result;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//初始化自己定制的扫描界面
scanView = new ZXingScannerViewNew(this);
scanView.setContentView(R.layout.logistics_scan_qr);
scanView.setQrSize(this);
setContentView(scanView);
setupFormats();
initUI();
}
private void initUI() {
findViewById(R.id.confirm).setOnClickListener(this);
result= (TextView) findViewById(R.id.editText);
}
@Override
protected void onResume() {
super.onResume();
//设置处理扫描结果的回调方法
scanView.setResultHandler(this);
//设置camera
scanView.startCamera(-1);
scanView.setFlash(false);
scanView.setAutoFocus(true);
}
@Override
public void handleResult(Result rawResult) {
//回调方法中接收扫描结果并且设置到textview中
result.setText(rawResult.toString());
}
@Override
protected void onPause() {
super.onPause();
scanView.stopCamera();
}
//zxing项目可以解析二维码和条形码等,我们这里只添加二维码格式,让他只处理二维码
public void setupFormats() {
List formats = new ArrayList();
formats.add(BarcodeFormat.QR_CODE);
if (scanView != null) {
scanView.setFormats(formats);
}
}
//计算扫描区域的rect,算法比较简单就不解释了
@Override
public Rect getDetectRect() {
View view = findViewById(R.id.scan_window);
int top = ((View) view.getParent()).getTop() + view.getTop();
int left = view.getLeft();
int width = view.getWidth();
int height = view.getHeight();
Rect rect = null;
if (width != 0 && height != 0) {
rect = new Rect(left, top, left + width, top + height);
addLineAnim(rect);
}
return rect;
}
//给扫描框内的绿线设置移动动画,让它在扫描区域内循环移动
private void addLineAnim(Rect rect) {
ImageView imageView = (ImageView) findViewById(R.id.scanner_line);
imageView.setVisibility(View.VISIBLE);
if (imageView.getAnimation() == null) {
TranslateAnimation anim = new TranslateAnimation(0, 0, 0, rect.height());
anim.setDuration(1500);
anim.setRepeatCount(Animation.INFINITE);
imageView.startAnimation(anim);
}
}
@Override
public void onClick(View v) {
int id = v.getId();
if (id == R.id.confirm){
//TODO something
}
}
}
我自己定制的扫描界面如下图
其实layout布局也很简单,下面我贴出下载地址,感兴趣的话大家可以下载下来研究研究!~~~
好了,写到这了,希望能给大家带来思路上的提示!~欢迎大家留言一起交流!~
下载链接
高仿微信扫一扫,轻松实现定制扫描界面