现在一维码二维码在我们的日常生活中使用如此的广泛,所以拥有扫码功能的APP变得非常普遍,一个安卓APP需要扫码功能就要用到zxing了,zxing是谷歌开源的让开发者更方便使用摄像头的库,而我们常用的扫码功能就是其中之一。
github地址:https://github.com/zxing/zxing
但是因为zxing的功能太强大了,包含了很多我们用不上的功能,所以一般都会抽取其中的扫码功能单独使用,这个抽取的过程还是有点麻烦的,但是已经有很多开发者为我们省去了这个过程,现在就来介绍一个很棒的第三方zxing库:zxing-android-embedded
github地址:https://github.com/journeyapps/zxing-android-embedded
使用方式也很简单,首先在gradle中添加依赖
compile 'com.journeyapps:zxing-android-embedded:3.5.0'
然后在Activity或fragment中调用即可:
new IntentIntegrator(this)
.setDesiredBarcodeFormats(IntentIntegrator.ONE_D_CODE_TYPES)// 扫码的类型,可选:一维码,二维码,一/二维码
.setPrompt("请对准二维码")// 设置提示语
.setCameraId(0)// 选择摄像头,可使用前置或者后置
.setBeepEnabled(false)// 是否开启声音,扫完码之后会"哔"的一声
.setBarcodeImageEnabled(true)// 扫完码之后生成二维码的图片
.initiateScan();// 初始化扫码
扫完码之后会在onActivityResult方法中回调结果
@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, "Cancelled", Toast.LENGTH_LONG).show();
} else {
Toast.makeText(this, "Scanned: " + result.getContents(), Toast.LENGTH_LONG).show();
}
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
以上就是简单的使用方式了
我们开发的时候可能会想要自定义界面,那么就需要我们修改代码了,自定义界面可以实现的:
1. 默认的的扫码是横屏的,自定义后可以竖屏扫码
2. 修改扫码框布局,可以添加其他View
其实在设置IntentIntegrator时会有一个方法:setCaptureActivity(Activity),这个方法就是用来设置扫码界面的Activity,当不设置的时候会默认调用库作者自己写的CaptureActivity,我们可以一起看一下这个Activity里面做了什么:
public class CaptureActivity extends Activity {
private CaptureManager capture;
private DecoratedBarcodeView barcodeScannerView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
barcodeScannerView = initializeContent();
capture = new CaptureManager(this, barcodeScannerView);
capture.initializeFromIntent(getIntent(), savedInstanceState);
capture.decode();
}
...省略代码
}
这里有2个很重要的成员变量:CaptureManager和DecoratedBarcodeView,从他们的名字可以看出:
1. CaptureManager是用来拉起扫码和处理扫码结果的类
2. DecoratedBarcodeView则是一个显示扫码界面的自定义View
有了这个了解之后我们要自定义扫码界面就很明了了,再一起简单看一下DecoratedBarcodeView是个啥:
public class DecoratedBarcodeView extends FrameLayout {
private BarcodeView barcodeView;
private ViewfinderView viewFinder;
private TextView statusView;
...省略代码
}
了解了这三个View的作用之后我们就可以开始我们的自定义了,而这三个View具体怎么去扫码与解析并不是我们关心的重点我们直接跳过。自定义界面的步骤:
上代码,我自定义的activity:
public class CustomCaptureActivity extends Activity {
private CaptureManager capture;
private DecoratedBarcodeView barcodeScannerView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_custom_capture);// 自定义布局
barcodeScannerView = (DecoratedBarcodeView) findViewById(R.id.dbv_custom);
capture = new CaptureManager(this, barcodeScannerView);
capture.initializeFromIntent(getIntent(), savedInstanceState);
capture.decode();
}
@Override
protected void onResume() {
super.onResume();
capture.onResume();
}
@Override
protected void onPause() {
super.onPause();
capture.onPause();
}
@Override
protected void onDestroy() {
super.onDestroy();
capture.onDestroy();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
capture.onSaveInstanceState(outState);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
capture.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
return barcodeScannerView.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event);
}
}
自定义的布局:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical">
<com.journeyapps.barcodescanner.DecoratedBarcodeView
android:id="@+id/dbv_custom"
android:layout_width="match_parent"
android:layout_height="235dp"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:layout_marginTop="50dp"
app:zxing_preview_scaling_strategy="fitXY" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:text="我是新添加的按钮啊" />
LinearLayout>
调用方式:
new IntentIntegrator(this)
// 自定义Activity,重点是这行----------------------------
.setCaptureActivity(CustomCaptureActivity.class)
.setDesiredBarcodeFormats(IntentIntegrator.ONE_D_CODE_TYPES)// 扫码的类型,可选:一维码,二维码,一/二维码
.setPrompt("请对准二维码")// 设置提示语
.setCameraId(0)// 选择摄像头,可使用前置或者后置
.setBeepEnabled(false)// 是否开启声音,扫完码之后会"哔"的一声
.setBarcodeImageEnabled(true)// 扫完码之后生成二维码的图片
.initiateScan();// 初始化扫码
大家可以根据自己的需要定制自己想要的界面。
我们在使用zxing-android-embedded时会发现一点,那就是每次扫完码之后都会结束掉扫码界面Activity,然后在上一个Activity的onActivityResult获取扫码的结果,一般情况下这样的操作是可以满足我们项目中的需求,但是作者最近在开发的时候遇到了一个需求,需要在扫完码之后在不退出Activity的情况下弹出一个Dialog,然后在Dialog消失之后再扫码。找了一圈发现zxing-android-embedded没有提供这样的方法可以满足扫码后不结束扫码Activity。没办法逼得我去查阅源码看能否从源码下手,这里总结一下需求:
1. 首先扫码结束后不能结束扫码Activity
2. 在扫码Activity中能获取到扫码的结果
3. 获取到结果之后弹出Dialog,此时扫码应该是处于暂停状态,否则会出现未知问题
4. Dialog消失之后应该重新激活扫码
有了以上的需求,我们就从调用扫码的地方开始看起:
new IntentIntegrator(this)
.setCaptureActivity(CustomCaptureActivity.class)// 自定义Activity
.setDesiredBarcodeFormats(IntentIntegrator.ONE_D_CODE_TYPES)// 扫码的类型,可选:一维码,二维码,一/二维码
.setPrompt("请对准二维码")// 设置提示语
.setCameraId(0)// 选择摄像头,可使用前置或者后置
.setBeepEnabled(false)// 是否开启声音,扫完码之后会"哔"的一声
.setBarcodeImageEnabled(true)// 扫完码之后生成二维码的图片
.initiateScan();// 初始化扫码
构造方法中把当前的Activity设置到IntentIntegrator中,作者demo中对应的是MainActivity
public IntentIntegrator(Activity activity) {
this.activity = activity;
}
然后关键的第二行,设置扫码Activity
public IntentIntegrator setCaptureActivity(Class> captureActivity) {
this.captureActivity = captureActivity;
return this;
}
从第三行开始就是对一些属性的设置了,这些不是我们关心的重点,所以我们跳过。说了这么久还没有提到过IntentIntegrator这个类,这个类其实是我们调用扫码的一个入口,各种参数都是在这个类中设置,还记得我们的需求是扫完码之后不结束扫码的Activity码?我们把重心放到captureActivity上,但是结果却是让人失落的,这个类中并没有captureActivity相关的操作,不过在initiateScan()方法中我们可以找到一些蛛丝马迹。
public final void initiateScan() {
startActivityForResult(createScanIntent(), REQUEST_CODE);
}
这里只有简单的一行代码,让我们先看一下createScanIntent()中做了什么:
public Intent createScanIntent() {
Intent intentScan = new Intent(activity, getCaptureActivity());
intentScan.setAction(Intents.Scan.ACTION);
// check which types of codes to scan for
if (desiredBarcodeFormats != null) {
// set the desired barcode types
StringBuilder joinedByComma = new StringBuilder();
for (String format : desiredBarcodeFormats) {
if (joinedByComma.length() > 0) {
joinedByComma.append(',');
}
joinedByComma.append(format);
}
intentScan.putExtra(Intents.Scan.FORMATS, joinedByComma.toString());
}
intentScan.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intentScan.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
attachMoreExtras(intentScan);
return intentScan;
}
这段代码我们只需要关心第一行即可,从第一行我们可以知道这个方法会返回一个Intent,这个Intent就是从activity跳转到我们自定义的captureActivity,并且携带了很多设置的参数,让我返回到startActivityForResult()中:
protected void startActivityForResult(Intent intent, int code) {
if (fragment != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
fragment.startActivityForResult(intent, code);
}
} else if (supportFragment != null) {
supportFragment.startActivityForResult(intent, code);
} else {
activity.startActivityForResult(intent, code);
}
}
因为我们的扫码有可能是从fragment中调用的,所以框架的作者在这里分别做了对fragment和activity中调用扫码的处理,这里我们关心activity即可,很可惜,IntentIntegrator中没有我们能下手的地方,但是我们了解到扫码的操作其实都是在captureActivity做的,让我们把重点放到captureActivity中。
其实在第三节中我们我们已经看过这个Activity了,但是我们当时只关心自定义界面,所以忽略了一个重要的类:CaptureManager,这个类其实就是真正处理扫码的地方,所有的设置、界面、Activity的调用都会在CaptureManager完成,我们先看一下他的使用:
capture = new CaptureManager(this, barcodeScannerView);
capture.initializeFromIntent(getIntent(), savedInstanceState);
capture.decode();
构造方法中会把当前扫码Activity和扫码的View设置进去。
public CaptureManager(Activity activity, DecoratedBarcodeView barcodeView) {
this.activity = activity;
this.barcodeView = barcodeView;
barcodeView.getBarcodeView().addStateListener(stateListener);
handler = new Handler();
inactivityTimer = new InactivityTimer(activity, new Runnable() {
@Override
public void run() {
Log.d(TAG, "Finishing due to inactivity");
finish();
}
});
beepManager = new BeepManager(activity);
}
而initializeFromIntent()方法主要是从Intent中取出我们所设置的各种扫码的参数,这里就不给出源码了。
/**
* Start decoding.
*/
public void decode() {
barcodeView.decodeSingle(callback);
}
最后的decode()方法就是真正开始扫码的地方,我们可以看到这里barcodeView调用了decodeSingle方法开启扫码,还设置了一个回调进去,可以猜到这个callback就是用来接收扫码结果的回调了
private BarcodeCallback callback = new BarcodeCallback() {
@Override
public void barcodeResult(final BarcodeResult result) {
barcodeView.pause();
beepManager.playBeepSoundAndVibrate();
handler.post(new Runnable() {
@Override
public void run() {
returnResult(result);
}
});
}
@Override
public void possibleResultPoints(List resultPoints) {
}
};
在这个回调中看到一个returnResult(result),这个就是扫码之后返回结果的地方了,我们果然没有猜错
protected void returnResult(BarcodeResult rawResult) {
Intent intent = resultIntent(rawResult, getBarcodeImagePath(rawResult));
activity.setResult(Activity.RESULT_OK, intent);
closeAndFinish();
}
BarcodeResult就是我们的扫码结果了,第一行代码用来解析BarcodeResult并生成一个Intent,这个Intent就是用来携带扫码结果返回到onActivityResult的关键了,第三行代码的名字很明显就是用来结束Activity的方法了吧,跟进去看:
protected void closeAndFinish() {
if(barcodeView.getBarcodeView().isCameraClosed()) {
finish();
} else {
finishWhenClosed = true;
}
barcodeView.pause();
inactivityTimer.cancel();
}
private void finish() {
activity.finish();
}
罪魁祸首就是这个finish()方法了!!就是它,它把我们的Activity结束掉了,找到源头了就可以下手了。我们把它给注释掉就结束不了我们的Activity了。但是现在Activity不结束了,但是我们要把结果给返回回去啊,那我们自己得加个回调了
public interface ResultCallBack {
void callBack(int requestCode, int resultCode, Intent intent)
}
private ResultCallBack mResultCallBack;
public void setResultCallBack(ResultCallBack resultCallBack) {
this.mResultCallBack = resultCallBack;
}
修改后的returnResult方法为下,这里我修改的是returnResult方法,而并没有修改上面的closeAndFinish方法,因为closeAndFinish方法不只是在返回扫码结构的时候调用了,所以为了防止出问题就修改是returnResult方法
/**
* 修改此方法,实现扫码之后不返回界面
*/
protected void returnResult(BarcodeResult rawResult) {
Intent intent = resultIntent(rawResult, getBarcodeImagePath(rawResult));
activity.setResult(Activity.RESULT_OK, intent);
if (barcodeView.getBarcodeView().isCameraClosed()) {
if (null != mResultCallBack) {
mResultCallBack.callBack(IntentIntegrator.REQUEST_CODE, Activity.RESULT_OK, intent);
}
// activity.finish(); 注释掉这行
} else {
finishWhenClosed = true;
}
barcodeView.pause();
inactivityTimer.cancel();
}
到这里为止我们的修改就完了,其实改动非常小,就几行代码就可以实现扫完码之后不结束扫码Activity,但是重要的是思考的过程和阅读源码的过程,刚拿到那些需求然后发现第三方库实现不了的时候我也是非常慌的,也尝试过修改源码但是没有成功,最后一次静下心来慢慢翻看源码终于搞定了,这也是对我自己的一次提升吧,谢谢大家阅读文章,最后附上修改后的代码:
修改后的CustomCaptureActivity:
public class CustomCaptureActivity extends Activity {
private CaptureManager capture;
private DecoratedBarcodeView barcodeScannerView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_custom_capture);// 自定义布局
barcodeScannerView = (DecoratedBarcodeView) findViewById(R.id.dbv_custom);
capture = new CaptureManager(this, barcodeScannerView);
capture.initializeFromIntent(getIntent(), savedInstanceState);
capture.setResultCallBack(new CaptureManager.ResultCallBack() {
@Override
public void callBack(int requestCode, int resultCode, Intent intent) {
IntentResult result = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent);
if (null != result && null != result.getContents()) {
showDialog(result.getContents());
}
}
});
capture.decode();
}
public void showDialog(String result) {
// 弹出dialog的代码略...
// 重新拉起扫描
capture.onResume();
capture.decode();
}
@Override
protected void onResume() {
super.onResume();
capture.onResume();
}
@Override
protected void onPause() {
super.onPause();
capture.onPause();
}
@Override
protected void onDestroy() {
super.onDestroy();
capture.onDestroy();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
capture.onSaveInstanceState(outState);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
capture.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
return barcodeScannerView.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event);
}
}
修改后的CaptureManager:
public class CaptureManager{
... 省略代码
/**
* 修改此方法,实现扫码之后不返回界面
*
* @author mark.liu
* created at 2017-9-5
*/
protected void returnResult(BarcodeResult rawResult) {
Intent intent = resultIntent(rawResult, getBarcodeImagePath(rawResult));
activity.setResult(Activity.RESULT_OK, intent);
if (barcodeView.getBarcodeView().isCameraClosed()) {
if (null != mResultCallBack) {
mResultCallBack.callBack(IntentIntegrator.REQUEST_CODE, Activity.RESULT_OK, intent);
}
// activity.finish(); 注释这一行
} else {
finishWhenClosed = true;
}
barcodeView.pause();
inactivityTimer.cancel();
}
public interface ResultCallBack {
void callBack(int requestCode, int resultCode, Intent intent);
}
private ResultCallBack mResultCallBack;
public void setResultCallBack(ResultCallBack resultCallBack) {
this.mResultCallBack = resultCallBack;
}
... 省略代码
}