这次和大家分享的是如何画一枚有趣的二维码。具体实现效果如下,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 的值,这样一来就回到上面的水平渐变的计算了。
是不是很简单?具体实现请查看 源码 。
来源: https://juejin.im/post/5a3118f65188256e7a06ce08