iOS 扫描二维码/条形码

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

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

https://github.com/QiShare/QiQRCode

以方便在项目中复用。

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

// 初始化扫码界面

_previewView = [[QiCodePreviewView alloc] initWithFrame:self.view.bounds];

_previewView.autoresizingMask =UIViewAutoresizingFlexibleHeight;

[self.view addSubview:_previewView];

// 初始化扫码管理类

__weaktypeof(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 = [AVCaptureDevicedefaultDeviceWithMediaType:AVMediaTypeVideo];

AVCaptureDeviceInput*input = [AVCaptureDeviceInputdeviceInputWithDevice:device error:nil];

// output

AVCaptureMetadataOutput*output = [[AVCaptureMetadataOutputalloc] init];

[output setMetadataObjectsDelegate:selfqueue:dispatch_get_main_queue()];

// session

_session = [[AVCaptureSessionalloc] init];

_session.sessionPreset =AVCaptureSessionPresetHigh;

if([_session canAddInput:input]) {

[_session addInput:input];

}

if([_session canAddOutput:output]) {

[_session addOutput:output];

// output在被add到session后才可设置metadataObjectTypes属性

output.metadataObjectTypes = @[AVMetadataObjectTypeQRCode,AVMetadataObjectTypeCode128Code,AVMetadataObjectTypeEAN13Code];

}

// previewLayer

AVCaptureVideoPreviewLayer*previewLayer = [AVCaptureVideoPreviewLayerlayerWithSession:_session];

previewLayer.frame = previewView.layer.bounds;

previewLayer.videoGravity =AVLayerVideoGravityResizeAspectFill;

[previewView.layer insertSublayer:previewLayer atIndex:0];

// AVCaptureMetadataOutputObjectsDelegate

- (void)captureOutput:(AVCaptureOutput*)output didOutputMetadataObjects:(NSArray<__kindofAVMetadataObject*> *)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坐标

CGFloaty = rectFrame.origin.y;

CGFloatx = previewView.bounds.size.width - rectFrame.origin.x - rectFrame.size.width;

CGFloath = rectFrame.size.height;

CGFloatw = rectFrame.size.width;

CGFloatrectY = y / previewView.bounds.size.height;

CGFloatrectX = x / previewView.bounds.size.width;

CGFloatrectH = h / previewView.bounds.size.height;

CGFloatrectW = 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 = [[UIPinchGestureRecognizeralloc] initWithTarget:selfaction:@selector(pinch:)];

[previewView addGestureRecognizer:pinchGesture];

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

AVCaptureDevice*device = [AVCaptureDevicedefaultDeviceWithMediaType:AVMediaTypeVideo];

// 设定有效缩放范围,防止超出范围而崩溃

CGFloatminZoomFactor =1.0;

CGFloatmaxZoomFactor = device.activeFormat.videoMaxZoomFactor;

if(@available(iOS11.0, *)) {

minZoomFactor = device.minAvailableVideoZoomFactor;

maxZoomFactor = device.maxAvailableVideoZoomFactor;

}

staticCGFloatlastZoomFactor =1.0;

if(gesture.state ==UIGestureRecognizerStateBegan) {

// 记录上次缩放的比例,本次缩放在上次的基础上叠加

lastZoomFactor = device.videoZoomFactor;// lastZoomFactor为外部变量

}

elseif(gesture.state ==UIGestureRecognizerStateChanged) {

CGFloatzoomFactor = 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(^)(BOOL,BOOL))lightObserver {

_lightObserver = lightObserver;

AVCaptureVideoDataOutput*lightOutput = [[AVCaptureVideoDataOutputalloc] init];

[lightOutput setSampleBufferDelegate:selfqueue:dispatch_get_main_queue()];

if([_session canAddOutput:lightOutput]) {

[_session addOutput:lightOutput];

}

}

// AVCaptureVideoDataOutputSampleBufferDelegate

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

// 通过sampleBuffer获取到光线亮度值brightness

CFDictionaryRefmetadataDicRef =CMCopyDictionaryOfAttachments(NULL, sampleBuffer, kCMAttachmentMode_ShouldPropagate);

NSDictionary*metadataDic = (__bridgeNSDictionary*)metadataDicRef;

CFRelease(metadataDicRef);

NSDictionary*exifDic = metadataDic[(__bridgeNSString*)kCGImagePropertyExifDictionary];

CGFloatbrightness = [exifDic[(__bridgeNSString*)kCGImagePropertyExifBrightnessValue] floatValue];

// 初始化一些变量,作为是否透传brightness的因数

AVCaptureDevice*device = [AVCaptureDevicedefaultDeviceWithMediaType:AVMediaTypeVideo];

BOOLtorchOn = device.torchMode ==AVCaptureTorchModeOn;

BOOLdimmed = brightness <1.0;

staticBOOLlastDimmed =NO;

// 控制透传逻辑:第一次监测到光线或者光线明暗变化(dimmed变化)时透传

if(_lightObserver) {

if(!_lightObserverHasCalled) {

_lightObserver(dimmed, torchOn);

_lightObserverHasCalled =YES;

lastDimmed = dimmed;

}

elseif(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 做相应的调整,代码如下:

__weaktypeof(self) weakSelf =self;

[selfobserveLightStatus:^(BOOLdimmed,BOOLtorchOn) {

if(dimmed || torchOn) {// 变为弱光或者手电筒处于开启状态

[weakSelf.previewView stopScanning];// 停止扫描动画

[weakSelf.previewView showTorchSwitch];// 显示手电筒开关

}else{// 变为亮光并且手电筒处于关闭状态

[weakSelf.previewView startScanning];// 开始扫描动画

[weakSelf.previewView hideTorchSwitch];// 隐藏手电筒开关

}

}];

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

+ (void)switchTorch:(BOOL)on {

AVCaptureDevice*device = [AVCaptureDevicedefaultDeviceWithMediaType:AVMediaTypeVideo];

AVCaptureTorchModetorchMode = on?AVCaptureTorchModeOn:AVCaptureTorchModeOff;

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


V     X   获 取 更 多 精彩 内容

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容