文章目录
- 前言
- 正文
- 1.定位二维码的位置
- 2.扫码、解析
- 总结
前言
业务中一直有扫码的需求,这次说需要扫多个码(详细一点是一图多码),有点东西的。
第一点:怎么做:拿到手第一反应是有没有什么库可以直接调用的,不动脑星人检索了一下Zbar和ZXing,ZXing不适合iOS、Zbar也没有一图多码的方法可以直接用的,况且引入库增加代价也高,最后,原生撸吧。
第二点:做成什么样:app有什么对标的,打开支某宝、微某的二维码扫码看看,平时自己也遇见过吧。
扫码:
- 识别到一个码,跳转对应链接
- 识别到多个码,显示二维码定位位置,点击定位图跳转
第三点:划重点,原生扫码的方式很简单,难点在于如何定位到二维码的位置,上菜。
正文
1.定位二维码的位置
思路参考:二维码扫码效果(多个二维码识别和点选)
首先二维码识别原理是三个角标定位的,然后再通过角标读取内部信息;假设扫码layer是整个view,再将识别到的坐标信息转换到view上的坐标,这样就可以得到定位的信息进行绘图标注。
2.扫码、解析
扫码需要放到队列去,否则容易主线程阻塞。相机权限得先请求开启一波。
#import <AVFoundation/AVFoundation.h>
// AVCaptureMetadataOutputObjectsDelegate
///主队列
#define GCD_main_queue dispatch_get_main_queue()
#define WeakSelf typeof(self) __weak weakSelf = self;
// 输入输出中间桥梁(会话)
@property (strong, nonatomic) AVCaptureSession *session;
// 多个二维码 定位的数组
@property (strong, nonatomic) NSMutableArray *qrCodesArray;
//扫码点选回调
@property (nonatomic, copy) void(^clickQrCodeBlock)(NSString *qrStr);
//多个二维码位置点击按钮数组
@property (strong, nonatomic) NSMutableArray *qrCodesButtonArray;
// 重新扫码
@property (strong, nonatomic) UIButton *reScanButton;
- (void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
if (granted) {
dispatch_async(GCD_main_queue, ^{
[self startRunning];// 扫码
});
}else{
dispatch_async(GCD_main_queue, ^{
//@"无法访问照相机,请在设置中打开相机权限"
});
}
}];
}
重启扫码的btn,这里可以自由发挥
self.reScanButton = [UIButton buttonWithType:UIButtonTypeCustom];//重新扫码
self.reScanButton.backgroundColor = [UIColor blueColor];
self.reScanButton.hidden = YES;
[self.reScanButton setTitle:@"重新扫码" forState:UIControlStateNormal];
self.reScanButton.backgroundColor = [UIColor blueColor];
[self.cameraView addSubview:self.reScanButton];
[self.reScanButton addTarget:self action:@selector(reScanBtnAction:) forControlEvents:UIControlEventTouchUpInside];
[self.reScanButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.equalTo(self.cameraView);
make.bottom.equalTo(self.cameraView.mas_bottom).offset(-60 * FTGetScreenScale());
make.size.mas_equalTo(CGSizeMake(200 * FTGetScreenScale(), 60 * FTGetScreenScale()));
}];
self.qrCodesButtonArray = [NSMutableArray new];
干货:扫码开启和暂停
/**
start running capture
*/
- (void)startRunning {
if(![self.session isRunning]){
[self.session startRunning];
}
if(self.qrCodesButtonArray.count){//移除上一次的标记
[self.qrCodesButtonArray enumerateObjectsUsingBlock:^(UIButton *button, NSUInteger idx, BOOL *stop) {
[button removeFromSuperview];
}];
self.qrCodesButtonArray = [NSMutableArray new];
self.reScanButton.hidden = YES;
}
}
/**
stop running capture
*/
- (void)stopRunning {
// reload animation on mainThread
WeakSelf
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf.session stopRunning];
});
}
- (AVCaptureSession *)session {
if (!_session) {
//1.获取输入设备(摄像头)
AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
//2.根据输入设备创建输入对象
AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:device error:NULL];
if (input == nil) {
return nil;
}
//3.创建元数据的输出对象
AVCaptureMetadataOutput *output = [[AVCaptureMetadataOutput alloc]init];
//4.设置代理监听输出对象输出的数据,在主线程中刷新
[output setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];
// 5.创建会话(桥梁)
AVCaptureSession *session = [[AVCaptureSession alloc]init];
//实现高质量的输出和摄像,默认值为AVCaptureSessionPresetHigh,可以不写
[session setSessionPreset:AVCaptureSessionPresetHigh];
// 6.添加输入和输出到会话中(判断session是否已满)
if ([session canAddInput:input]) {
[session addInput:input];
}
if ([session canAddOutput:output]) {
[session addOutput:output];
}
// 7.告诉输出对象, 需要输出什么样的数据 (二维码还是条形码等) 要先创建会话才能设置
output.metadataObjectTypes = @[AVMetadataObjectTypeQRCode,AVMetadataObjectTypeCode128Code,AVMetadataObjectTypeCode93Code,AVMetadataObjectTypeCode39Code,AVMetadataObjectTypeCode39Mod43Code,AVMetadataObjectTypeEAN8Code,AVMetadataObjectTypeEAN13Code,AVMetadataObjectTypeUPCECode,AVMetadataObjectTypePDF417Code,AVMetadataObjectTypeAztecCode];
// 8.创建预览图层
AVCaptureVideoPreviewLayer *previewLayer = [AVCaptureVideoPreviewLayer layerWithSession:session];
[previewLayer setVideoGravity:AVLayerVideoGravityResizeAspectFill];
previewLayer.frame = self.cameraView.bounds;
[self.cameraView.layer insertSublayer:previewLayer atIndex:0];
//9.设置有效扫描区域,默认整个图层(很特别,1、要除以屏幕宽高比例,2、其中x和y、width和height分别互换位置)
// CGRect rect = CGRectMake(kBgImgY/ScreenHeight, kBgImgX/ScreenWidth, kBgImgWidth/ScreenHeight, kBgImgWidth/ScreenWidth);
_session = session;
}
return _session;
}
#pragma mark AVCaptureMetadataOutputObjectsDelegate
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection{
if ([metadataObjects count] >0){
NSLog(@"========扫描后的url是:==============");
NSMutableArray *muchArray = [NSMutableArray new];
[metadataObjects enumerateObjectsUsingBlock:^(AVMetadataMachineReadableCodeObject *obj, NSUInteger idx, BOOL *stop) {
if ([obj.type isEqualToString:AVMetadataObjectTypeQRCode]) { //判断是否有数据,是否是二维码数据
[muchArray addObject:obj];
}
}];
[self stopRunning];// 我这里是扫码到结果就暂停扫码、加个重新扫描btn会用户友好一点,后面是处理扫码结果
if([muchArray count] == 1){//扫描到一个二维码信息
AVMetadataMachineReadableCodeObject * metadataObject = [muchArray objectAtIndex:0];
NSString *stringValue = metadataObject.stringValue;
self.urlString = stringValue.length?stringValue:@"";
NSLog(@" 1111扫描后的url是:%@",self.urlString);
if(self.urlString.length){
dispatch_async(dispatch_get_main_queue(), ^{
[self analyseResultAry:self.urlString];
self.reScanButton.hidden = NO;
});
}
}else if([muchArray count] >1){// 多个二维码信息 显示二维码定位页面 选择跳转
self.qrCodesArray = [NSMutableArray new];
[muchArray enumerateObjectsUsingBlock:^(AVMetadataMachineReadableCodeObject *result, NSUInteger idx, BOOL *stop) {
NSMutableDictionary *dic = [NSMutableDictionary new];
NSString *code = result.stringValue;
[dic setObject:code forKey:@"code"];
NSLog(@"2222 扫描后的url是:%@",code);
// 标注多个二维码
CGRect frame = [self makeFrameWithCodeObject:result Index:self.qrCodesArray.count];
NSString *frameStr = NSStringFromCGRect(frame);
[dic setObject:frameStr forKey:@"frame"];
[self.qrCodesArray addObject:dic];//记录下标注的数组,下次扫码移除前面的标注
}];
dispatch_async(dispatch_get_main_queue(), ^{
self.reScanButton.hidden = NO;
});
}
}
}
//选择多个二维码中的一个
- (void)handleBtnAction:(UIButton *)sender {
NSInteger index = sender.tag - 1000;
if (index < self.qrCodesArray.count) {
NSDictionary *dic = self.qrCodesArray[index];
if([dic.allKeys containsObject:@"code"]){
self.urlString = [dic objectForKey:@"code"]?[dic objectForKey:@"code"]:@"";
NSLog(@"2222 扫描后的url是: 选中 %@",self.urlString);
if(self.urlString.length){
[self analyseResultAry:self.urlString];
}
}
}
}
//重新扫码
-(void)reScanBtnAction:(UIButton *)sender {
[self startRunning];
}
/*
AVMetadataMachineReadableCodeObject,输出的点位坐标是其在原始数据流上的坐标,与屏幕视图坐标不一样,(坐标系,值都会有差别)
将坐标值转为屏幕显示的图像视图(self.videoPreviewLayer)上的坐标值
*/
-(CGRect)makeFrameWithCodeObject:(AVMetadataMachineReadableCodeObject *)objc Index:(NSInteger)index
{
//将二维码坐标转化为扫码控件输出视图上的坐标
// CGSize isize = CGSizeMake(720.0, 1280.0); // 尺寸可以考虑不要写死,当前设置的是captureSession.sessionPreset = AVCaptureSessionPreset1280x720;
CGSize isize = self.cameraView.frame.size; //扫码控件的输出尺寸,
float Wout = 0.00;
float Hout = 0.00;
BOOL wMore = YES;
/*取分辨率与输出的layer尺寸差,
此处以AVLayerVideoGravityResizeAspectFill填充方式为例,判断扫描的范围更宽还是更长,并计算出超出部分的尺寸,后续计算减去这部分。
如果是其它填充方式,计算方式不一样(比如AVLayerVideoGravityResizeAspect,则计算计算留白的尺寸,并后续补足这部分)
*/
if (isize.width/isize.height > self.cameraView.bounds.size.width/self.cameraView.bounds.size.height) {
//当更宽时,计算扫描的坐标x为0 的点比输出视图的0点差多少(输出视图为全屏时,即屏幕外有多少)
wMore = YES;
Wout = (isize.width/isize.height)* self.cameraView.bounds.size.height;
Wout = Wout - self.cameraView.bounds.size.width;
Wout = Wout/2;
}else{
// 当更长时,计算y轴超出多少。
wMore = NO;
Hout = (isize.height/isize.width)* self.cameraView.bounds.size.width;
Hout = Hout - self.cameraView.bounds.size.height;
Hout = Hout/2;
}
CGPoint point1 = CGPointZero;
CGPoint point2 = CGPointZero;
CGPoint point3 = CGPointZero;
CGPoint point4 = CGPointZero;
/*
源坐标系下frame和角点,都是比例值,即源视频流尺寸下的百分比值。
例子:frame :(x = 0.26720550656318665, y = 0.0014114481164142489), size = (width = 0.16406852006912231, height = 0.29584407806396484))
objc.corners:{0.26823519751360592, 0.29203594744002659}
{0.4312740177700658, 0.29725551905635411}
{0.4294213439632073, 0.012761536345436197}
{0.26720551457151021, 0.0014114481640513654}
*/
CGRect frame = objc.bounds;//在源坐标系的frame,
NSArray *array = objc.corners;//源坐标系下二维码的角点
CGPoint P = frame.origin;
CGSize S = frame.size;
//获取点
for (int n = 0; n< array.count; n++) {
CGPoint point = CGPointZero;
CFDictionaryRef dict = (__bridge CFDictionaryRef)(array[n]);
CGPointMakeWithDictionaryRepresentation(dict, &point);
// NSLog(@"二维码角点%@",NSStringFromCGPoint(point));
//交换xy轴
point.x = point.y + point.x;
point.y = point.x - point.y;
point.x = point.x - point.y;
//x轴反转
point.x = (1-point.x);
//point乘以比列。减去尺寸差,
if (wMore) {
point.x = (point.x * (isize.width/isize.height)* self.cameraView.bounds.size.height) - Wout;
point.y = self.cameraView.bounds.size.height *(point.y);
}else{
point.x = self.cameraView.bounds.size.width *(point.x);
point.y = (point.y) * (isize.height/isize.width)* self.cameraView.bounds.size.width - Hout;
}
if (n == 0) {
point1 = point;
}
if (n == 1) {
point2 = point;
}
if (n == 2) {
point3 = point;
}
if (n == 3) {
point4 = point;
}
}
//通过获取最小和最大的X,Y值,二维码在视图上的frame(前面得到的点不一定是正方形的二维码,也可能是菱形的或者有一定旋转角度的)
float minX = point1.x;
minX = minX>point2.x?point2.x:minX;
minX = minX>point3.x?point3.x:minX;
minX = minX>point4.x?point4.x:minX;
float minY = point1.y;
minY = minY>point2.y?point2.y:minY;
minY = minY>point3.y?point3.y:minY;
minY = minY>point4.y?point4.y:minY;
P.x = minX;
P.y = minY;
float maxX = point1.x;
maxX = maxX<point2.x?point2.x:maxX;
maxX = maxX<point3.x?point3.x:maxX;
maxX = maxX<point4.x?point4.x:maxX;
float maxY = point1.y;
maxY = maxY<point2.y?point2.y:maxY;
maxY = maxY<point3.y?point3.y:maxY;
maxY = maxY<point4.y?point4.y:maxY;
S.width = maxX - minX;
S.height = maxY - minY;
//y轴坐标方向调整
CGRect QRFrame = CGRectMake(P.x , P.y , S.width, S.height);
UIButton *tempButton = [UIButton buttonWithType:UIButtonTypeCustom];//多个二维码添加选择btn
tempButton.backgroundColor = [UIColor blueColor];
tempButton.frame = QRFrame;
[self.cameraView addSubview:tempButton];
tempButton.tag = 1000 + index;
[tempButton addTarget:self action:@selector(handleBtnAction:) forControlEvents:UIControlEventTouchUpInside];
[self.qrCodesButtonArray addObject:tempButton];
return QRFrame;
}
-(void)analyseResultAry:(NSString *)resultAsString{
WeakSelf
[[AFNetworkReachabilityManager sharedManager] startMonitoring];
[[AFNetworkReachabilityManager sharedManager] setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) {
[[AFNetworkReachabilityManager sharedManager] stopMonitoring];
if (status >= 1) {
// 有网络
if (resultAsString.length) {
// 结果干点啥~~~~😊😊😊😊😊😊😊😊😊
} else {
NSLog(@"识别结果内容:为空");
[weakSelf stopRunning];
}
} else {
NSLog(@"没有网络");
[weakSelf stopRunning];
}
}];
}
总结
思路看大厂,方法网上学,细节自己写,记录一手。