内容简介:级别:★★☆☆☆标签:「iOS 原生扫描」「AVCaptureSession」「AVCaptureDevice」「rectOfInterest」作者: Xs·H
级别:★★☆☆☆
标签:「iOS 原生扫描」「AVCaptureSession」「AVCaptureDevice」「rectOfInterest」
作者: Xs·H
审校:QiShare团队
最近做IoT项目,在智能设备配网过程中有一个扫描设备或说明书上的二维码/条形码来读取设备信息的需求,要达到的效果大体如下:
想到几年前在帐号卫士中开发过扫码功能,就扒出来封装了一下(可以从 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 中查看源码。关于CAShapeLayer的使用,QiShare在iOS 绘制圆角文章中有介绍到。
接下来重点介绍一下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 = @[AVMetadataObjectTypeQRCode, AVMetadataObjectTypeCode128Code, AVMetadataObjectTypeEAN13Code]; } // 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区域)。
PS: 其实iOS提供了官方API来将标准rect转换成 rectOfInterest
,但只有在 [session startRunning]
之后调用才有效果,而且还会时不时地出现卡顿式地闪一下。代码如下:
// 可以在[session startRunning]之后用此语句设置扫码区域 metadataOutput.rectOfInterest = [previewLayer metadataOutputRectOfInterestForRect:rectFrame]; 复制代码
三、拉近二维码/条形码(放大视频内容)
当二维码/条形码离我们较远时,拉近二维码/条形码会是一个不错的功能,效果如下:
上述效果是使用双指缩放的方式来实现的,具体代码如下:
// 添加缩放手势 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 (^)(BOOL, BOOL))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; } } } 复制代码
弱光监测是依赖AVCaptureVideoDataOutput和AVCaptureVideoDataOutputSampleBufferDelegate来实现的。
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? 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; } 复制代码
综上,扫描二维码/条形码的功能就实现完了。此外,QiCodeManager中还封装了生成二维码/条形码的方法,下篇文章介绍。
示例源码: QiQRCode 可从GitHub的 QiShare开源库 中获取。
关注我们的途径有:
QiShare(微信公众号)
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- iOS 生成二维码/条形码
- zbar条形码识别库vs配置
- 小程序中生成二维码和条形码
- Seppiko Chart 1.5 已经发布,Java 条形码库
- 用OpenCV和Python识别二维码和条形码
- 条形码处理类库 ZXing 3.3.3 发布,支持 Java 9
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。