稀土 2018-01-05
这次和大家分享的是如何画一枚有趣的二维码。具体实现效果如下,GitHub 链接在 这里 。
二维码就是一个矩阵,只不过对于不同的纠错率,生成的矩阵的大小会有不同。
如下图所示,当生成二维码的时候,会根据不同的纠错率生成一个对应大小的矩阵,比方说生成下图左侧 3 × 3 大小的空矩阵,然后根据生成二维码的字符串进行编码,把编码数据以 1 和 0 的形式插入到矩阵当中。如下图右侧图所示,第一个方块有数据为 1,就绘制一个圆形标记,其他方块没有数据为 0,不用绘制标记。按照这样的规则进行绘制,就会得到一枚二维码,只不过以上描述的只是规则的一个简单版本。
除了知道以上简单的原理,下图有一个更加详细的,如果你只是想实现这篇文章中的功能,你知道这么多已经够了。但是如果你觉得不够,这里有一篇文章详细介绍了二维码的原理,感兴趣请 点击 前往。
按照惯例,我们先来分析要画这么一枚二维码大致需要哪些步骤。
01.首先,我们肯定需要依靠系统生成一枚二维码。
02.拿到系统的二维码以后我们需要将这张系统生成的二维码转成矩阵,并以二维数组的形式保存起来。
03.有了这个矩阵以后,我们就可以自己创建一张画布,按照矩阵的数据进行二维码的绘制。此时,我们可以选择绘制圆,也可以绘制正方形等等。
04.我们在绘制的同时可以进行着色的操作。
在 iOS 中创建二维码依赖 CIFilter
类,传进字符串的二进制流和纠错类型就能生成一张对应的二维码。
+(CIImage *)createOriginalCIImageWithString:(NSString *)str withCorrectionLevel:(kQRCodeCorrectionLevel)corLevel{ CIFilter *filter = [CIFilter filterWithName:@"CIQRCodeGenerator"]; [filter setDefaults]; NSData *data = [str dataUsingEncoding:NSUTF8StringEncoding]; [filter setValue:data forKeyPath:@"inputMessage"]; NSString *corLevelStr = nil; switch (corLevel) { case kQRCodeCorrectionLevelLow: corLevelStr = @"L"; break; case kQRCodeCorrectionLevelNormal: corLevelStr = @"M"; break; case kQRCodeCorrectionLevelSuperior: corLevelStr = @"Q"; break; case kQRCodeCorrectionLevelHight: corLevelStr = @"H"; break; } [filter setValue:corLevelStr forKey:@"inputCorrectionLevel"]; CIImage *outputImage = [filter outputImage]; return outputImage; }
在生成矩阵数组之前,我们先要将系统生成的二维码从 CIImage
转成 CGImageRef
备用。
+(CGImageRef)convertCIImage2CGImageForCIImage:(CIImage *)image{ CGRect extent = CGRectIntegral(image.extent); size_t width = CGRectGetWidth(extent); size_t height = CGRectGetHeight(extent); CGColorSpaceRef cs = CGColorSpaceCreateDeviceGray(); CGContextRef bitmapRef = CGBitmapContextCreate(nil, width, height, 8, 0, cs, (CGBitmapInfo)kCGImageAlphaNone); CIContext *context = [CIContext contextWithOptions:nil]; CGImageRef bitmapImage = [context createCGImage:image fromRect:extent]; CGContextSetInterpolationQuality(bitmapRef, kCGInterpolationNone); CGContextScaleCTM(bitmapRef, 1, 1); CGContextDrawImage(bitmapRef, extent, bitmapImage); CGImageRef scaledImage = CGBitmapContextCreateImage(bitmapRef); CGContextRelease(bitmapRef); CGImageRelease(bitmapImage); return scaledImage; }
接下来就是核心代码。利用 CoreGraphics 取出一张图片指定 pixel 的 RGBA 值,然后将这个值存在二维数组中。具体看源码。
+(NSArray<NSArray *>*)getPixelsWithCIImage:(CIImage *)ciimg{ NSMutableArray *pixels = [NSMutableArray array]; // 将系统生成的二维码从 `CIImage` 转成 `CGImageRef`. CGImageRef imageRef = [self convertCIImage2CGImageForCIImage:ciimg]; CGFloat width = CGImageGetWidth(imageRef); CGFloat height = CGImageGetHeight(imageRef); // 创建一个颜色空间. CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); // 开辟一段 unsigned char 的存储空间,用 rawData 指向这段内存. // 每个 RGBA 色值的范围是 0-255,所以刚好是一个 unsigned char 的存储大小. // 每张图片有 height * width 个点,每个点有 RGBA 4个色值,所以刚好是 height * width * 4. // 这段代码的意思是开辟了 height * width * 4 个 unsigned char 的存储大小. unsigned char *rawData = (unsigned char *)calloc(height * width * 4, sizeof(unsigned char)); // 每个像素的大小是 4 字节. NSUInteger bytesPerPixel = 4; // 每行字节数. NSUInteger bytesPerRow = width * bytesPerPixel; // 一个字节8比特 NSUInteger bitsPerComponent = 8; // 将系统的二维码图片和我们创建的 rawData 关联起来,这样我们就可以通过 rawData 拿到指定 pixel 的内存地址. CGContextRef context = CGBitmapContextCreate(rawData, width, height, bitsPerComponent, bytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big); CGColorSpaceRelease(colorSpace); CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); CGContextRelease(context); for (int indexY = 0; indexY < height; indexY++) { NSMutableArray *tepArrM = [NSMutableArray array]; for (int indexX = 0; indexX < width; indexX++) { // 取出每个 pixel 的 RGBA 值,保存到矩阵中. @autoreleasepool { NSUInteger byteIndex = bytesPerRow * indexY + indexX * bytesPerPixel; CGFloat red = (CGFloat)rawData[byteIndex]; CGFloat green = (CGFloat)rawData[byteIndex + 1]; CGFloat blue = (CGFloat)rawData[byteIndex + 2]; BOOL shouldDisplay = red == 0 && green == 0 && blue == 0; [tepArrM addObject:@(shouldDisplay)]; byteIndex += bytesPerPixel; } } [pixels addObject:[tepArrM copy]]; } free(rawData); return [pixels copy]; }
我们有了二维码矩阵以后,只要开启一张画布,将这个矩阵的数据对应的绘制到画布上,就能获得一张二维码。此时,因为是自己在画布中绘制,我们可以自定义绘制的形状,可以是圆形,也可以是矩形,还可以是其他形状,只要你能想到。
绘制不是难点,但是计算渐变颜色要求有一点初中三角函数的基础才行。
由于每一个颜色都是由 RGB 组成的,所以我们可以将颜色分解成为 Red、Green、Blue,分别进行渐变运算。
如下图,渐变的颜色区间为 Red1 到 Red2,对应的坐标为(x1, y1) 和 (x2, y2)。要求的点的坐标为(x, y),显然 Red = Red1 + (Red2 - Red1) × (x - x1) / (x2 - x1)。然后我们再将分解求得的值进行合成 UIColor
,然后就得到了水平渐变的颜色的渐变颜色区间色值。
这篇文章的开始那张二维码就是用的对角渐变。
如下图所示,要实现对角渐变就需要计算出 targetValue 的值。我们可以通过 Red 点的坐标值计算出角度 α 的值,由于 α + β = 90°,因此我们可以计算出 β 的值,然后计算出 targetValue 的值,这样一来就回到上面的水平渐变的计算了。
是不是很简单?具体实现请查看 源码 。