這次和大家分享的是如何畫一枚有趣的二維碼。具體實現效果如下,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 的值,這樣一來就回到上面的水平漸變的計算了。
是不是很簡單?具體實現請檢視 原始碼 。