iOS扫码一图多码原生处理AVCaptureSession

前言

业务中一直有扫码的需求,这次说需要扫多个码(详细一点是一图多码),有点东西的。
第一点:怎么做:拿到手第一反应是有没有什么库可以直接调用的,不动脑星人检索了一下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];
        }
    }];
}

总结

思路看大厂,方法网上学,细节自己写,记录一手。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值