生成圆形二维码
二维码识别必须是方形的,如果在二维码中间添加图标,不会影响二维码的识别,但是如果去掉二维码的四角,把方形二维码直接切割成圆形,就会出现识别不了的情况,那么怎么才能生成一个圆形的可以识别的二维码呢?答案就是,把实际可以识别的方形区域放进一个圆形中,然后在没有被方形二维码的区域随机填充上二维码的图案
生成圆形二维码的步骤分解
- 根据要生成的圆形二维码尺寸,计算所需内接正方形的尺寸 ,内接正方形的尺寸就是二维码有效识别的区域;
- 按照内接正方形的尺寸,生成一个二维码,同时计算二维码的两个尺寸参数,这个步骤和正常生成二维码的方式是一模一样的,在生这个生成的过程中,我们还需要计算两个参数(1)单个二维码小格子的宽度,(2)二维码边框的宽度,参数(1)是为了便于我们在填充圆形区域的时候,更好地和中间地有效二维码地密度做融合,使做出来效果中间的有效区域和边缘的模拟区域没有明显地界限;
- 按照圆形二维码的尺寸,生成一个圆外接正方型的模拟二维码,二维码的小格子的宽度是步骤2计算出来的小格子的宽度 ,这个步骤先生成一个二维码的底图,这个二维码是不能识别的,需要下在下个步骤里,把步骤二生成的二维码贴在这个底图的中间;
- 把步骤二生成的二维码贴在步骤三生成的模拟二维码的中间,在这个步骤里面,要注意两个问题,(1)步骤二生成的二维码是由边框的,这个边框不被排除,直接贴上来,最终效果是圆形二维码中间区域有一个白色方框,白色方框里也是二维码,所以在bitmap切割的时候,source bounds要排除边框的宽度,这时会用到步骤二计算时得到第二个参数;
同时需要注意,在贴二维码时,目的是要把二维码居中显示,但是为了避免交接处存在偏移导致模拟二维码和中间方形区域的图案不契合,要对贴图是的左边距做微调,确保左边距刚好是二维码小格子的整数倍,上边距同理 - 把图标按照一定的比例缩小,贴在步骤4得到的二维码中间;
- 把整个图片切割成圆形;
步骤对应的代码片段
1.根据要生成的圆形二维码尺寸,计算所需内接正方形的尺寸
内接正方形要满足的条件是正方形四个顶点都在圆周上,就是说如果把正方形四等分成四个小正方形,这个小正方形的对角线就是圆的半径,
小正方形的边长*Math.sqrt(2) = 半径,
因为小正方形的变成是大正方形边长的二分之一,所以
大正方形的边长 = 2 *(半径/Math.sqrt(2));
化简得到以下公式,其中width是圆的直径,也就是传入的二维码尺寸,targetWidth就是内接正方形的宽度,也就是实际上可识别二维码的宽度
int targetWidth = (int) (width*1.4142f/2);
2.按照内接正方形的尺寸,生成一个二维码,同时计算二维码的两个尺寸参数
/**
*
* @param url 二维码的内容
* @param width 二维码绘制的宽度
* @param shortest 用来乘放二维码位图中相关的尺寸参数 0 保存的是获取的二维码小格子的宽度,1保存的是方形二维码的边框的宽度
* @return 二维码位图,包含边框
*/
public static Bitmap createQrBitmap(String url,int width,int[] shortest){
try {
Hashtable<EncodeHintType,Object> hints = new Hashtable<>();
hints.put(EncodeHintType.CHARACTER_SET,"utf-8");
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
hints.put(EncodeHintType.MARGIN,0);
BitMatrix byteMatrix = new QRCodeWriter().encode(url, BarcodeFormat.QR_CODE,width,width,hints);
int[] pixels = new int[width*width];
int shortestPix = 0;
boolean lastSelected = false;
int currentShortest = width;
int consequenceSelected = 0;
int tempForMargin = 0;
int temptConsequenceSelected = 0;
boolean allUnSelected = true;
for(int y = 0;y<width;y++){
allUnSelected = true;
for(int x = 0;x<width;x++){
boolean selected = byteMatrix.get(x,y);
//把选中状态记录到bitmap中,用颜色来表示
pixels[y*width+x]=byteMatrix.get(x,y)? Color.WHITE:Color.BLACK;
//记录这一行是不是全部未选中,如果全部未选中,说明这一行是上边框或下边框,连续未选中行数可以确定上边框或下边框的宽度
if(selected){
allUnSelected = false;
}
//如果margin找到了,说明接下来的才是真正二维码的内容
if(shortest[1]>0) {
//***********这些逻辑是为了找到单个二维码格子的宽度,最小的连续格子数就是二维码最小格子的宽度
//如果是第一列,记录初始的选中状态
if(x<shortest[1]||x>=width-shortest[1]) {
//排除掉横向在绘制margin的情况
continue;
}
if (x == shortest[1]) {//在每行的第一个像素开始记录最初的宽度
lastSelected = selected;
shortestPix = 1;
} else if (selected == lastSelected) {
shortestPix++;//如果选中状态不变,说明是同一个格子
} else {
if (shortestPix > 1) {
if (shortestPix < currentShortest) {
//如果有比上一次计算得到的更小的格子,就记录当前值
currentShortest = shortestPix;
}
}
lastSelected = selected;
shortestPix = 1;
}
}
}
if(consequenceSelected==0&&allUnSelected){
tempForMargin++;
}else if(tempForMargin>0&&consequenceSelected==0){
consequenceSelected = tempForMargin;
shortest[1] = consequenceSelected;
}
}
L.d("currentShortest "+currentShortest+" margin "+shortest[1]);
shortest[0] = currentShortest;
Bitmap bitmap = Bitmap.createBitmap(width,width, Bitmap.Config.ARGB_8888,true);
bitmap.setPixels(pixels,0,width,0,0,width,width);
return bitmap;
}catch (Exception e){
e.printStackTrace();
}
return null;
}
- 二维码的bitmap是怎样绘制的
BitMatrix byteMatrix = new QRCodeWriter().encode(url, BarcodeFormat.QR_CODE,width,width,hints);
在这句代码里,zxing库给我们返回了一个BitMatrix的对象,它代表了一个布尔值矩阵,通过以下方法获取其存储的布尔值。
boolean selected = byteMatrix.get(x,y);
false代表空白区域和无数据区域,true代表有数据区域,其中二维码通常地白色区域和边框区域都是false地区域,黑色往往代表有数据地区域,这两种颜色交替出现,所以也可以把true设置成白色区域,false设置成黑色区域,这时就会是一个黑色边框地二维码,最终不会影响扫描地结果。
- 如何计算二维码的边框宽度在这里我们注意到
hints.put(EncodeHintType.MARGIN,0);
这一句代码,这个是设置边框的宽度,虽然参数传入的是0,但是经过实际测试,生成的二维码的结果还是包含一个默认的边框,这个边框的宽度在zxing底层的是按照一个什么样的比例来设置我们不得而知,但是我们可以通过返回的矩阵值来判断这个边框宽度对应的像素值是多少,通过从第一行开始,计算连续有多少行全部为fase,来确定二维码上边框的像素宽度,因为边框部分的矩阵一定是false,而且二维码的有效区域不可能一整行都是fase,所以连续为fase的行数就是矩阵上边框的宽度,也就是矩阵边框的宽度。代码中
allUnSelected
这个参数就是来标记某一行整行都是fase,而且对于二维码小格子的宽度的计算,必定是计算完上边距以后,才开始出现存在true的情况,并且依据已知边框的宽度,可以排除计算小格子宽度时左右边距对计算结果的影响。
if(x<shortest[1]||x>=width-shortest[1]) {
//排除掉横向在绘制margin的情况
continue;
}
代码中的这个判断语句,就是在遍历整行的时候,排除对左右边距的统计,其中shortest[1]保存的就是边框宽度的计算结果
- 如何计算二维码小格子的宽度
二维码小格子,组成二维码的黑色小块和白色小块,他们有时候会有连续的两个或者更多排列成一排或者一列,我们通过计算所以行里面连续的fase或者true的最小宽度,来确定小格子的宽度
if (x == shortest[1]) {//在每行的第一个像素开始记录最初的宽度
lastSelected = selected;
shortestPix = 1;
} else if (selected == lastSelected) {
shortestPix++;//如果选中状态不变,说明是同一个格子
} else {
if (shortestPix > 1) {
if (shortestPix < currentShortest) {
//如果有比上一次计算得到的更小的格子,就记录当前值
currentShortest = shortestPix;
}
}
lastSelected = selected;
shortestPix = 1;
}
上面代码中的这一段,就时对比宽度和记录连续相同布尔值的过程,其中”currentShortest“记录目前最小的宽度,如果有更小的连续相同布尔值的个数,这个值就会被更新,shortestPix 随着每次一检测累加一个px,直到布尔值发生变化,累加结束,结束时的shortestPix值,就是某一次连续布尔值相同的长度,它被拿来会跟currentShortest做对比;
3. 按照圆形二维码的尺寸,生成一个圆外接正方型的模拟二维码,二维码的小格子的宽度是步骤2计算出来的小格子的宽度
/**
* 绘制一个模拟的二维码,二维码的图案是随机填充的
* @param canvas
* @param shortest 模拟二维码小格子的宽度
* @param width 模拟二维码的宽度
*/
private static void drawDefQr(Canvas canvas,int shortest,int width){
Paint paint = new Paint();
for(int y = 0;y<width;){
for(int x= 0;x<width;){
int select = ((int) (Math.random()*10))&0x01;
paint.setColor(select==1?Color.BLACK:Color.WHITE);
canvas.drawRect(x,y,x+shortest,y+shortest,paint);
x = x+shortest;
}
y = y+shortest;
}
因为是模拟二维码,所以小格子的颜色是随机的,小格子的宽度和步骤二计算得到的小格子的宽度一致
4.把步骤二生成的二维码贴在步骤三生成的模拟二维码的中间
//生成的二维码的边框宽度
int qrMargin = params[1];
//要绘制的实际的二维码的宽度,去掉边框
targetWidth = targetWidth-params[1]*2;
//待绘制二维码在背景中实际的左边距和上边距
int drawMargin = (width-targetWidth)/2;
//为了待位置二维码和作为背景的模拟二维码格子能无缝拼接,需要微调一下边距,确保边距是格子的整数倍
drawMargin = wrapDrawMargin(drawMargin,params[0]);
//计算原二维码位图要绘制的区域,这个区域要排除边框的宽度
Rect source = new Rect(qrMargin,qrMargin,qrMargin+targetWidth,qrMargin+targetWidth);
//计算待绘制的二维码位图在画布上的位置
Rect target = new Rect(drawMargin,drawMargin,drawMargin+targetWidth,drawMargin+targetWidth);
//把实际的二维码绘制到画布上
canvas.drawBitmap(qrBitmap,source,target,paint);
其中,wrapDrawMargin方法就是微调有效二维码在模拟的二维码上面绘制时的左边距和上边距,需要保持这个边距是小格子的整数倍,以免在拼接处的周围出现黑色的小长方形格子或者白色的小长方形格子wrapDrawMargin的代码如下
/**
* 把left/top margin微调到格子的整数倍,以便模拟二维码和实际二维码之间没有缝隙
* @param sourceMargin
* @param cubSize
* @return
*/
private static int wrapDrawMargin(int sourceMargin,int cubSize){
int left = sourceMargin%cubSize;
return sourceMargin-left;
}
5.把图标按照一定的比例缩小,贴在步骤4得到的二维码中间
//绘制图标
int icWidth = (int)(width*imageScale);
int margin = (int) ((1-imageScale)/2f*width);
Rect rect = new Rect(margin,margin,margin+icWidth,margin+icWidth);
canvas.drawBitmap(icBitmap,null,rect,paint);
其中imageScale是图标占圆形二维码宽度的比例,icBitmap是要绘制上去的图标的Bitmap
这个时候基本上二维码基本绘制成功,只剩下把方形切成圆形,为了美观,可以在切圆形之前,在画布上再绘制一个白色的圆形边框,代码如下
//绘制圆形边框
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(5);
float radius = width/2f-2.5f;
paint.setColor(Color.WHITE);
canvas.drawCircle(width/2f,width/2f,radius,paint);
6.把整个图片切割成圆形
public static Bitmap cropCircle(Bitmap sourceBitmap, int size){
Bitmap bitmap = Bitmap.createBitmap(size,size, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setFilterBitmap(true);
Bitmap bitmap1 = Bitmap.createBitmap(size,size, Bitmap.Config.ARGB_8888);
Canvas canvas1 = new Canvas(bitmap1);
paint.setColor(Color.RED);
canvas1.drawCircle(size/2f,size/2f,size/2f,paint);
Rect target = new Rect(0,0,size,size);
canvas.drawBitmap(sourceBitmap,null,target,paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
canvas.drawBitmap(bitmap1,0f,0f,paint);
bitmap1.recycle();
return bitmap;
}
把图案切割成圆形的原理,就是先制作一个bitmap,这个bitmap是一个实心圆,它的颜色是什么不重要,因为它只是一个模板,再以上代码中,这个bitmap就是bitmap1,
然后创建一个新的bitmap,先把我们前面步骤得到的bitmap绘制再画布上,也就是代码中的sourceBitmap,然后给画笔设置Xfermode为PorterDuff.Mode.DST_IN,这个的意思就是“destination in”,就是目标图案在我绘制的这个图形的区域内,我将要绘制的图形区域以外的区域都清除掉,这个时候把预先准备的圆形模板以“DST_IN”的模式绘制在我们的二维码图案上,就“切”掉了圆形以外无用的区域。此时画布对应的这个Bitmap就是我们需要的圆形二维码的Bitmap
以下是生成的效果图,微信扫码可以跳转到csdn的首页