摘要
最近,在公司项目上需要加入“二维码扫描”的功能(Android端),笔者在网上查阅了一些资料,实现了这个功能。最后给自己做个笔记,给各位做下分享。
原理说明
“二维码扫描”实际上就是通过手机相机扫描『二维码图片』,将『二维码图片』中的字符串数据通过解码的方式解析出来。
实现方式
借助开源库 ZXing Android Embedded 实现二维码扫描。
Github地址: https://github.com/journeyapps/zxing-android-embedded
接下来,笔者分两部分进行讲解:
第1部分:ZXing Android Embedded简介及使用方法。
第2部分:自定义扫描界面。
一、ZXing Android Embedded简介及使用方法
1.简介
ZXing Android Embedded 是用于Android的条形码扫描库,使用ZXing进行解码。
注:二维码是条形码中的一种,该库也可以扫描二维码。
2.引入方法
添加gradle库依赖:
dependencies {
......
compile 'com.journeyapps:zxing-android-embedded:3.5.0'
}
注意事项:
- 该库在需要时会自动引入ZXing库,无需额外手动引入。
- buildToolsVersion '23.0.2'(构建工具的版本要>=23.0.2)
- compile 'com.android.support:appcompat-v7:23.1.0' (support-v7包版本要在23+以上)
- 最低支持的Android版本(API level 9+)
想要了解更多详情,可打开Github链接研究学习。
3.使用方法
接下来,笔者用一个实例来介绍一下该库的使用方法。
1.新建一个Android工程。
2.添加gradle库依赖,引入ZXing Android Embedded库。
3.在MainActivity的布局文件中放置一个Button(用于打开二维码扫描界面)。
4.在MainActivity中为Button设置点击事件,点击后跳转至扫描界面。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 创建IntentIntegrator对象
IntentIntegrator intentIntegrator = new IntentIntegrator(MainActivity.this);
// 开始扫描
intentIntegrator.initiateScan();
}
});
}
}
5.重写onActivityResult方法接收扫描结果。
public class MainActivity extends AppCompatActivity {
......
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// 获取解析结果
IntentResult result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data);
if (result != null) {
if (result.getContents() == null) {
Toast.makeText(this, "取消扫描", Toast.LENGTH_LONG).show();
} else {
Toast.makeText(this, "扫描内容:" + result.getContents(), Toast.LENGTH_LONG).show();
}
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
}
完成此步,基本的二维码扫描功能就已经出来了。
接下来,我们可以准备二维码图片试验一下。如果没有二维码图片,可以用草料二维码生成器在线生成一个二维码使用(如下图所示)。
6.跑一下Android程序,扫描一下二维码。(如下图所示)
我们看到扫描成功了,最后Toast出了“http://www.baidu.com”这个信息。
但这个扫描过程怎么感觉天旋地转的,一点也不流畅?.../(ㄒoㄒ)/~~
这是由于ZXing Android Embedded库提供的扫码Activity默认是横屏的。
不过,扫描界面的方向是可调的,Github文档也有说明,举个例子。
固定竖屏(仅需在manifest文件中添加如下配置)
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.wangnan7.qrcodescandemo">
<application
......
<!-- 调整二维码扫描界面为竖屏 -->
<activity
android:name="com.journeyapps.barcodescanner.CaptureActivity"
android:screenOrientation="portrait"
tools:replace="screenOrientation" />
</application>
</manifest>
重新跑下程序,如下所示:
7.其他配置项
在上述实例中,我们用两行代码(如下所示)实现了启动二维码扫描界面。
IntentIntegrator intentIntegrator = new IntentIntegrator(MainActivity.this);
intentIntegrator.initiateScan();
基本上没有添加什么配置。但是,该库还提供了其他配置项(如下所示)。
接下来,笔者详解一下这8个配置项。
1. setBarcodeImageEnabled(boolean enabled)
该方法用于设置“被扫描的二维码图片”可以保存在本地。
举个例子说明一下:
接着之前的例子,我们在布局文件中添加一个ImageView(用于显示二维码图片):
MainActivity修改后的代码如下:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
IntentIntegrator intentIntegrator = new IntentIntegrator(MainActivity.this);
// 设置可以保存条形码(二维码)图片
intentIntegrator.setBarcodeImageEnabled(true);
intentIntegrator.initiateScan();
}
});
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// 获取解析结果
IntentResult result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data);
if (result != null) {
if (result.getBarcodeImagePath() != null) {
// 显示条形码(二维码)图片的保存路径
Toast.makeText(this, result.getBarcodeImagePath(), Toast.LENGTH_LONG).show();
// 显示条形码(二维码)图片
showBarcodeImage(result.getBarcodeImagePath());
}
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
/**
* 加载并显示条形码图片
*/
private void showBarcodeImage(String barcodeImagePath) {
FileInputStream fis = null;
try {
fis = new FileInputStream(new File(barcodeImagePath));
((ImageView)findViewById(R.id.iv)).setImageBitmap(BitmapFactory.decodeStream(fis));
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
跑下程序,如下图所示:
可以看到,笔者Toast出了二维码图片被保存后的路径信息,并根据文件保存路径将二维码图片显示了出来。
所以,如果添加这个配置:
intentIntegrator.setBarcodeImageEnabled(true);
扫描后的二维码图片会被保存;如果不添加这个配置或参数设置为false,二维码图片不会被保存,我们拿到的路径result.getBarcodeImagePath()
就会变成null。
2. setCaptureActivity(Class<?> captureActivity)
该方法用于设置扫描Activity。如果你不想用该库提供的扫描Activity,可以自定义一个扫描Activity,将该Acitivty的运行时类作为参数传进去,这个方法后续用到时再详细说明。
3. setBeepEnabled(boolean enabled)
该方法用于设置扫码成功后的提示音,传true为开启,不设置或设置false为关闭。
4. setCameraId(int cameraId)
该方法用于设置相机ID。我们使用的手机一般都有前置和后置摄像头,该方法传0将会使用后置摄像头,传1将会使用前置摄像头。不设置则默认使用后置摄像头。
现在有些手机后置双摄像头,相机ID可能有所变化,有兴趣的朋友请自行研究。
5. setDesiredBarcodeFormats(Collection<String> desiredBarcodeFormats)
该方法用于设置你期望的条形码格式。(该库提供了5种格式,如下所示)
注:不设置默认为全部类型
所以对于扫描二维码,你可以选择不设置,如果设置可以使用QR_CODE_TYPES和ALL_CODE_TYPES。但是,笔者建议设置QR_CODE_TYPES,即:
intentIntegrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE_TYPES);
因为不设置或设置支持全部类型,会附带扫描其他条形码的功能,笔者认为实际功能应与描述功能相一致。
6. setOrientationLocked(boolean locked)
该方法用于设置方向锁。(源码解释如下:)
这个功能是用来调整扫描界面方向的,可以配合传感器使用,举个例子。
修改一下之前的manifest文件,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.wangnan7.qrcodescandemo">
<application
......
<!-- 调整二维码扫描界面方向为"完全依赖传感器" -->
<activity
android:name="com.journeyapps.barcodescanner.CaptureActivity"
android:screenOrientation="fullSensor"
tools:replace="screenOrientation" />
</application>
</manifest>
在MainActivity中添加方向锁设置,如下所示:
运行一下程序,如下所示:
可以看到调整手机方向时,扫描布局也会重新布置,最后笔者按Back返回键取消了扫描。
7. setPrompt(String prompt)
该方法用于设置扫描界面的提示信息。
举个例子,笔者设置一条提示信息(如下图所示)
运行一下程序,可以看到扫描界面的“提示文字”(如下图所示)
8. setTimeout(long timeout)
该方法用于设置扫描界面的超时时间。(避免用户打开扫描页面,忘记关闭)
举个例子,笔者设置一个2秒的超时时间(如下图所示)
运行一下程序,如下图所示:
可以看到,2秒后,扫描自动取消了。
ZXing Android Embedded的基本使用方法介绍完了。想了解更多用法的朋友可以通过GitHub链接或查看源码的方式学习。
二、自定义扫描界面
各位可能发现 ZXing Android Embedded库 提供的默认的扫描界面有些简陋(或丑陋),满足不了产品和设计的需求,举个例子:
产品想要下图这种效果,该怎么办呢?
这时就需要我们自定义扫描界面了...
自定义策略:比着葫芦画瓢
由于源码中的类在AndroidStudio中默认是被加锁的,我们无权直接修改。但我们可以仿写其中的一些类,方便我们添加自己的逻辑。自定义起点可以从Activity开始。
1.自定义扫描Activity
在源码中可以查到,我们之前一直在使用一个CaptureActivity进行二维码扫描(如下所示):
接下来,我们可以仿照CaptureActivity写一个自己的Activity(直接Copy也可以)。
笔者仿写的代码如下:
/**
* @Class: CustomCaptureActivity
* @Description: 自定义条形码/二维码扫描
* @Author: wangnan7
* @Date: 2017/5/19
*/
public class CustomCaptureActivity extends AppCompatActivity {
/**
* 条形码扫描管理器
*/
private CaptureManager mCaptureManager;
/**
* 条形码扫描视图
*/
private DecoratedBarcodeView mBarcodeView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(com.google.zxing.client.android.R.layout.zxing_capture);
mBarcodeView = (DecoratedBarcodeView)findViewById(com.google.zxing.client.android.R.id.zxing_barcode_scanner);
mCaptureManager = new CaptureManager(this, mBarcodeView);
mCaptureManager.initializeFromIntent(getIntent(), savedInstanceState);
mCaptureManager.decode();
}
@Override
protected void onResume() {
super.onResume();
mCaptureManager.onResume();
}
@Override
protected void onPause() {
super.onPause();
mCaptureManager.onPause();
}
@Override
protected void onDestroy() {
super.onDestroy();
mCaptureManager.onDestroy();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
mCaptureManager.onSaveInstanceState(outState);
}
/**
* 权限处理
*/
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
mCaptureManager.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
/**
* 按键处理
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
return mBarcodeView.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event);
}
}
注:XML布局还是使用的源码中CaptureActivity的布局。
紧接着,我们可以在manifest文件中声明一下这个新创建的Activity。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.wangnan7.qrcodescandemo">
<application
.......
<!-- 设置二维码扫描界面方向为竖屏 -->
<activity
android:name=".CustomCaptureActivity"
android:label="自定义扫描界面"
android:screenOrientation="portrait"/>
</application>
</manifest>
最后,我们就可以在MainActivity中调用这个新的扫描Activity了。
运行程序,效果如下:
可以看到我们自定义的扫描Activity可以正常运行,扫码也成功了。
但是,我们自定义Activty使用的布局还是源码中的布局文件,对于这个布局文件我们没有权限修改,接下来就需要自定义扫描布局了。
2.自定义扫描布局
源码布局如下:
笔者仿写的自定义扫描布局 (activity_zxing_layout.xml):
属性简介:
app:zxing_preview_scaling_strategy : 预览视图的缩放策略,使用centerCrop即可
app:zxing_use_texture_view : 是否使用纹理视图(黑色背景)
接下来,我们就可以把自定义扫描Activity的布局文件给替换掉了。
/**
* @Class: CustomCaptureActivity
* @Description: 自定义条形码/二维码扫描
* @Author: wangnan7
* @Date: 2017/5/19
*/
public class CustomCaptureActivity extends AppCompatActivity {
......
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_zxing_layout);
mBarcodeView = (DecoratedBarcodeView)findViewById(R.id.zxing_barcode_scanner);
......
}
......
}
最后,我们跑程序验证一下:
可以看到我们的自定义布局文件也没有问题。
我们的自定义Activity和自定义布局文件都完成了,剩下的就是修改扫描视图的样式了。
3.修改扫描视图的样式
想要修改扫描视图的样式,需要略微研究下DecoratedBarcodeView的源码。
1.DecoratedBarcodeView初始化分析
补充:可以看到 scannerLayout 最后被作为扫描布局inflate进了DecorateBarcodeView中。
2.默认布局R.layout.zxing_barcode_scanner分析
分析到这里,我们需要做的工作就显现出来了。那就是:
自定义View(继承ViewfinderView),重写onDraw方法,然后替换掉这里的ViewfinderView。
因为R.layout.zxing_barcode_scanner是源码中的布局文件,无法直接修改,所以还要重写一份布局文件给DecoratedBarcodeView加载。那么,接下来需要做两步准备工作:
(1)仿写默认布局文件R.layout.zxing_barcode_scanner
(2)让DecoratedBarcodeView加载刚刚仿写布局,不再使用默认布局。
3.开始自定义扫描视图(继承ViewfinderView重写onDraw方法)
小技巧:如果不知道如何开始,可以先将原ViewfinderView的onDraw方法copy进来一点一点研究修改。
笔者直接将自己的自定义扫描布局粘贴出来,需要的朋友可以借鉴或Copy:
/**
* @Class: CustomViewfinderView
* @Description: 自定义扫描框样式
* @Author: wangnan7
* @Date: 2017/5/22
*/
public class CustomViewfinderView extends ViewfinderView {
/**
* 重绘时间间隔
*/
public static final long CUSTOME_ANIMATION_DELAY = 16;
/* ****************************************** 边角线相关属性 ************************************************/
/**
* "边角线长度/扫描边框长度"的占比 (比例越大,线越长)
*/
public float mLineRate = 0.1F;
/**
* 边角线厚度 (建议使用dp)
*/
public float mLineDepth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 4, getResources().getDisplayMetrics());
/**
* 边角线颜色
*/
public int mLineColor = Color.WHITE;
/* ******************************************* 扫描线相关属性 ************************************************/
/**
* 扫描线起始位置
*/
public int mScanLinePosition = 0;
/**
* 扫描线厚度
*/
public float mScanLineDepth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 4, getResources().getDisplayMetrics());
/**
* 扫描线每次重绘的移动距离
*/
public float mScanLineDy = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, getResources().getDisplayMetrics());
/**
* 线性梯度
*/
public LinearGradient mLinearGradient;
/**
* 线性梯度位置
*/
public float[] mPositions = new float[]{0f, 0.5f, 1f};
/**
* 线性梯度各个位置对应的颜色值
*/
public int[] mScanLineColor = new int[]{0x00FFFFFF, Color.WHITE, 0x00FFFFFF};
public CustomViewfinderView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public void onDraw(Canvas canvas) {
refreshSizes();
if (framingRect == null || previewFramingRect == null) {
return;
}
Rect frame = framingRect;
Rect previewFrame = previewFramingRect;
int width = canvas.getWidth();
int height = canvas.getHeight();
//绘制4个角
paint.setColor(mLineColor); // 定义画笔的颜色
canvas.drawRect(frame.left, frame.top, frame.left + frame.width() * mLineRate, frame.top + mLineDepth, paint);
canvas.drawRect(frame.left, frame.top, frame.left + mLineDepth, frame.top + frame.height() * mLineRate, paint);
canvas.drawRect(frame.right - frame.width() * mLineRate, frame.top, frame.right, frame.top + mLineDepth, paint);
canvas.drawRect(frame.right - mLineDepth, frame.top, frame.right, frame.top + frame.height() * mLineRate, paint);
canvas.drawRect(frame.left, frame.bottom - mLineDepth, frame.left + frame.width() * mLineRate, frame.bottom, paint);
canvas.drawRect(frame.left, frame.bottom - frame.height() * mLineRate, frame.left + mLineDepth, frame.bottom, paint);
canvas.drawRect(frame.right - frame.width() * mLineRate, frame.bottom - mLineDepth, frame.right, frame.bottom, paint);
canvas.drawRect(frame.right - mLineDepth, frame.bottom - frame.height() * mLineRate, frame.right, frame.bottom, paint);
// Draw the exterior (i.e. outside the framing rect) darkened
paint.setColor(resultBitmap != null ? resultColor : maskColor);
canvas.drawRect(0, 0, width, frame.top, paint);
canvas.drawRect(0, frame.top, frame.left, frame.bottom + 1, paint);
canvas.drawRect(frame.right + 1, frame.top, width, frame.bottom + 1, paint);
canvas.drawRect(0, frame.bottom + 1, width, height, paint);
if (resultBitmap != null) {
// Draw the opaque result bitmap over the scanning rectangle
paint.setAlpha(CURRENT_POINT_OPACITY);
canvas.drawBitmap(resultBitmap, null, frame, paint);
} else {
// 绘制扫描线
mScanLinePosition += mScanLineDy;
if(mScanLinePosition > frame.height()){
mScanLinePosition = 0;
}
mLinearGradient = new LinearGradient(frame.left, frame.top + mScanLinePosition, frame.right, frame.top + mScanLinePosition, mScanLineColor, mPositions, Shader.TileMode.CLAMP);
paint.setShader(mLinearGradient);
canvas.drawRect(frame.left, frame.top + mScanLinePosition, frame.right, frame.top + mScanLinePosition + mScanLineDepth, paint);
paint.setShader(null);
float scaleX = frame.width() / (float) previewFrame.width();
float scaleY = frame.height() / (float) previewFrame.height();
List<ResultPoint> currentPossible = possibleResultPoints;
List<ResultPoint> currentLast = lastPossibleResultPoints;
int frameLeft = frame.left;
int frameTop = frame.top;
if (currentPossible.isEmpty()) {
lastPossibleResultPoints = null;
} else {
possibleResultPoints = new ArrayList<>(5);
lastPossibleResultPoints = currentPossible;
paint.setAlpha(CURRENT_POINT_OPACITY);
paint.setColor(resultPointColor);
for (ResultPoint point : currentPossible) {
canvas.drawCircle(frameLeft + (int) (point.getX() * scaleX),
frameTop + (int) (point.getY() * scaleY),
POINT_SIZE, paint);
}
}
if (currentLast != null) {
paint.setAlpha(CURRENT_POINT_OPACITY / 2);
paint.setColor(resultPointColor);
float radius = POINT_SIZE / 2.0f;
for (ResultPoint point : currentLast) {
canvas.drawCircle(frameLeft + (int) (point.getX() * scaleX),
frameTop + (int) (point.getY() * scaleY),
radius, paint);
}
}
}
// Request another update at the animation interval, but only repaint the laser line,
// not the entire viewfinder mask.
postInvalidateDelayed(CUSTOME_ANIMATION_DELAY,
frame.left,
frame.top,
frame.right,
frame.bottom);
}
}
代码简介:
(1)onDraw方法中的大部分代码Copy自ViewfinderView,笔者添加了两部分逻辑:第一部分是边角线的绘制;第二部分是用“扫描线”替换掉了原有的“激光线”。
(2)代码的核心是在onDraw方法的第5行代码:
Rect frame = framingRect;
这个矩阵记录了扫描框四个顶点的坐标,有了这个变量,各位可以发挥想象力自定义自己需要的扫描样式。
接下来,我们用CustomViewfinderView替换掉ViewfinderView(如下图所示)
最后,跑下程序(如下图所示)
4.样式调整(UI优化)
我们的自定义扫描界面搞定了,但UI样式还需要再优化一下:
(1) 框体大小调整 (DecoratedBarcodeView有属性支持修改)
调整后的效果图:
(2) 将扫描界面底部文字平移至扫描框底部
调整后的效果图:
(3) 将扫描框向上平移
扫描框在默认情况下是相对于相机视图居中的,想要调整扫描框的位置还要去修改源码...
笔者想了一个投机取巧的办法:透明掉标题栏和状态栏让相机预览视图向上延伸,使扫描框在视觉上略微上移。
这部分代码和二维码扫描没有直接关系,笔者就不贴代码了,各位可以尝试自己实现,但最后笔者会附上本Demo的GitHub链接。
最终的效果:
Demo的Github链接: