iOS 扫描二维码/条形码

百家 作者:iOS开发 2018-12-14 13:42:33

Linux编程
点击右侧关注,免费入门到精通!


作者丨QiShare

https://www.jianshu.com/p/6529f5729b36


最近做IoT项目,在智能设备配网过程中有一个扫描设备或说明书上的二维码/条形码来读取设备信息的需求,要达到的效果大体如下:



想到几年前在帐号卫士中开发过扫码功能,就扒出来封装了一下

https://github.com/QiShare/QiQRCode

以方便在项目中复用。


封装共包括 QiCodeManager 和 QiCodePreviewView 两个类。QiCodeManager 负责扫描功能(二维码/条形码的识别和读取等),QiCodePreviewView 负责扫描界面(扫码框、扫描线、提示语等)。可按照如下方式在项目中使用两个类。


// 初始化扫码界面
_previewView = [[QiCodePreviewView alloc] initWithFrame:self.view.bounds];
_previewView.autoresizingMask = UIViewAutoresizingFlexibleHeight;
[self.view addSubview:_previewView];

// 初始化扫码管理类
__weak typeof(self) weakSelf = self;
_codeManager = [[QiCodeManager alloc] initWithPreviewView:_previewView completion:^{
    // 开始扫描
    [weakSelf.codeManager startScanningWithCallback:^(NSString * _Nonnull code) {} autoStop:YES];
}];


QiCodePreviewView 内部使用 CAShapeLayer 绘制了遮罩 maskLayer、扫描框 rectLayer、框角标 cornerLayer 和扫描线 lineLayer。因为此部分涉及代码较多,本文不做详解,可从QiQRCode中查看源码。


接下来重点介绍一下 QiCodeManager 中扫码功能的实现过程。


一、识别(捕捉)二维码/条形码


QiCodeManager是基于iOS 7+,对 AVFoundation框架中的 AVCaptureSession及相关类进行的封装。 


AVCaptureSession是 AVFoundation框架中捕捉音视频等数据的核心类。要实现扫码功能,除了用到 AVCaptureSession之外,还要用到 AVCaptureDevice、 AVCaptureDeviceInput、 AVCaptureMetadataOutput和 AVCaptureVideoPreviewLayer。


核心代码如下:


// input
AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:device error:nil];

// output
AVCaptureMetadataOutput *output = [[AVCaptureMetadataOutput alloc] init];
[output setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];

// session
_session = [[AVCaptureSession alloc] init];
_session.sessionPreset = AVCaptureSessionPresetHigh;
if ([_session canAddInput:input]) {
    [_session addInput:input];
}
if ([_session canAddOutput:output]) {
    [_session addOutput:output];
    // output在被add到session后才可设置metadataObjectTypes属性
    output.metadataObjectTypes = @[AVMetadataObjectTypeQRCodeAVMetadataObjectTypeCode128CodeAVMetadataObjectTypeEAN13Code];    
}

// previewLayer
AVCaptureVideoPreviewLayer *previewLayer = [AVCaptureVideoPreviewLayer layerWithSession:_session];
previewLayer.frame = previewView.layer.bounds;
previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
[previewView.layer insertSublayer:previewLayer atIndex:0];


// AVCaptureMetadataOutputObjectsDelegate
- (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof  AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection {

    AVMetadataMachineReadableCodeObject *code = metadataObjects.firstObject;
    if (code.stringValue) { }
}


以“面向人脑”的编程思想对上述代码进行解释:


1、我们需要使用AVCaptureVideoPreviewLayer的实例 previewLayer显示扫描二维码/条形码时看到的影像;


2、但是 previewLayer 的初始化需要 AVCaptureSession 的实例 session 对数据的输入输出进行控制;


3、那我们就初始化一个 session ,并将输出流的质量设置为高质量 AVCaptureSessionPresetHigh;


4、因为 session 是依
靠 AVCaptureDeviceInput 和 AVCaptureMetadataOutput 来控制数据输入输出的;


5、那就用AVCaptureDevice的实例 device 初始化一个 input,指明 device为AVMediaTypeVideo类型;


6、再初始化一个 output,设置好 delegate 和 queue 以及所支持的元数据类型(二维码和不同格式的条形码);


7、然后将 input 和 output 添加到 session 中就OK了,调用[session startRunning]; 就可以扫描二维码了;


8、最终从-captureOutput:didOutputMetadataObjects:fromConnection:方法中得到捕捉到的二维码/条形码数据。


至此,在previewLayer范围内就可以识别二维码/条形码了。


二、指定识别二维码/条形码的区域


如果要控制在 previewLayer 的指定区域内识别二维码/条形码,可以通过修改 output 的rectOfInterest 属性来达到目的。代码如下:


// 计算rect坐标
CGFloat y = rectFrame.origin.y;
CGFloat x = previewView.bounds.size.width - rectFrame.origin.x - rectFrame.size.width;
CGFloat h = rectFrame.size.height;
CGFloat w = rectFrame.size.width;
CGFloat rectY = y / previewView.bounds.size.height;
CGFloat rectX = x / previewView.bounds.size.width;
CGFloat rectH = h / previewView.bounds.size.height;
CGFloat rectW = w / previewView.bounds.size.width;

// 坐标赋值
output.rectOfInterest = CGRectMake(rectY, rectX, rectH, rectW);


1、上述的 CGRectMake(rectY, rectX, rectH, rectW) 与 CGRectMake(x, y, w, h) 的传统定义不同,可以将 rectOfInterest 理解成被翻转过的 CGRect;


2、而 rectY, rectX, rectH, rectW 也不是控件或区域的值,而是所对应的比例,如上述代码中的计算公式,y, x, h, w 的值可参考下图;


3、rectOfInterest 的默认值为CGRectMake(.0, .0, 1.0, 1.0),表示识别二维码/条形码的区域为全屏(previewLayer区域)。



三、拉近二维码/条形码(放大视频内容)


当二维码/条形码离我们较远时,拉近二维码/条形码会是一个不错的功能,效果如下:



上述效果是使用双指缩放的方式来实现的,具体代码如下:


// 添加缩放手势
UIPinchGestureRecognizer *pinchGesture = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinch:)];
[previewView addGestureRecognizer:pinchGesture];


- (void)pinch:(UIPinchGestureRecognizer *)gesture {

    AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];

    // 设定有效缩放范围,防止超出范围而崩溃
    CGFloat minZoomFactor = 1.0;
    CGFloat maxZoomFactor = device.activeFormat.videoMaxZoomFactor;
    if (@available(iOS 11.0, *)) {
        minZoomFactor = device.minAvailableVideoZoomFactor;
        maxZoomFactor = device.maxAvailableVideoZoomFactor;
    }

    static CGFloat lastZoomFactor = 1.0;
    if (gesture.state == UIGestureRecognizerStateBegan) {
        // 记录上次缩放的比例,本次缩放在上次的基础上叠加
        lastZoomFactor = device.videoZoomFactor;// lastZoomFactor为外部变量
    }
    else if (gesture.state == UIGestureRecognizerStateChanged) {
        CGFloat zoomFactor = lastZoomFactor * gesture.scale;
        zoomFactor = fmaxf(fminf(zoomFactor, maxZoomFactor), minZoomFactor);
        [device lockForConfiguration:nil];// 修改device属性之前须lock
        device.videoZoomFactor = zoomFactor;// 修改device的视频缩放比例
        [device unlockForConfiguration];// 修改device属性之后unlock
    }
}


上述代码的核心逻辑比较简单:


1、在 previewView 上添加一个双指捏合的手势 pinchGesture,并设定 target 和 selector;


2、在 selector方法中根据 gesture.scale 调整 device.videoZoomFactor;


3、注意在修改 device 属性之前要 lock 一下,修改完后 unlock 一下。


四、弱光环境下开启手电筒


弱光环境对扫码功能有较大的影响,通过监测光线亮度给用户提供打开手电筒的选择会提升不少的体验,如下图:弱光监测的代码如下:



- (void)observeLightStatus:(void (^)(BOOLBOOL))lightObserver {

    _lightObserver = lightObserver;

    AVCaptureVideoDataOutput *lightOutput = [[AVCaptureVideoDataOutput alloc] init];
    [lightOutput setSampleBufferDelegate:self queue:dispatch_get_main_queue()];

    if ([_session canAddOutput:lightOutput]) {
        [_session addOutput:lightOutput];
    }
}

// AVCaptureVideoDataOutputSampleBufferDelegate
- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {

    // 通过sampleBuffer获取到光线亮度值brightness
    CFDictionaryRef metadataDicRef = CMCopyDictionaryOfAttachments(NULL, sampleBuffer, kCMAttachmentMode_ShouldPropagate);
    NSDictionary *metadataDic = (__bridge NSDictionary *)metadataDicRef;
    CFRelease(metadataDicRef);
    NSDictionary *exifDic = metadataDic[(__bridge NSString *)kCGImagePropertyExifDictionary];
    CGFloat brightness = [exifDic[(__bridge NSString *)kCGImagePropertyExifBrightnessValue] floatValue];

    // 初始化一些变量,作为是否透传brightness的因数
    AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
    BOOL torchOn = device.torchMode == AVCaptureTorchModeOn;
    BOOL dimmed = brightness <  1.0;
    static BOOL lastDimmed = NO;

    // 控制透传逻辑:第一次监测到光线或者光线明暗变化(dimmed变化)时透传
    if (_lightObserver) {
        if (!_lightObserverHasCalled) {
            _lightObserver(dimmed, torchOn);
            _lightObserverHasCalled = YES;
            lastDimmed = dimmed;
        }
        else if (dimmed != lastDimmed) {
            _lightObserver(dimmed, torchOn);
            lastDimmed = dimmed;
        }
    }
}


1、

初始化 AVCaptureVideoDataOutput 的实例 lightOutput 后,设定 delegate 并将 lightOutput添加到 session 中;


2、

实现 

AVCaptureVideoDataOutputSampleBufferDelegate 的回调方法 -captureOutput:didOutputSampleBuffer:fromConnection:;


3、

对回调方法中的 sampleBuffer 进行各种操作(具体参考上述代码细节),并最终获取到光线亮度 brightness;


4、

根据 brightness 的值设定弱光的标准以及是否透传给业务逻辑(这里认为 brightness <  1.0 为弱光)。


调用 -observeLightStatus:方法并实现 blck 即可接收透传过来的光线状态和手电筒状态,并根据状态对 UI 做相应的调整,代码如下:


__weak typeof(self) weakSelf = self;
[self observeLightStatus:^(BOOL dimmed, BOOL torchOn) {
    if (dimmed || torchOn) {// 变为弱光或者手电筒处于开启状态
        [weakSelf.previewView stopScanning];// 停止扫描动画
        [weakSelf.previewView showTorchSwitch];// 显示手电筒开关
    } else {// 变为亮光并且手电筒处于关闭状态
        [weakSelf.previewView startScanning];// 开始扫描动画
        [weakSelf.previewView hideTorchSwitch];// 隐藏手电筒开关
    }
}];


当出现手电筒开关时,我们可以通过点击开关改变手电筒的状态。开关手电筒的代码如下:


+ (void)switchTorch:(BOOL)on {

    AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
    AVCaptureTorchMode torchMode = on? AVCaptureTorchModeOnAVCaptureTorchModeOff;

    if (device.hasFlash && device.hasTorch && torchMode != device.torchMode) {
        [device lockForConfiguration:nil];// 修改device属性之前须lock
        [device setTorchMode:torchMode];// 修改device的手电筒状态
        [device unlockForConfiguration];// 修改device属性之后unlock
    }
}


手电筒开关(按钮)封装在 QiCodePreviewView 中,QiCodeManager 中通过QiCodePreviewViewDelegate 的 -codeScanningView:didClickedTorchSwitch:方法获取手电筒开关的点击事件,并做相应的逻辑处理。代码如下:


// QiCodePreviewViewDelegate
- (void)codeScanningView:(QiCodePreviewView *)scanningView didClickedTorchSwitch:(UIButton *)switchButton {

    switchButton.selected = !switchButton.selected;

    [QiCodeManager switchTorch:switchButton.selected];
    _lightObserverHasCalled = switchButton.selected;
}


综上,扫描二维码/条形码的功能就实现完了。


源码:


https://github.com/QiShare/QiQRCode.git


 推荐↓↓↓ 

?16个技术公众号】都在这里!

涵盖:程序员大咖、源码共读、程序员共读、数据结构与算法、黑客技术和网络安全、大数据科技、编程前端、Java、Python、Web编程开发、Android、iOS开发、Linux、数据库研发、幽默程序员等。

关注公众号:拾黑(shiheibook)了解更多

[广告]赞助链接:

四季很好,只要有你,文娱排行榜:https://www.yaopaiming.com/
让资讯触达的更精准有趣:https://www.0xu.cn/

公众号 关注网络尖刀微信公众号
随时掌握互联网精彩
赞助链接