安卓开发学习之二维码的生成、保存和扫描

背景

近两日辞了实习的工作以准备毕业,现在写完了毕设论文,只待飞赴学校。现在无事可做,便从二维码的学习开始,再次学习新的技术知识,于是记录这个demo项目的完成过程。

导包

在项目gradle文件的指明使用仓库里,加上jitpack.io

buildscript {
    
    repositories {
        ...
        maven { url 'https://jitpack.io' }
    }
    ...
}

allprojects {
    repositories {
        ...
        maven { url 'https://jitpack.io' }
    }
}

...

再到模块gradle文件里,加上依赖

implementation 'com.github.jwkj:LibZXing:v1.0.4'

主界面

主界面包括三个控件:一个编辑区、一个按钮和一张二维码图片

编辑区用来输入内容,根据输入的内容生成二维码

按钮用来启动二维码的生成或扫描

二维码图片长摁保存到内部存储根目录下

主界面的xml文件内容如下

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center"
    tools:context=".MainActivity">

    <EditText
        android:id="@+id/content_str"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    <Button
        android:id="@+id/btn_generateZxing"
        android:text="@string/generate_qr_code"
        android:layout_gravity="center"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    <ImageView
        android:id="@+id/content_zxing"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</LinearLayout>

主界面的逻辑实现

权限的赋予

主要用到摄像头和写存储两个权限,安卓高版本需要动态申请。我这里用了一个第三方权限库来实现之

		PermissonUtil.checkPermission(this, new PermissionListener() {
			@Override
			public void havePermission() {

			}

			@Override
			public void requestPermissionFail() {
				finish();
			}
		}, Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE);

依赖地址如下

implementation 'com.github.tyhjh:picturePickUtil:v1.0.6'

设置控件的监听

主要是按钮的点击和图片的长摁

		final EditText editText = findViewById(R.id.content_str);
		final ImageView contentZxing = findViewById(R.id.content_zxing);
		final Button btnGenerateQRCode = findViewById(R.id.btn_generateZxing);
		btnGenerateQRCode.setText(mRes.getString(R.string.generate_qr_code));
		btnGenerateQRCode.setOnClickListener(new View.OnClickListener() {
			@Override
			public void onClick(View view) {
				....
			}
		});
		contentZxing.setOnLongClickListener(new View.OnLongClickListener() {
			@Override
			public boolean onLongClick(View view) {
				....
				return false;
			}
		});

二维码的生成和扫描

首先获取一下QRCodeManager的单例,因为它是二维码生成、扫描和结果处理的管家

mQRCodeManager = QRCodeManager.getInstance();

然后让MainActivity覆写onActivityResult(),在里面调用QRCodeManager的同名方法,让其进行处理。只有这样,我们设置的扫描回调方法才能被调用。

	@Override
	protected void onActivityResult(int requestCode, int resultCode, Intent data) {
		super.onActivityResult(requestCode, resultCode, data);
		mQRCodeManager.onActivityResult(requestCode, resultCode, data);
	}

然后就可以在按钮的点击监听里,根据按钮的提示文字的不同,来实现二维码的生成和扫描回调的设置了

		btnGenerateQRCode.setOnClickListener(new View.OnClickListener() {
			@Override
			public void onClick(View view) {
				if (btnGenerateQRCode.getText().toString().equals(mRes.getString(R.string.generate_qr_code))) {
					String text = TextUtils.isEmpty(editText.getText()) ? "szc" : editText.getText().toString();
					contentZxing.setImageBitmap(mQRCodeManager.createQRCode(text, 500, 500));
					btnGenerateQRCode.setText(mRes.getString(R.string.begin_scan));
				} else {
					mQRCodeManager.with(MainActivity.this).setReqeustType(0).scanningQRCode(new OnQRCodeScanCallback() {
						@Override
						public void onCompleted(String s) {
							System.out.println("扫描完成:" + s);
						}

						@Override
						public void onError(Throwable throwable) {
							throwable.printStackTrace();
						}

						@Override
						public void onCancel() {
							System.out.println("扫描取消");
						}
					});
					btnGenerateQRCode.setText(mRes.getString(R.string.generate_qr_code));
				}
			}
		});

其中生成二维码用到的方法是mQRCodeManager.createQRCode(text, widthPix, heightPix),这个方法会生成一个二维码位图对象,如果要生成带头像的二维码对象,则调用重载方法mQRCodeManager.createQRCode(text, widthPix, heightPix, bitmap)就行,最后一个参数就是头像的位图对象,可以用本地的,也可以用Picasso等加载网络的。

二维码的扫描里,主要是回调接口里的三个方法,其中成功时的回调onCompleted()的参数s,就是二维码的内容,可能是一个网址,这样就可以让webview进行加载了。

保存二维码图片

这个功能在图片的长摁里实现

		contentZxing.setOnLongClickListener(new View.OnLongClickListener() {
			@Override
			public boolean onLongClick(View view) {
				new Thread(new Runnable() {
					@Override
					public void run() {
						try {
							Context context = MainActivity.this;
							contentZxing.setDrawingCacheEnabled(true);
							final Bitmap qrCodeBitmap = contentZxing.getDrawingCache();

							File destFile = new File(getRootIntervalStorage(), "qrCode.jpg");
							if (destFile.exists()) {
							    destFile.delete();
							}
							destFile.createNewFile();
							BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(destFile));
							qrCodeBitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);
							outputStream.flush();
							outputStream.close();

							MediaScannerConnection.scanFile(context, new String[]{destFile.toString()}, null, new MediaScannerConnection.OnScanCompletedListener() {
								@Override
								public void onScanCompleted(String s, Uri uri) {
									System.out.println("扫描完成:" + s + ",uri:" + uri.toString());
								}
							});
                            contentZxing.setDrawingCacheEnabled(false);
							System.out.println("保存成功");
						} catch (Exception e) {
							e.printStackTrace();
						}
					}
				}).start();
				return false;
			}
		});

由于是文件操作,所以新开了一个线程。

获取imageView的位图对象,是通过获取其缓存实现的,关键代码如下

                            contentZxing.setDrawingCacheEnabled(true);
                            final Bitmap qrCodeBitmap = contentZxing.getDrawingCache();
                            ...
                            contentZxing.setDrawingCacheEnabled(false);

其中,使能和禁止缓存的方法要成对出现,否则可能报错位图已经被回收或位图为空

然后就是位图对象的压缩和输出,对应代码

                            File destFile = new File(getRootIntervalStorage(), "qrCode.jpg");
                            if (destFile.exists()) {
                                destFile.delete();
                            }
                            destFile.createNewFile();
                            BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(destFile));
                            qrCodeBitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);
                            outputStream.flush();
                            outputStream.close();

其中为了保存到内部存储根目录下,我自定义了方法getRootIntervalStorage()。

	private File getRootIntervalStorage() {
		File appCacheFile = getExternalCacheDir();
		while (!appCacheFile.getAbsolutePath().endsWith("0") && !appCacheFile.getAbsolutePath().endsWith("0" + File.separator)) {
			appCacheFile = appCacheFile.getParentFile();
		}
		return appCacheFile;
	}

由于应用的外部缓存目录路径是/storage/emulated/0/Android/data/包名/cache,而内部存储根目录的路径是/storage/emulated/0,所以我们就能进行回溯了。

最后要通知一下系统图库进行扫描,否则在扫描本地二维码图片时,读取不到图片数据(这个库是用cursor读取本地图片的,如果不更新图库的话,会导致cursor读不到_data这一列,因此无法识别二维码,还报错Failure delievery...,其实是数组越界)。对应代码

                                MediaScannerConnection.scanFile(context, new String[]{destFile.toString()}, null, new MediaScannerConnection.OnScanCompletedListener() {
                                @Override
                                public void onScanCompleted(String s, Uri uri) {
                                    System.out.println("扫描完成:" + s + ",uri:" + uri.toString());
                                }
                            });

结果测试

应用起来时的主界面

输入一段文字后,点击生成二维码

长摁二维码图片后日志输出和图库显示结果

点击扫描二维码

结果分析及源码简读

从扫描时可以看出来,他调用QRCodeManager.scanningQRCode()时启动了一个新的activitiy,对应源码如下

    public QRCodeManager scanningQRCode(OnQRCodeScanCallback callback) {
        this.callback = callback;
        this.scanning(410);
        return this;
    }

    void scanning(int requestCode) {
        this.curRequestCode = requestCode;
        Intent intent = new Intent(this.context, CaptureActivity.class);
        intent.putExtra("type", this.requestType);
        this.context.startActivityForResult(intent, 410);
    }

启动的是CaptureActivity,我们点进去看看它的onCreate()方法,其中主要代码如下

        this.scanPreview = (SurfaceView)this.findViewById(id.capture_preview);
        this.scanContainer = (RelativeLayout)this.findViewById(id.capture_container);
        this.scanCropView = (RelativeLayout)this.findViewById(id.capture_crop_view);
        this.scanLine = (ImageView)this.findViewById(id.capture_scan_line);
        this.ivBack = (ImageView)this.findViewById(id.iv_back);
        this.ivMullt = (ImageView)this.findViewById(id.iv_mudle);
        this.tvAlbum = (TextView)this.findViewById(id.tv_capture_select_album);
        this.tvAlbum.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                Intent intent = new Intent("android.intent.action.GET_CONTENT");
                intent.addCategory("android.intent.category.OPENABLE");
                intent.setType("image/*");
                CaptureActivity.this.startActivityForResult(intent, 101);
            }
        });
        this.ivBack.setTag(123);
        this.ivMullt.setTag(124);
        this.ivBack.setOnClickListener(this);
        this.ivMullt.setOnClickListener(this);
        this.inactivityTimer = new InactivityTimer(this);
        this.beepManager = new BeepManager(this);
        if (this.captureType == 1) {
            this.ivMullt.setVisibility(4);
        }

        TranslateAnimation animation = new TranslateAnimation(2, 0.0F, 2, 0.0F, 2, 0.0F, 2, 0.9F);
        animation.setDuration(4500L);
        animation.setRepeatCount(-1);
        animation.setRepeatMode(1);
        this.scanLine.startAnimation(animation);

其中scanPreview就是预览画面,scanCropView是预览框,scanLine是扫描线。扫描线用TranslateAnimation控制其上下移动,也就是个动画

而从相册中读取图片则是从tvAlbum的点击开始的,对应那一段onClick()回调。从内容可以看到,是读取了系统图库,获取内容。然后请求码是101,在onActivityResult()中处理

    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (resultCode == -1 && requestCode == 101) {
            String picPath = SelectAlbumUtils.getPicPath(this, data);
            Result result = DecodeBitmap.scanningImage(picPath);
            if (result == null) {
                Toast.makeText(this, this.getString(string.pic_no_qrcode), 0).show();
            } else {
                this.beepManager.playBeepSoundAndVibrate();
                String scanResult = DecodeBitmap.parseReuslt(result.toString());
                this.scanDeviceSuccess(scanResult, new Bundle());
            }
        }

    }

找到图片后,先获取路径,再扫描图片,最后处理结果

获取路径的话,调用SelectAlbumUtils.getPicPath()方法,此方法会最终走到SelectAlbumUtils.getImagePath()方法,代码如下

    private static String getImagePath(Context context, Uri uri, String selection) {
        String Path = null;
        Cursor cursor = context.getContentResolver().query(uri, (String[])null, selection, (String[])null, (String)null);
        if (cursor != null) {
            if (cursor.moveToFirst()) {
                Path = cursor.getString(cursor.getColumnIndex("_data"));
            }

            cursor.close();
        }

        return Path;
    }

保存二维码图片后,如果不通知图库进行更新,cursor.getColumnIndex("_data")会返回-1,断点调试发现游标里没有_data这一列,因此报错退出。

如果是用相机扫描的话,从获取相机预览数据到结果回调,调用的关键方法历次是PreviewCallback.onPreviewFrame()、DecodeHandler.decode()、CaptureActivityHandler.handleMessage()、CaptureActivity.handleDecode()、MainActivity.onActivityResult()、QRCodeManager.onActivityResult()、OnQRCodeScanCallback.onCompleted()。有兴趣可以看看怎么解码的

 

结语

这个库挺好用,只是扫描界面不能自定义,不过可以把库克隆下来,把UI界面改一改,就可以做到自定义了。

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值