級別:★★☆☆☆
標籤:「iOS 原生掃描」「AVCaptureSession」「AVCaptureDevice」「rectOfInterest」
做者: Xs·H
審校: QiShare團隊php
最近作IoT項目,在智能設備配網過程當中有一個掃描設備或說明書上的二維碼/條形碼來讀取設備信息的需求,要達到的效果大致以下:git
想到幾年前在賬號衛士中開發過掃碼功能,就扒出來封裝了一下(能夠從QiQRCode中獲取),以方便在項目中複用。
封裝共包括QiCodeManager和QiCodePreviewView兩個類。QiCodeManager負責掃描功能(二維碼/條形碼的識別和讀取等),QiCodePreviewView負責掃描界面(掃碼框、掃描線、提示語等)。可按照以下方式在項目中使用兩個類。github
// 初始化掃碼界面
_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
。核心代碼以下:session
// 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) { }
}
複製代碼
以「面向人腦」的編程思想對上述代碼進行解釋:
一、咱們須要使用AVCaptureVideoPreviewLayer的實例previewLayer
顯示掃描二維碼/條形碼時看到的影像;
二、可是previewLayer
的初始化須要AVCaptureSession的實例session
對數據的輸入輸出進行控制;
三、那咱們就初始化一個session
,並將輸出流的質量設置爲高質量AVCaptureSessionPresetHigh;
四、由於session
是依靠AVCaptureDeviceInput和AVCaptureMetadataOutput來控制數據輸入輸出的;
五、那就用AVCaptureDevice的實例device
初始化一個input
,指明device
爲AVMediaTypeVideo類型;
六、再初始化一個output
,設置好delegate和queue以及所支持的元數據類型(二維碼和不一樣格式的條形碼);
七、而後將input
和output
添加到session
中就OK了,調用[session startRunning];就能夠掃描二維碼了;
八、最終從-captureOutput:didOutputMetadataObjects:fromConnection:方法中獲得捕捉到的二維碼/條形碼數據。框架
至此,在previewLayer範圍內就能夠識別二維碼/條形碼了。ide
若是要控制在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);
複製代碼
一、上述的CGRectMake(rectY, rectX, rectH, rectW)與CGRectMake(x, y, w, h)的傳統定義不一樣,能夠將
rectOfInterest
理解成被翻轉過的CGRect;
二、而rectY
,rectX
,rectH
,rectW
也不是控件或區域的值,而是所對應的比例,如上述代碼中的計算公式,y, x, h, w的值可參考下圖;
三、rectOfInterest
的默認值爲CGRectMake(.0, .0, 1.0, 1.0),表示識別二維碼/條形碼的區域爲全屏(previewLayer區域)。spa
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
}
}
複製代碼
上述代碼的核心邏輯比較簡單:
一、在previewView
上添加一個雙指捏合的手勢pinchGesture
,並設定target和selector
;
二、在selector
方法中根據gesture.scale
調整device.videoZoomFactor
;
三、注意在修改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來實現的。
一、初始化AVCaptureVideoDataOutput的實例lightOutput
後,設定delegate並將lightOutput
添加到session
中;
二、實現AVCaptureVideoDataOutputSampleBufferDelegate的回調方法-captureOutput:didOutputSampleBuffer:fromConnection:
;
三、對回調方法中的sampleBuffer
進行各類操做(具體參考上述代碼細節),並最終獲取到光線亮度brightness
;
四、根據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(簡書)
QiShare(掘金)
QiShare(知乎)
QiShare(GitHub)
QiShare(CocoaChina)
QiShare(StackOverflow)
QiShare(微信公衆號)
推薦文章:
iOS 瞭解Xcode Bitcode
iOS 重繪之drawRect
iOS 編寫高質量Objective-C代碼(八)
iOS KVC與KVO簡介
奇舞週刊