这是一篇关于二维码生成的故事,你确定不进来看看?
一、前言
在一个风和日丽的下午,接到一个海报与二维码拼接成一张图的需求,如下图
咋一看,很简单,就是海报图(Bitmap)和二维码(Bitmap)然后合并成一张图就行。然而问题就是发生在二维码的生成上,我们先看看如何生成一张二维码图。
二、如何生成二维码?
因为需要对数据进行编码,需要用到第三方库Zxing。
2.1 如何引入?
implementation 'com.google.zxing:core:3.3.0'
2.2 编写工具类
直接上代码,代码就不过多解释了,网上也很多,不懂的多看看注释。
public class QRCodeUtil { public static Bitmap createQRCodeBitmap(String content, int width, int height, String margin) { return createQRCodeBitmap(content, width, height, "UTF-8", "L", "", margin, Color.BLACK, Color.WHITE); } public static Bitmap createQRCodeBitmap(String content, int width, int height, String qrCodeVersion, String margin) { return createQRCodeBitmap(content, width, height, "UTF-8", "L", qrCodeVersion, margin, Color.BLACK, Color.WHITE); } / * 生成简单二维码 * * @param content 字符串内容 * @param width 二维码宽度 * @param height 二维码高度 * @param character_set 编码方式(一般使用UTF-8) * @param error_correction_level 容错率 L:7% M:15% Q:25% H:35%(可不填,默认L) * @param qrCodeVersion 二维码版本号(可不填,默认根据content的数据提供QR码的最小版本) * @param margin 二维码与边框的边距(可不填,默认4) * @param color_black 黑色色块(可自定义) * @param color_white 白色色块(可自定义) * @return BitMap */ public static Bitmap createQRCodeBitmap(String content, int width, int height, String character_set, String error_correction_level, String qrCodeVersion, String margin, int color_black, int color_white) { // 字符串内容判空 if (TextUtils.isEmpty(content)) { return null; } // 宽和高>=0 if (width < 0 || height < 0) { return null; } try { //1.设置二维码相关配置 Hashtable hints = new Hashtable(); // 字符转码格式设置 if (!TextUtils.isEmpty(character_set)) { hints.put(EncodeHintType.CHARACTER_SET, character_set); } // 容错率设置 if (!TextUtils.isEmpty(error_correction_level)) { hints.put(EncodeHintType.ERROR_CORRECTION, error_correction_level); } // 空白边距设置 if (!TextUtils.isEmpty(margin)) { hints.put(EncodeHintType.MARGIN, margin); } // QRCODE版本设置 if (!TextUtils.isEmpty(qrCodeVersion)) { hints.put(EncodeHintType.QR_VERSION, qrCodeVersion); } //2.将配置参数传入到QRCodeWriter的encode方法生成BitMatrix(位矩阵)对象 BitMatrix bitMatrix = new QRCodeWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints); //3.创建像素数组,并根据BitMatrix(位矩阵)对象为数组元素赋颜色值 int[] pixels = new int[width * height]; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { //bitMatrix.get(x,y)方法返回true是黑色色块,false是白色色块 if (bitMatrix.get(x, y)) { pixels[y * width + x] = color_black;//黑色色块像素设置 } else { pixels[y * width + x] = color_white;// 白色色块像素设置 } } } //4.创建Bitmap对象,根据像素数组设置Bitmap每个像素点的颜色值,并返回Bitmap对象 Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); bitmap.setPixels(pixels, 0, width, 0, 0, width, height); return bitmap; } catch (WriterException e) { e.printStackTrace(); return null; } }}
既然生成二维码的方法有了,那我们就看看生成的效果是什么样的?
首先,肯定要看看设计的要求。宽高176*176,二维码与外边框的边距,我们先统一成10。
代码编写
在这里我弄了三个同等大小的二维码,但是他们margin不同。
val content = "www.baidu.com" val codeBitmap = QRCodeUtil.createQRCodeBitmap(content, 176, 176,"10") val codeBitmap1 = QRCodeUtil.createQRCodeBitmap(content, 176, 176,"4") val codeBitmap2 = QRCodeUtil.createQRCodeBitmap(content, 176, 176,"0") val findViewById = findViewById(R.id.iv_code) val findViewById1 = findViewById(R.id.iv_code1) val findViewById2 = findViewById(R.id.iv_code2) findViewById.setImageBitmap(codeBitmap) findViewById1.setImageBitmap(codeBitmap1) findViewById2.setImageBitmap(codeBitmap2)
布局文件
效果图
看完后你觉得惊讶了嘛?
- 第一个二维码,也就是按照设计的宽高(176*176px)和margin(10px)设置的,结果这margin值相差到离谱
- 第二个二维码区别就只有margin为4px,结果margin值好像还看的过去。
- 第三个二维码区别就只有margin好像没有,那我设置的margin呢?
最后,UI看到后,肯定是不满意的。这做的什么鬼?
虽然最后问题肯定解决了,但是本着对代码的探索,我们一起去分析问题。
三、问题分析
首先,三个二维码的值区别就是margin的不同,那我们主要分析是什么导致他的margin都大不相同。
第一步:我们从工具类入手,发现其实就一行代码是Zxing,也就是最关键的编码代码。如下:
BitMatrix bitMatrix = new QRCodeWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints);
我们跟着encode方法进去看看。可以看到,在这个方法里,核心就两句,如下:
QRCode code = Encoder.encode(contents, errorCorrectionLevel, hints); return renderResult(code, width, height, quietZone);
- 第一行代码:也就是真正对数据进行编码方法
- 第二行代码:返回一个BitMatrix,也就是一个二阶矩阵的位信息,存储着二维码每一个点的信息。
我们先从renderResult方法入手,因为这是最终确定二维码大小的。代码如下:
private static BitMatrix renderResult(QRCode code, int width, int height, int quietZone) { ByteMatrix input = code.getMatrix(); int inputWidth = input.getWidth(); int inputHeight = input.getHeight(); int qrWidth = inputWidth + (quietZone * 2); int qrHeight = inputHeight + (quietZone * 2); int outputWidth = Math.max(width, qrWidth); int outputHeight = Math.max(height, qrHeight); int multiple = Math.min(outputWidth / qrWidth, outputHeight / qrHeight); int leftPadding = (outputWidth - (inputWidth * multiple)) / 2; int topPadding = (outputHeight - (inputHeight * multiple)) / 2;}
咋一看,很难看出什么,我们可以debug调试一下。调试信息如下:
看上面三个标注的红框所代表的意思
-
inputWidth,inputHeight,其实是二维码的真实的大小,无论你外面传入的二维码宽高是176,还是1760,他都不会变。因为他的值是根据二维码版本号来决定的。(后面会介绍这个值的由来)
-
qrWidth,qrHeight,其实就是加上了quietZone的大小,quietZone也就是我们外面传入的margin值。
-
leftPadding,topPadding,其实就是真正的二维码边距值。
那是不是说,我们只要修改leftPadding,topPadding的值,就可以实现我们的功能了呢?
那我只能说,异想天开了,因为这个方法的作用就是让二维码实际的大小与你外面传入的二维码大小作一个计算,使二维码最终居中显示。
例如:修改leftPadding,topPadding,如下:
int leftPadding = 10; int topPadding = 10;
效果图:
可以看到,设置的左上边距确实生效了,难道你还要进行裁剪二维码嘛?也不是不行,
但是,这个还会有个闪退的问题,那就是如果实际leftPadding是0(也就是我们最开始第三个二维码),那你给他设置10,那就会导致剩下代码的逻辑闪退,你们也可以自己试试。
说了这么多,那到底有什么解决办法?别急!着急的话可以直接跳第四章节。
我们得继续弄懂二维码的真实大小的又来,我们从他的code的传值方法encode入手。
public static QRCode encode(String content, ErrorCorrectionLevel ecLevel, Map hints) throws WriterException { // Choose the mask pattern and set to "qrCode". int dimension = version.getDimensionForVersion(); ByteMatrix matrix = new ByteMatrix(dimension, dimension); return qrCode; }
可以看到,他也返回了一个ByteMatrix的矩阵,而这个矩阵的大小是由getDimensionForVersion的大小决定的,我们看下getDimensionForVersion方法。
public int getDimensionForVersion() { return 17 + 4 * versionNumber; }
很明显的看到这个一个公式,其实这是一个计算二维码的实际大小的信息。可变的因素就只有versionNumber,而这就是二维码的版本号。其实我们在日常生活中看到的二维码,都是有对应的版本号的,而版本号的递增也意味着内容可以不断递增。
那这里就会有一个问题,那我们生成的二维码的版本号是多少?
我们可以从QR_VERSION 版本号设置的地方去跟踪下代码,如下:
// QRCODE版本设置 if (!TextUtils.isEmpty(qrCodeVersion)) { hints.put(EncodeHintType.QR_VERSION, qrCodeVersion); }
继续跟踪 QR_VERSION 调用的地方,发现还是在我们上面的encode方法里,代码如下:
Version version; if (hints != null && hints.containsKey(EncodeHintType.QR_VERSION)) { int versionNumber = Integer.parseInt(hints.get(EncodeHintType.QR_VERSION).toString()); version = Version.getVersionForNumber(versionNumber); } else { version = recommendVersion(ecLevel, mode, headerBits, dataBits); }
这里的逻辑也很简单,version的获取就两个
- 我们在外面通过QR_VERSION传入
- 如果我们没有传入的话,那他就会自己推荐一个版本号。而这个版本号的确定是由你提供的数据的 QR 码的能装纳的最小版本。简单的说,你的数据长度是100,如果7的版本号能装下,那就不会给你8。
总结一下:我们知道二维码的实际大小的边长是由versionNumber决定的,而versionNumber默认是由数据长度或者我们传入的版本号决定的。
注意:一般不会自己传入版本号,因为你不能确定数据能装纳的最小版本。当然,你也可以直接传最大值,也就是40。
总结一下:
- 二维码的实际大小,由二维码版本号决定
- 二维码的版本号,由你提供的数据决定
- 最终的二维码的边距,由二维码实际大小和你传入的大小计算
四、解决方案
解决方案如下:
- (⭐⭐⭐推荐)生成一个没有边距的二维码,然后我们想要什么样的边距那还不是我们说了算😀?
- (⭐⭐推荐)生成一个边距差不多和设计一样的大小,然后再进行放大缩小。如果设计不介意细节的话
- (⭐推荐)与设计沟通,找到一个二维码宽高与UI要的边距差不多大小的值。
4.1 第一种方案:如何生成一个没有边距的二维码?
转化一下,也就是传入什么值,可以把leftPadding和topPadding的值变成0?
直接说结论把:当你传入值宽高大小是inputWidth的倍数(这里宽高是一样的,我们就拿宽来举例说明),也就是inputWidth的值是21,那么你的宽高就必须是21*1, 21*2.。。。等,这样你的边距也就是为0 。我们看一个例子:
例如:宽高设置为210*210 ,margin为0
可以看到,当我们margin设置为0的时候:
- inputWidth、qrWidth和outputWidth是一样的,也就是21,
- multiple的值也就是:210 / 21 = 10
- leftPadding的值也就是:[210 - (21 * 10 )] / 2 = 0
其他21倍数的情况可以自行测试。
至此,我们可以得出一个结论,当我们margin设置为0的时候,只要我们传入的宽高是inputWidth的倍数,而inputWidth是什么?它是二维码的实际宽高,它的值由版本号决定,它的版本号由我们提供的数据决定。
那么,只要我们确定了提供的数据对应的二维码版本号,那么是不是就可以确定边长?
如何确定数据对应的二维码版本号?
- 一般来说,如果你的数据是一个网址,那么数据基本都是固定的,那也就是说你就能提前知道版本号,例如数据是:www.baidu.com,那基本上对应的二维码版本号就是1,对应的边长为21。
- 利用Zxing的方法来计算,也就是我们前面提到的recommendVersion方法
代码如下:
注意:在下面代码中,很多Zxing的方法都是private的,由于我们是集成源码到项目里,可以直接改为public,不然就只能通过反射。
public static Version recommendVersion(String content) { Mode mode = Encoder.chooseMode(content, "UTF-8"); BitArray headerBits = new BitArray(); BitArray dataBits = new BitArray(); try { return Encoder.recommendVersion(ErrorCorrectionLevel.L, mode, headerBits, dataBits); } catch (WriterException e) { e.printStackTrace(); } return null; }
测试一下:
val content = "www.baidu.com" val version = QRCodeUtil.recommendVersion(content) val size = version.dimensionForVersion Log.e(tag,"size:$size")
结果:
com.example.viewdemo E/MainActivity: size:21
接下来你就传你需要的大小(尽量跟UI差不多,防止拉伸后模糊)就可以了,然后再根据UI进行调整,第一种方法讲到这里就结束。
4.2 第二种方案:如何找到要给跟UI差不多边距的?
说实话,我没找到什么方法。。。慢慢调
4.3 第三种方案:如何找到二维码宽高与UI要的边距的二维码?
暴力测试,代码如下:
public class Main { public static void main(String[] args) { //你所要的边距 int margin = 5; while (true) { //不断轮询查找符合边距的大小尺寸 int size = 500; while (size > 0) { int version = 40; while (version > 0) { int dimensionForVersion = getDimensionForVersion(version--); int padding = renderResult(size, dimensionForVersion, margin); if (padding == margin) { System.out.println("找到了 二维码版本号为:"+version+ "大小为:"+size +"边距为:"+ + padding); } } size--; } break; } } private static int renderResult(int size, int qrCodeWidth, int margin) { int inputWidth = qrCodeWidth; int qrWidth = inputWidth + ((int) (margin * 2f)); int outputWidth = Math.max(size, qrWidth); int multiple = Math.min(outputWidth / qrWidth, outputWidth / qrWidth); int leftPadding = (outputWidth - (inputWidth * multiple)) / 2; return leftPadding; } public static int getDimensionForVersion(int versionNumber) { return 17 + 4 * versionNumber; }}
结果:
然后你只需要
val codeBitmap = QRCodeUtil.createQRCodeBitmap(content, 188, 188,"39","5")
效果图(为了看清,图片经过放大):
很完美,但是有个缺点,他的二维码版本号普遍偏高,导致扫码识别速度降低,还不是很好看。
好了,到这里就结束了。