作者写了一个给 iPhone X 去掉刘海的 APP,而且其他 iPhone 也可以玩,有兴趣的话去 App Store 看看。点击前往。

这次和大家分享的是如何画一枚有趣的二维码。具体实现效果如下,GitHub 链接在 这里 。
![]()
01.二维码常识扫盲
二维码就是一个矩阵,只不过对于不同的纠错率,生成的矩阵的大小会有不同。
如下图所示,当生成二维码的时候,会根据不同的纠错率生成一个对应大小的矩阵,比方说生成下图左侧 3 × 3 大小的空矩阵,然后根据生成二维码的字符串进行编码,把编码数据以 1 和 0 的形式插入到矩阵当中。如下图右侧图所示,第一个方块有数据为 1,就绘制一个圆形标记,其他方块没有数据为 0,不用绘制标记。按照这样的规则进行绘制,就会得到一枚二维码,只不过以上描述的只是规则的一个简单版本。
除了知道以上简单的原理,下图有一个更加详细的,如果你只是想实现这篇文章中的功能,你知道这么多已经够了。但是如果你觉得不够,这里有一篇文章详细介绍了二维码的原理,感兴趣请 点击 前往。
02.大致实现原理
按照惯例,我们先来分析要画这么一枚二维码大致需要哪些步骤。
01.首先,我们肯定需要依靠系统生成一枚二维码。
02.拿到系统的二维码以后我们需要将这张系统生成的二维码转成矩阵,并以二维数组的形式保存起来。
03.有了这个矩阵以后,我们就可以自己创建一张画布,按照矩阵的数据进行二维码的绘制。此时,我们可以选择绘制圆,也可以绘制正方形等等。
04.我们在绘制的同时可以进行着色的操作。
03.生成二维码
在 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;
}
04.生成矩阵数组
在生成矩阵数组之前,我们先要将系统生成的二维码从 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];
}
05.自定义绘制二维码
我们有了二维码矩阵以后,只要开启一张画布,将这个矩阵的数据对应的绘制到画布上,就能获得一张二维码。此时,因为是自己在画布中绘制,我们可以自定义绘制的形状,可以是圆形,也可以是矩形,还可以是其他形状,只要你能想到。
06.渐变绘制
绘制不是难点,但是计算渐变颜色要求有一点初中三角函数的基础才行。
6.1、水平渐变
由于每一个颜色都是由 RGB 组成的,所以我们可以将颜色分解成为 Red、Green、Blue,分别进行渐变运算。
如下图,渐变的颜色区间为 Red1 到 Red2,对应的坐标为(x1, y1) 和 (x2, y2)。要求的点的坐标为(x, y),显然 Red = Red1 + (Red2 - Red1) × (x - x1) / (x2 - x1)。然后我们再将分解求得的值进行合成 UIColor
,然后就得到了水平渐变的颜色的渐变颜色区间色值。
6.2、对角渐变
这篇文章的开始那张二维码就是用的对角渐变。
如下图所示,要实现对角渐变就需要计算出 targetValue 的值。我们可以通过 Red 点的坐标值计算出角度 α 的值,由于 α + β = 90°,因此我们可以计算出 β 的值,然后计算出 targetValue 的值,这样一来就回到上面的水平渐变的计算了。
是不是很简单?具体实现请查看 源码 。
我的文章集合
下面这个链接是我所有文章的一个集合目录。这些文章凡是涉及实现的,每篇文章中都有 Github 地址,Github 上都有源码。
网友评论
就是类似https://github.com/EyreFree/EFQRCode这种效果的。