最近接到一个半屏扫描二位码的需求,要实现界面的上半屏是扫描二维码,下半屏是扫描结果数据的显示。接到这个需求第一时间想到的是,印象中人生中没有实现过半屏扫描二维码的工作经验!!!难受啊,但是为了保持自己的专业性!就硬着头皮答应了下来:“OK,没问题,我改一下就好”
我是不是答应的太快了??我代码都还没看过呢!!作为一个专业的Android工程师,这样评估一个需求工作量是完成不靠谱的!哎,答应都答应了,那就开干吧。
首先要接入Zxing二维码扫描包
compile files('libs/ZXing-core-3.3.3.jar')
接入后,你会发现扫描的主要页面activity是叫这个名字的:CaptureActivity,然后这个是全屏的扫描,打开的界面大概是下面这个样子的。
而扫描获取到的二维码数据是通过setResult()回调到你需要这个数据的界面。也就是说,我们调用二维码扫描是用startActivityForResult()调的。
好吧,这个简单的知识估计大家都知道。那就是接着继续做吧。首先先显示半屏的功能,我有个大胆想法,要实现半屏是不是可以用个控件把里面的扫描框顶上去就好了。好咧那就试试吧,先粗略看一下这个扫二维码的界面布局是怎样写的。
上图就是扫描的界面,一个SurfaceView,一个ViewfindView,还有一个TextView,简单啦。看我改一下,把它改成半屏。用最快速度改一下布局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical" >
<SurfaceView
android:id="@+id/surfaceview"
android:layout_width="match_parent"
android:layout_height="match_parent" >
</SurfaceView>
<com.zxing.android.view.ViewfinderView
android:id="@+id/viewfinderview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#00000000" >
</com.zxing.android.view.ViewfinderView>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:gravity="center_horizontal"
android:text="@string/msg_default_status"
android:textSize="15sp" >
</TextView>
<include layout="@layout/toolbar"/>
</RelativeLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
</LinearLayout>
运行后效果:
仔细看,会发现相机的预览界面被挤压的变形了,而我们想让扫码框往上移的功能也没有实现。很明显这种操作是不行的,难受啊。我们再看看扫描的源码吧,这次要带着目的性去看。带着问题去看,“到底是在哪里控制着扫描框的呢?”。一切的起源是在CaptureActivity。我们看看它里面的代码吧
package com.zxing.android;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.util.Vector;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.res.AssetFileDescriptor;
import android.graphics.Bitmap;
import android.graphics.Camera;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnCompletionListener;
import android.os.Bundle;
import android.os.Handler;
import android.os.Vibrator;
import android.view.KeyEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceHolder.Callback;
import android.view.SurfaceView;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.TextView;
import com.emms.R;
import com.emms.activity.BaseActivity;
import com.emms.activity.CusActivity;
import com.emms.activity.NfcUtils;
import com.emms.schema.Factory;
import com.emms.util.BaseData;
import com.emms.util.Constants;
import com.emms.util.DataUtil;
import com.emms.util.LocaleUtils;
import com.emms.util.LogUtils;
import com.emms.util.NumberFormatUtil;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.Result;
import com.zxing.android.camera.CameraManager;
import com.zxing.android.decoding.CaptureActivityHandler;
import com.zxing.android.decoding.InactivityTimer;
import com.zxing.android.view.ViewfinderView;
public class CaptureActivity extends BaseActivity implements Callback {
private CaptureActivityHandler handler;
private ViewfinderView viewfinderView;
private SurfaceView surfaceView;
private boolean hasSurface;
private Vector<BarcodeFormat> decodeFormats;
private String characterSet;
private InactivityTimer inactivityTimer;
private MediaPlayer mediaPlayer;
private boolean playBeep;
// private static final float BEEP_VOLUME = 0.10f;
private boolean vibrate;
CameraManager cameraManager;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_capture);
((TextView)findViewById(R.id.tv_title)).setText(LocaleUtils.getI18nValue("sacn_qr_code"));
findViewById(R.id.btn_right_action).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
surfaceView = (SurfaceView) findViewById(R.id.surfaceview);
viewfinderView = (ViewfinderView) findViewById(R.id.viewfinderview);
Window window = getWindow();
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
hasSurface = false;
inactivityTimer = new InactivityTimer(this);
}
@Override
protected void onResume() {
super.onResume();
if (getRequestedOrientation() != ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
// CameraManager.init(getApplication());
cameraManager = new CameraManager(getApplication());
viewfinderView.setCameraManager(cameraManager);
SurfaceHolder surfaceHolder = surfaceView.getHolder();
if (hasSurface) {
initCamera(surfaceHolder);
} else {
surfaceHolder.addCallback(this);
surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
}
decodeFormats = null;
characterSet = null;
playBeep = true;
AudioManager audioService = (AudioManager) getSystemService(AUDIO_SERVICE);
if (audioService.getRingerMode() != AudioManager.RINGER_MODE_NORMAL) {
playBeep = false;
}
initBeepSound();
vibrate = true;
}
@Override
protected void onPause() {
super.onPause();
if (handler != null) {
handler.quitSynchronously();
handler = null;
}
cameraManager.closeDriver();
}
@Override
protected void onDestroy() {
inactivityTimer.shutdown();
super.onDestroy();
}
private void initCamera(SurfaceHolder surfaceHolder) {
try {
// CameraManager.get().openDriver(surfaceHolder);
cameraManager.openDriver(surfaceHolder);
} catch (IOException ioe) {
return;
} catch (RuntimeException e) {
return;
}
if (handler == null) {
handler = new CaptureActivityHandler(this, decodeFormats, characterSet);
}
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
if (!hasSurface) {
hasSurface = true;
initCamera(holder);
}
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
hasSurface = false;
}
public CameraManager getCameraManager() {
return cameraManager;
}
public ViewfinderView getViewfinderView() {
return viewfinderView;
}
public Handler getHandler() {
return handler;
}
public void drawViewfinder() {
viewfinderView.drawViewfinder();
}
public void handleDecode(Result obj, Bitmap barcode) {
inactivityTimer.onActivity();
playBeepSoundAndVibrate();
LogUtils.e("获取到扫描数据----->"+obj.toString());
showResult(obj, barcode);
}
protected void showResult(final Result rawResult, Bitmap barcode) {
inactivityTimer.onActivity();
Intent it = new Intent(CaptureActivity.this, CusActivity.class);
if(BaseData.getConfigData().get(BaseData.TASK_GET_EQUIPMENT_DATA_FROM_ICCARD_ID)==null) {
switch (getLoginInfo().getFromFactory()) {
case Factory.FACTORY_GEW: {
it.putExtra("result", rawResult.getText());
break;
}
case Factory.FACTORY_EGM: {
try {
//rawResult.getText().toCharArray();
it.putExtra("result",new BigInteger(rawResult.getText(),16).toString());
// it.putExtra("result", NumberFormatUtil.HexToLongString(rawResult.getText()));
} catch (Exception e) {
it.putExtra("result", rawResult.getText());
}
break;
}
default: {
it.putExtra("result", rawResult.getText());
break;
}
}
}else {
switch (DataUtil.isDataElementNull(BaseData.getConfigData().get(BaseData.TASK_GET_EQUIPMENT_DATA_FROM_ICCARD_ID))){
case "1":{
try {
it.putExtra("result", new BigInteger(rawResult.getText(),16).toString());
} catch (Exception e) {
it.putExtra("result", rawResult.getText());
}
break;
}
case "2":{
it.putExtra("result", rawResult.getText());
break;
}
default:{
it.putExtra("result", rawResult.getText());
break;
}
}
}
setResult(Constants.RESULT_CODE_CAPTURE_ACTIVITY_TO_TASK_DETAIL, it);
finish();
}
public void restartPreviewAfterDelay(long delayMS) {
if (handler != null) {
handler.sendEmptyMessageDelayed(MessageIDs.restart_preview, delayMS);
}
}
private void initBeepSound() {
if (playBeep && mediaPlayer == null) {
// The volume on STREAM_SYSTEM is not adjustable, and users found it
// too loud,
// so we now play on the music stream.
setVolumeControlStream(AudioManager.STREAM_MUSIC);
mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setOnCompletionListener(beepListener);
try {
AssetFileDescriptor fileDescriptor = getAssets().openFd("qrbeep.ogg");
this.mediaPlayer.setDataSource(fileDescriptor.getFileDescriptor(), fileDescriptor.getStartOffset(),
fileDescriptor.getLength());
this.mediaPlayer.setVolume(0.1F, 0.1F);
this.mediaPlayer.prepare();
} catch (IOException e) {
this.mediaPlayer = null;
}
}
}
private static final long VIBRATE_DURATION = 200L;
private void playBeepSoundAndVibrate() {
if (playBeep && mediaPlayer != null) {
mediaPlayer.start();
}
if (vibrate) {
Vibrator vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);
vibrator.vibrate(VIBRATE_DURATION);
}
}
/**
* When the beep has finished playing, rewind to queue up another one.
*/
private final OnCompletionListener beepListener = new OnCompletionListener() {
public void onCompletion(MediaPlayer mediaPlayer) {
mediaPlayer.seekTo(0);
}
};
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
setResult(RESULT_CANCELED);
finish();
return true;
} else if (keyCode == KeyEvent.KEYCODE_FOCUS || keyCode == KeyEvent.KEYCODE_CAMERA) {
return true;
}
return super.onKeyDown(keyCode, event);
}
public static String str2HexStr(String str)
{
char[] chars = "0123456789ABCDEF".toCharArray();
StringBuilder sb = new StringBuilder("");
byte[] bs = str.getBytes();
int bit;
for (int i = 0; i < bs.length; i++)
{
bit = (bs[i] & 0x0f0) >> 4;
sb.append(chars[bit]);
bit = bs[i] & 0x0f;
sb.append(chars[bit]);
sb.append(' ');
}
return sb.toString().trim();
}
}
都说看代码从oncreate方法开始看,所以我们可以看到这里面初始化了两个关键的控件,surfaceView和viewfinderView。这两个控件一个是用来做相机预览的(surfaceView),一个是就是那个我们要找的扫码框(viewfinderView)。所以我们可以解析了为什么预览界面会变形了,因为原本全屏的surfaceView被我强制挤压变成了半屏。所以我们还是调整回原来的布局,其实我们只要想办法把viewfinderView往上移,就可以显示半屏显示了。
我们找找哪里有配置viewfinderView的,Ctrl+F后你会发现,在onResume方法有用到viewfinderView
@Override
protected void onResume() {
super.onResume();
if (getRequestedOrientation() != ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
// CameraManager.init(getApplication());
cameraManager = new CameraManager(getApplication());
viewfinderView.setCameraManager(cameraManager);
SurfaceHolder surfaceHolder = surfaceView.getHolder();
if (hasSurface) {
initCamera(surfaceHolder);
} else {
surfaceHolder.addCallback(this);
surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
}
decodeFormats = null;
characterSet = null;
playBeep = true;
AudioManager audioService = (AudioManager) getSystemService(AUDIO_SERVICE);
if (audioService.getRingerMode() != AudioManager.RINGER_MODE_NORMAL) {
playBeep = false;
}
initBeepSound();
vibrate = true;
}
这里先创建了一个CameraManager的对象,然后viewfinderView把对象给设置进去了。看看里面用的CameraManager对象干了什么事情
Override
public void onDraw(Canvas canvas) {
// 中间的扫描框,你要修改扫描框的大小,去CameraManager里面修改
Rect frame = cameraManager.getFramingRect();
if (frame == null) {
return;
}
可以看到在ViewFinder的onDraw方法里面用了cameraManager.getFramingRect(),这里的意思就是获取需要扫描框要显示的位置,或者说是摆放的位置,来了来了,真的来了。也就是说,CameraManager这个东西是决定ViewFinderView的摆放位置的。我们去看看CameraManager,下面就是我们的关键代码了。
/**
* Calculates the framing rect which the UI should draw to show the user
* where to place the barcode. This target helps with alignment as well as
* forces the user to hold the device far enough away to ensure the image
* will be in focus.
*
* @return The rectangle to draw on screen in window coordinates.
*/
public Rect getFramingRect() {
if (framingRect == null) {
if (camera == null) {
return null;
}
if(configManager.getScreenResolution()==null){
return null;
}
Point screenResolution = configManager.getScreenResolution();
int width = screenResolution.x * 3 / 4;
LogUtils.e("执行这里的width--->"+width);
if (width < MIN_FRAME_WIDTH) {
width = MIN_FRAME_WIDTH;
} else if (width > 800) {
width = MAX_FRAME_WIDTH;
} else {
width = MID_FRAME_WIDTH;
}
int height = screenResolution.y * 3 / 4;
if (height < MIN_FRAME_HEIGHT) {
height = MIN_FRAME_HEIGHT;
} else if (height > 1400) {
height = MAX_FRAME_HEIGHT;
} else {
height = MID_FRAME_HEIGHT;
}
LogUtils.e("width相减的结果---->"+(screenResolution.x-width));
LogUtils.e("height相减的结果---->"+(screenResolution.y-height));
int leftOffset = (screenResolution.x - width)/2;
int topOffset = (screenResolution.y - height)*2/5;
LogUtils.e("最后的左边距--->"+leftOffset);
LogUtils.e("最后的顶边距--->"+topOffset);
if (isHalfSize){
framingRect = new Rect(leftOffset, 80, leftOffset + width, width);
}else{
framingRect = new Rect(leftOffset, topOffset, leftOffset + width, topOffset + height);
}
// framingRect = new Rect(leftOffset, topOffset, leftOffset + width, topOffset + height);
LogUtils.e("底部的高度--->"+height);
Log.d(TAG, "Calculated framing rect: " + framingRect);
}
return framingRect;
}
在CameraManager的getFramingRect()方法里面
if (isHalfSize){
framingRect = new Rect(leftOffset, 80, leftOffset + width, width);
}else{
framingRect = new Rect(leftOffset, topOffset, leftOffset + width, topOffset + height);
}
上面的两行代码决定了扫描框的摆放位置,所以我加了一个判断,isHalfSize是否半屏显示来控制扫描框的位置,从而实现半屏的显示。说了这么多,贴了这么多代码,终于找到了需要修改的地方。真的是看代码三天,修改三分钟。修改完成后,我们运行显示看一下。
没想到,我截图上传居然变形了,大家不要误会,这是图片变形了,并不是扫描界面变形了。所以说,最后的成果就是这样啦,显示了,上面扫描下面同步显示数据。这样的一个功能就完成了。除了实现半屏以外,其实还可以实现自定义界面,不一定要在CaptureActivity界面修改,我们还可以在需要的界面要接入扫描的功能,去自定义。不过这里面还涉及了要修改其他的文件和代码,这里就不一一展开了。需要实现类型的功能的,可以私信我,我们一起研究哈哈!
希望可以帮到你们,我是一名Android工程师,确不甘于只有工程师!我们一起加油,向往美好的生活。