前言
二維碼現(xiàn)在很常見, App中有許多都配置了該功能, 網(wǎng)上也有很多對(duì)iOS二維碼的講解, 但是對(duì)配置掃碼范圍這個(gè)問題好像都沒怎么講清晰. 作者今天就寫一下二維碼.
一 二維碼介紹
三個(gè)回形大方塊, 是為了給相機(jī)定位的; 黑白塊, 黑塊代表1, 白塊代表0, 八個(gè)一組, 組成二進(jìn)制信息. 科普:二維碼是什么原理? 這個(gè)小視頻, 簡單介紹了下二維碼.
二 iOS 掃碼二維碼
一.工程中General -> Linked Frameworks and Libraries -> 引入 AVFoundation.framework
二. 代碼部分解析
- 頭文件
//引入頭文件
#import <AVFoundation/AVFoundation.h>
// 作者自定義的View視圖, 繼承UIView
#import "ShadowView.h"
#define kWidth [UIScreen mainScreen].bounds.size.width
#define kHeight [UIScreen mainScreen].bounds.size.height
#define customShowSize CGSizeMake(200, 200);
- 定義屬性
// ScanCodeViewController是作者創(chuàng)建的VC , 用Navi推出, 寫入?yún)f(xié)議 (UIImagePickerControllerDelegate, UINavigationControllerDelegate 是為了 可以直接掃碼圖庫中的二維碼, 在Navi右上角創(chuàng)建button)
@interface ScanCodeViewController ()<AVCaptureMetadataOutputObjectsDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate>
/** 輸入數(shù)據(jù)源 */
@property (nonatomic, strong) AVCaptureDeviceInput *input;
/** 輸出數(shù)據(jù)源 */
@property (nonatomic, strong) AVCaptureMetadataOutput *output;
/** 輸入輸出的中間橋梁 負(fù)責(zé)把捕獲的音視頻數(shù)據(jù)輸出到輸出設(shè)備中 */
@property (nonatomic, strong) AVCaptureSession *session;
/** 相機(jī)拍攝預(yù)覽圖層 */
@property (nonatomic, strong) AVCaptureVideoPreviewLayer *layerView;
/** 預(yù)覽圖層尺寸 */
@property (nonatomic, assign) CGSize layerViewSize;
/** 有效掃碼范圍 */
@property (nonatomic, assign) CGSize showSize;
/** 作者自定義的View視圖 */
@property (nonatomic, strong) ShadowView *shadowView;
@end
- 創(chuàng)建二維碼掃碼
-(void)creatScanQR{
/** 創(chuàng)建輸入數(shù)據(jù)源 */
AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; //獲取攝像設(shè)備
self.input = [AVCaptureDeviceInput deviceInputWithDevice:device error:nil]; //創(chuàng)建輸出流
/** 創(chuàng)建輸出數(shù)據(jù)源 */
self.output = [[AVCaptureMetadataOutput alloc] init];
[self.output setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()]; //設(shè)置代理 在主線程里刷新
/** Session設(shè)置 */
self.session = [[AVCaptureSession alloc] init];
[self.session setSessionPreset:AVCaptureSessionPresetHigh]; //高質(zhì)量采集
[self.session addInput:self.input];
[self.session addOutput:self.output];
//設(shè)置掃碼支持的編碼格式
self.output.metadataObjectTypes = @[AVMetadataObjectTypeQRCode,
AVMetadataObjectTypeEAN13Code,
AVMetadataObjectTypeEAN8Code,
AVMetadataObjectTypeCode128Code];
/** 掃碼視圖 */
//掃描框的位置和大小
self.layerView = [AVCaptureVideoPreviewLayer layerWithSession:self.session];
self.layerView.videoGravity = AVLayerVideoGravityResizeAspectFill;
self.layerView.frame = CGRectMake(0, 64, self.view.frame.size.width, self.view.frame.size.height - 64);
// 將掃描框大小定義為屬行, 下面會(huì)有調(diào)用
self.layerViewSize = CGSizeMake(_layerView.frame.size.width, _layerView.frame.size.height);
}
#pragma mark - 實(shí)現(xiàn)代理方法, 完成二維碼掃描
-(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection{
if (metadataObjects.count > 0) {
// 停止動(dòng)畫, 看完全篇記得打開注釋, 不然掃描條會(huì)一直有動(dòng)畫效果
//[self.shadowView stopTimer];
//停止掃描
[self.session stopRunning];
AVMetadataMachineReadableCodeObject * metadataObject = [metadataObjects objectAtIndex : 0 ];
//輸出掃描字符串
NSLog(@"%@",metadataObject.stringValue);
UIAlertView * alert = [[UIAlertView alloc]initWithTitle:@"提示" message:[NSString stringWithFormat:@"%@", metadataObject.stringValue] delegate:nil cancelButtonTitle:@"確定" otherButtonTitles:nil, nil];
[alert show];
}
}
- 調(diào)用方法
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
//調(diào)用
[self creatScanQR];
//添加拍攝圖層
[self.view.layer addSublayer:self.layerView];
//開始二維碼
[self.session startRunning];
// Do any additional setup after loading the view.
}
至此 二維碼掃碼完成 , 你會(huì)發(fā)現(xiàn) 整個(gè)屏幕都可以掃碼二維碼, 與微信的二維碼掃碼 差的太多. 下面開始個(gè)性化設(shè)置
- 自定義陰影視圖層
#import <UIKit/UIKit.h>
@interface ShadowView : UIView
@property (nonatomic, assign) CGSize showSize;
- (void)stopTimer;
@end
#import "ShadowView.h"
@interface ShadowView ()
@property (nonatomic, strong) UIImageView *lineView;
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation ShadowView
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor clearColor];
// 圖片下方附上
self.lineView = [[UIImageView alloc] init];
self.lineView.image = [UIImage imageNamed:@"line"];
[self addSubview:self.lineView];
}
return self;
}
-(void)playAnimation{
[UIView animateWithDuration:2.4 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
self.lineView .frame = CGRectMake((self.frame.size.width - self.showSize.width) / 2, (self.frame.size.height + self.showSize.height) / 2, self.showSize.width, 2);
} completion:^(BOOL finished) {
self.lineView .frame = CGRectMake((self.frame.size.width - self.showSize.width) / 2, (self.frame.size.height - self.showSize.height) / 2, self.showSize.width, 2);
}];
}
- (void)stopTimer
{
[_timer invalidate];
_timer = nil;
}
-(void)layoutSubviews{
[super layoutSubviews];
self.lineView .frame = CGRectMake((self.frame.size.width - self.showSize.width) / 2, (self.frame.size.height - self.showSize.height) / 2, self.showSize.width, 2);
if (!_timer) {
[self playAnimation];
/* 自動(dòng)播放 */
self.timer = [NSTimer scheduledTimerWithTimeInterval:2.5 target:self selector:@selector(playAnimation) userInfo:nil repeats:YES];
}
}
-(void)drawRect:(CGRect)rect{
[super drawRect:rect];
CGContextRef ctx = UIGraphicsGetCurrentContext();
// 整體顏色
CGContextSetRGBFillColor(ctx, 0.15, 0.15, 0.15, 0.6);
CGContextFillRect(ctx, rect); //draw the transparent layer
//中間清空矩形框
CGRect clearDrawRect = CGRectMake((rect.size.width - self.showSize.width) / 2, (rect.size.height - self.showSize.height) / 2, self.showSize.width, self.showSize.height);
CGContextClearRect(ctx, clearDrawRect);
//邊框
CGContextStrokeRect(ctx, clearDrawRect);
CGContextSetRGBStrokeColor(ctx, 1, 1, 1, 1); //顏色
CGContextSetLineWidth(ctx, 0.5); //線寬
CGContextAddRect(ctx, clearDrawRect); //矩形
CGContextStrokePath(ctx);
[self addCornerLineWithContext:ctx rect:clearDrawRect];
}
- (void)addCornerLineWithContext:(CGContextRef)ctx rect:(CGRect)rect{
float cornerWidth = 4.0;
float cornerLong = 16.0;
//畫四個(gè)邊角 線寬
CGContextSetLineWidth(ctx, cornerWidth);
//顏色
CGContextSetRGBStrokeColor(ctx, 83 /255.0, 239/255.0, 111/255.0, 1);//綠色
//左上角
CGPoint poinsTopLeftA[] = {CGPointMake(rect.origin.x + cornerWidth/2, rect.origin.y),
CGPointMake(rect.origin.x + cornerWidth/2, rect.origin.y + cornerLong)};
CGPoint poinsTopLeftB[] = {CGPointMake(rect.origin.x, rect.origin.y + cornerWidth/2),
CGPointMake(rect.origin.x + cornerLong, rect.origin.y + cornerWidth/2)};
[self addLine:poinsTopLeftA pointB:poinsTopLeftB ctx:ctx];
//左下角
CGPoint poinsBottomLeftA[] = {CGPointMake(rect.origin.x + cornerWidth/2, rect.origin.y + rect.size.height - cornerLong),
CGPointMake(rect.origin.x + cornerWidth/2, rect.origin.y + rect.size.height)};
CGPoint poinsBottomLeftB[] = {CGPointMake(rect.origin.x, rect.origin.y + rect.size.height - cornerWidth/2),
CGPointMake(rect.origin.x + cornerLong, rect.origin.y + rect.size.height - cornerWidth/2)};
[self addLine:poinsBottomLeftA pointB:poinsBottomLeftB ctx:ctx];
//右上角
CGPoint poinsTopRightA[] = {CGPointMake(rect.origin.x+ rect.size.width - cornerLong, rect.origin.y + cornerWidth/2),
CGPointMake(rect.origin.x + rect.size.width, rect.origin.y + cornerWidth/2 )};
CGPoint poinsTopRightB[] = {CGPointMake(rect.origin.x+ rect.size.width - cornerWidth/2, rect.origin.y),
CGPointMake(rect.origin.x + rect.size.width- cornerWidth/2, rect.origin.y + cornerLong)};
[self addLine:poinsTopRightA pointB:poinsTopRightB ctx:ctx];
//右下角
CGPoint poinsBottomRightA[] = {CGPointMake(rect.origin.x+ rect.size.width - cornerWidth/2, rect.origin.y+rect.size.height - cornerLong),
CGPointMake(rect.origin.x- cornerWidth/2 + rect.size.width, rect.origin.y +rect.size.height )};
CGPoint poinsBottomRightB[] = {CGPointMake(rect.origin.x+ rect.size.width - cornerLong, rect.origin.y + rect.size.height - cornerWidth/2),
CGPointMake(rect.origin.x + rect.size.width, rect.origin.y + rect.size.height - cornerWidth/2 )};
[self addLine:poinsBottomRightA pointB:poinsBottomRightB ctx:ctx];
CGContextStrokePath(ctx);
}
- (void)addLine:(CGPoint[])pointA pointB:(CGPoint[])pointB ctx:(CGContextRef)ctx {
CGContextAddLines(ctx, pointA, 2);
CGContextAddLines(ctx, pointB, 2);
}
@end
注意 : 如果你對(duì)應(yīng)寫demo了, 并且真機(jī)測試的系統(tǒng)在ios7 以上, 會(huì)發(fā)現(xiàn)也許掃描線的初始位置會(huì)出現(xiàn)問題. 給個(gè)小提示 layoutSubviews 被執(zhí)行了兩次. 留個(gè)小尾巴. 讓大家思考.
傳送門 : 上述邊角畫圖方法, 作者引用 Raul7777 并附上鏈接 , 作者進(jìn)行了一下小改進(jìn) , 并添加注釋. 如果對(duì)你有幫助, 請(qǐng)給他點(diǎn)個(gè) Star
補(bǔ)充 : 掃描線動(dòng)畫, 用NSTimer, 偶爾手機(jī)處理卡頓下, 就會(huì)被坑. 作者看到有用 CABasicAnimation 寫的, 覺得挺好. 所以 提供下, 該方法
-(void)addAnimationAboutScan{
self.lineView.hidden = NO;
CABasicAnimation *animation = [ShadowView moveYTime:2.5 fromY:[NSNumber numberWithFloat:0] toY:[NSNumber numberWithFloat:(self.showSize.height-1)] rep:OPEN_MAX];
[self.lineView.layer addAnimation:animation forKey:@"LineAnimation"];
}
+ (CABasicAnimation *)moveYTime:(float)time fromY:(NSNumber *)fromY toY:(NSNumber *)toY rep:(int)rep{
CABasicAnimation *animationMove = [CABasicAnimation animationWithKeyPath:@"transform.translation.y"];
[animationMove setFromValue:fromY];
[animationMove setToValue:toY];
animationMove.duration = time;
animationMove.delegate = self;
animationMove.repeatCount = rep;
animationMove.fillMode = kCAFillModeForwards;
animationMove.removedOnCompletion = NO;
animationMove.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
return animationMove;
}
- (void)removeAnimationAboutScan{
[self.lineView.layer removeAnimationForKey:@"LineAnimation"];
self.lineView.hidden = YES;
}
- 回到ScanCodeViewController 配置掃碼范圍
重點(diǎn): 這是作者最想講的 . 附上官方介紹
rectOfInterest Property
A rectangle of interest for limiting the search area for visual metadata.
Discussion
The value of this property is a CGRect value that determines the object’s rectangle of interest for each frame of video.
The rectangle's origin is top left and is relative to the coordinate space of the device providing the metadata.
Specifying a rectangle of interest may improve detection performance for certain types of metadata. Metadata objects whose bounds do not intersect with the rectOfInterest will not be returned.
The default value of this property is a rectangle of (0.0, 0.0, 1.0, 1.0).
說明 :
看到矩形的原點(diǎn)是左上角, 但是真正測試 你會(huì)發(fā)現(xiàn)卻是在右上角, 因?yàn)閽叽a默認(rèn)是 橫屏, 所以原右上角變成左上角, 原寬變成高, 原高變成寬. 取值是按照 攝像頭分辨率 來取的比例 而不是屏幕的寬高比例.
屏幕寬高比例:
iPhone4 : [320 480] ; iPhone5: [320 568 ] ; iPhone 6 : [375 667] ; iPhone 6plus : [414 736].
作者設(shè)置 AVCaptureSessionPresetHigh 所以機(jī)型分辨率均為 1920×1080. 所以除了iPhone4 基本上 屏幕寬高比 符合 分辨率的比例. 會(huì)有些許誤差, 但影響不大. 如需支持包含iPhone4的所以機(jī)型 需要將 屏幕寬高與分辨率統(tǒng)一. 方法如下. 這樣便將 ShadowView 中間清空的矩形框 與 有效掃碼范圍 對(duì)應(yīng)上了.
/** 配置掃碼范圍 */
-(void)allowScanRect{
/** 掃描是默認(rèn)是橫屏, 原點(diǎn)在[右上角]
* rectOfInterest = CGRectMake(0, 0, 1, 1);
* AVCaptureSessionPresetHigh = 1920×1080 攝像頭分辨率
* 需要轉(zhuǎn)換坐標(biāo) 將屏幕與 分辨率統(tǒng)一
*/
//剪切出需要的大小位置
CGRect shearRect = CGRectMake((self.layerViewSize.width - self.showSize.width) / 2,
(self.layerViewSize.height - self.showSize.height) / 2,
self.showSize.height,
self.showSize.height);
CGFloat deviceProportion = 1920.0 / 1080.0;
CGFloat screenProportion = self.layerViewSize.height / self.layerViewSize.width;
//分辨率比> 屏幕比 ( 相當(dāng)于屏幕的高不夠)
if (deviceProportion > screenProportion) {
//換算出 分辨率比 對(duì)應(yīng)的 屏幕高
CGFloat finalHeight = self.layerViewSize.width * deviceProportion;
// 得到 偏差值
CGFloat addNum = (finalHeight - self.layerViewSize.height) / 2;
// (對(duì)應(yīng)的實(shí)際位置 + 偏差值) / 換算后的屏幕高
self.output.rectOfInterest = CGRectMake((shearRect.origin.y + addNum) / finalHeight,
shearRect.origin.x / self.layerViewSize.width,
shearRect.size.height/ finalHeight,
shearRect.size.width/ self.layerViewSize.width);
}else{
CGFloat finalWidth = self.layerViewSize.height / deviceProportion;
CGFloat addNum = (finalWidth - self.layerViewSize.width) / 2;
self.output.rectOfInterest = CGRectMake(shearRect.origin.y / self.layerViewSize.height,
(shearRect.origin.x + addNum) / finalWidth,
shearRect.size.height / self.layerViewSize.height,
shearRect.size.width / finalWidth);
}
}
- 讀取相冊(cè)中二維碼
**注意事項(xiàng) **: ios8 以上系統(tǒng)才開放讀取相冊(cè)二維碼功能(CIDetectorTypeQRCode), 所以如iPhone4 該功能實(shí)現(xiàn)不了, 需要有判斷.
#pragma mark - 相冊(cè)中讀取二維碼
/* navi按鈕實(shí)現(xiàn) */
-(void)takeQRCodeFromPic:(UIBarButtonItem *)leftBar{
if ([[[UIDevice currentDevice] systemVersion] doubleValue] < 8) {
UIAlertView * alert = [[UIAlertView alloc]initWithTitle:@"提示" message:@"請(qǐng)更新系統(tǒng)至8.0以上!" delegate:nil cancelButtonTitle:@"確定" otherButtonTitles:nil, nil];
[alert show];
}else{
if([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypePhotoLibrary]){
UIImagePickerController *pickerC = [[UIImagePickerController alloc] init];
pickerC.delegate = self;
pickerC.sourceType = UIImagePickerControllerSourceTypeSavedPhotosAlbum; //來自相冊(cè)
[self presentViewController:pickerC animated:YES completion:NULL];
}else{
UIAlertView * alert = [[UIAlertView alloc]initWithTitle:@"提示" message:@"設(shè)備不支持訪問相冊(cè),請(qǐng)?jiān)谠O(shè)置->隱私->照片中進(jìn)行設(shè)置!" delegate:nil cancelButtonTitle:@"確定" otherButtonTitles:nil, nil];
[alert show];
}
}
}
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
//1.獲取選擇的圖片
UIImage *image = info[UIImagePickerControllerEditedImage];
if (!image) {
image = info[UIImagePickerControllerOriginalImage];
}
//2.初始化一個(gè)監(jiān)測器
CIDetector*detector = [CIDetector detectorOfType:CIDetectorTypeQRCode context:nil options:@{ CIDetectorAccuracy : CIDetectorAccuracyHigh }];
[picker dismissViewControllerAnimated:YES completion:^{
//監(jiān)測到的結(jié)果數(shù)組 放置識(shí)別完之后的數(shù)據(jù)
NSArray *features = [detector featuresInImage:[CIImage imageWithCGImage:image.CGImage]];
//判斷是否有數(shù)據(jù)(即是否是二維碼)
if (features.count >=1) {
/**結(jié)果對(duì)象 */
CIQRCodeFeature *feature = [features objectAtIndex:0];
NSString *scannedResult = feature.messageString;
UIAlertView * alertView = [[UIAlertView alloc]initWithTitle:@"提示" message:scannedResult delegate:nil cancelButtonTitle:@"確定" otherButtonTitles:nil, nil];
[alertView show];
}
else{
UIAlertView * alertView = [[UIAlertView alloc]initWithTitle:@"提示" message:@"該圖片沒有包含二維碼!" delegate:nil cancelButtonTitle:@"確定" otherButtonTitles:nil, nil];
[alertView show];
}
}];
}
- 重寫調(diào)用
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
//顯示范圍
self.showSize = customShowSize;
//調(diào)用
[self creatScanQR];
//添加拍攝圖層
[self.view.layer addSublayer:self.layerView];
//開始二維碼
[self.session startRunning];
//設(shè)置可用掃碼范圍
[self allowScanRect];
//添加上層陰影視圖
self.shadowView = [[ShadowView alloc] initWithFrame:CGRectMake(0, 64, kWidth, kHeight - 64)];
[self.view addSubview:self.shadowView];
self.shadowView.showSize = self.showSize;
//添加掃碼相冊(cè)按鈕
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"相冊(cè)中選" style:UIBarButtonItemStylePlain target:self action:@selector(takeQRCodeFromPic:)];
// Do any additional setup after loading the view.
}
- 權(quán)限問題
權(quán)限問題 使用相機(jī)需要獲取相應(yīng)權(quán)限, 如用戶未開啟, 可以設(shè)置提醒, 留個(gè)小尾巴, 讀者自行設(shè)置.
三 iOS 二維碼生成
天聽云道的 [ iOS ] 生成二維碼 系統(tǒng)方法 這篇文章寫了, 挺簡單, 生成的是高清二維碼, 但是 在ios7系統(tǒng)上, 還是不清晰, 也就是說如果 你的程序支持到ios7 該方法有些問題.
船長_的 iOS的生成二維碼(彩色+陰影) 這篇文章寫了, 支持ios7 系統(tǒng), 高清二維碼.
所以對(duì)二維碼生成問題 作者就不贅述了. 對(duì)iOS中二維碼的講解完成.
四 三方二維碼鏈接
補(bǔ)充下三方傳送門 : https://github.com/MxABC/LBXScan
https://github.com/TheLevelUp/ZXingObjC
** 以上 ! **