iOS 掃描二維碼/條形碼

級(jí)別:★★☆☆☆
標(biāo)簽:「iOS 原生掃描」「AVCaptureSession」「AVCaptureDevice」「rectOfInterest」
作者: Xs·H
審校: QiShare團(tuán)隊(duì)


最近做IoT項(xiàng)目,在智能設(shè)備配網(wǎng)過程中有一個(gè)掃描設(shè)備或說明書上的二維碼/條形碼來讀取設(shè)備信息的需求,要達(dá)到的效果大體如下:

掃碼效果

想到幾年前在帳號(hào)衛(wèi)士中開發(fā)過掃碼功能,就扒出來封裝了一下(可以從QiQRCode中獲?。?,以方便在項(xiàng)目中復(fù)用。
封裝共包括QiCodeManager和QiCodePreviewView兩個(gè)類。QiCodeManager負(fù)責(zé)掃描功能(二維碼/條形碼的識(shí)別和讀取等),QiCodePreviewView負(fù)責(zé)掃描界面(掃碼框、掃描線、提示語等)。可按照如下方式在項(xiàng)目中使用兩個(gè)類。

// 初始化掃碼界面
_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內(nèi)部使用CAShapeLayer繪制了遮罩maskLayer、掃描框rectLayer、框角標(biāo)cornerLayer和掃描線lineLayer。因?yàn)榇瞬糠稚婕按a較多,本文不做詳解,可從QiQRCode中查看源碼。關(guān)于CAShapeLayer的使用,QiShare在iOS 繪制圓角文章中有介紹到。

接下來重點(diǎn)介紹一下QiCodeManager中掃碼功能的實(shí)現(xiàn)過程。

一、識(shí)別(捕捉)二維碼/條形碼

QiCodeManager是基于iOS 7+,對(duì)AVFoundation框架中的AVCaptureSession及相關(guān)類進(jìn)行的封裝。AVCaptureSessionAVFoundation框架中捕捉音視頻等數(shù)據(jù)的核心類。要實(shí)現(xiàn)掃碼功能,除了用到AVCaptureSession之外,還要用到AVCaptureDevice、AVCaptureDeviceInput、AVCaptureMetadataOutputAVCaptureVideoPreviewLayer。核心代碼如下:

// 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后才可設(shè)置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) { }
}

以“面向人腦”的編程思想對(duì)上述代碼進(jìn)行解釋:
1、我們需要使用AVCaptureVideoPreviewLayer的實(shí)例previewLayer顯示掃描二維碼/條形碼時(shí)看到的影像;
2、但是previewLayer的初始化需要AVCaptureSession的實(shí)例session對(duì)數(shù)據(jù)的輸入輸出進(jìn)行控制;
3、那我們就初始化一個(gè)session,并將輸出流的質(zhì)量設(shè)置為高質(zhì)量AVCaptureSessionPresetHigh;
4、因?yàn)?code>session是依靠AVCaptureDeviceInput和AVCaptureMetadataOutput來控制數(shù)據(jù)輸入輸出的;
5、那就用AVCaptureDevice的實(shí)例device初始化一個(gè)input,指明device為AVMediaTypeVideo類型;
6、再初始化一個(gè)output,設(shè)置好delegate和queue以及所支持的元數(shù)據(jù)類型(二維碼和不同格式的條形碼);
7、然后將inputoutput添加到session中就OK了,調(diào)用[session startRunning];就可以掃描二維碼了;
8、最終從- captureOutput:didOutputMetadataObjects:fromConnection:方法中得到捕捉到的二維碼/條形碼數(shù)據(jù)。

至此,在previewLayer范圍內(nèi)就可以識(shí)別二維碼/條形碼了。

二、指定識(shí)別二維碼/條形碼的區(qū)域

如果要控制在previewLayer的指定區(qū)域內(nèi)識(shí)別二維碼/條形碼,可以通過修改output的rectOfInterest屬性來達(dá)到目的。代碼如下:

// 計(jì)算rect坐標(biāo)
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;

// 坐標(biāo)賦值
output.rectOfInterest = CGRectMake(rectY, rectX, rectH, rectW);

1、上述的CGRectMake(rectY, rectX, rectH, rectW)與CGRectMake(x, y, w, h)的傳統(tǒng)定義不同,可以將rectOfInterest理解成被翻轉(zhuǎn)過的CGRect;
2、而rectY, rectX, rectH, rectW也不是控件或區(qū)域的值,而是所對(duì)應(yīng)的比例,如上述代碼中的計(jì)算公式,y, x, h, w的值可參考下圖;
3、rectOfInterest的默認(rèn)值為CGRectMake(.0, .0, 1.0, 1.0),表示識(shí)別二維碼/條形碼的區(qū)域?yàn)槿粒╬reviewLayer區(qū)域)。

PS: 其實(shí)iOS提供了官方API來將標(biāo)準(zhǔn)rect轉(zhuǎn)換成rectOfInterest,但只有在[session startRunning]之后調(diào)用才有效果,而且還會(huì)時(shí)不時(shí)地出現(xiàn)卡頓式地閃一下。代碼如下:

// 可以在[session startRunning]之后用此語句設(shè)置掃碼區(qū)域
metadataOutput.rectOfInterest = [previewLayer metadataOutputRectOfInterestForRect:rectFrame];
rectOfInterest計(jì)算輔助圖
三、拉近二維碼/條形碼(放大視頻內(nèi)容)

當(dāng)二維碼/條形碼離我們較遠(yuǎn)時(shí),拉近二維碼/條形碼會(huì)是一個(gè)不錯(cuò)的功能,效果如下:

放大掃碼效果

上述效果是使用雙指縮放的方式來實(shí)現(xiàn)的,具體代碼如下:

// 添加縮放手勢(shì)
UIPinchGestureRecognizer *pinchGesture = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinch:)];
[previewView addGestureRecognizer:pinchGesture];
- (void)pinch:(UIPinchGestureRecognizer *)gesture {
    
    AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
    
    // 設(shè)定有效縮放范圍,防止超出范圍而崩潰
    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) {
        // 記錄上次縮放的比例,本次縮放在上次的基礎(chǔ)上疊加
        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
    }
}

上述代碼的核心邏輯比較簡(jiǎn)單:
1、在previewView上添加一個(gè)雙指捏合的手勢(shì) pinchGesture,并設(shè)定target和selector
2、在selector方法中根據(jù)gesture.scale調(diào)整device.videoZoomFactor;
3、注意在修改device屬性之前要lock一下,修改完后unlock一下。

四、弱光環(huán)境下開啟手電筒

弱光環(huán)境對(duì)掃碼功能有較大的影響,通過監(jiān)測(cè)光線亮度給用戶提供打開手電筒的選擇會(huì)提升不少的體驗(yàn),如下圖:

弱光監(jiān)測(cè)打開手電筒效果

弱光監(jiān)測(cè)的代碼如下:

- (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];
    
    // 初始化一些變量,作為是否透?jìng)鱞rightness的因數(shù)
    AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
    BOOL torchOn = device.torchMode == AVCaptureTorchModeOn;
    BOOL dimmed = brightness < 1.0;
    static BOOL lastDimmed = NO;
    
    // 控制透?jìng)鬟壿嫞旱谝淮伪O(jiān)測(cè)到光線或者光線明暗變化(dimmed變化)時(shí)透?jìng)?    if (_lightObserver) {
        if (!_lightObserverHasCalled) {
            _lightObserver(dimmed, torchOn);
            _lightObserverHasCalled = YES;
            lastDimmed = dimmed;
        }
        else if (dimmed != lastDimmed) {
            _lightObserver(dimmed, torchOn);
            lastDimmed = dimmed;
        }
    }
}

弱光監(jiān)測(cè)是依賴AVCaptureVideoDataOutput和AVCaptureVideoDataOutputSampleBufferDelegate來實(shí)現(xiàn)的。
1、初始化AVCaptureVideoDataOutput的實(shí)例lightOutput后,設(shè)定delegate并將lightOutput添加到session中;
2、實(shí)現(xiàn)AVCaptureVideoDataOutputSampleBufferDelegate的回調(diào)方法-captureOutput:didOutputSampleBuffer:fromConnection:;
3、對(duì)回調(diào)方法中的sampleBuffer進(jìn)行各種操作(具體參考上述代碼細(xì)節(jié)),并最終獲取到光線亮度brightness
4、根據(jù)brightness的值設(shè)定弱光的標(biāo)準(zhǔn)以及是否透?jìng)鹘o業(yè)務(wù)邏輯(這里認(rèn)為brightness < 1.0為弱光)。

調(diào)用- observeLightStatus:方法并實(shí)現(xiàn)blck即可接收透?jìng)鬟^來的光線狀態(tài)和手電筒狀態(tài),并根據(jù)狀態(tài)對(duì)UI做相應(yīng)的調(diào)整,代碼如下:

__weak typeof(self) weakSelf = self;
[self observeLightStatus:^(BOOL dimmed, BOOL torchOn) {
    if (dimmed || torchOn) {// 變?yōu)槿豕饣蛘呤蛛娡蔡幱陂_啟狀態(tài)
        [weakSelf.previewView stopScanning];// 停止掃描動(dòng)畫
        [weakSelf.previewView showTorchSwitch];// 顯示手電筒開關(guān)
    } else {// 變?yōu)榱凉獠⑶沂蛛娡蔡幱陉P(guān)閉狀態(tài)
        [weakSelf.previewView startScanning];// 開始掃描動(dòng)畫
        [weakSelf.previewView hideTorchSwitch];// 隱藏手電筒開關(guān)
    }
}];

當(dāng)出現(xiàn)手電筒開關(guān)時(shí),我們可以通過點(diǎn)擊開關(guān)改變手電筒的狀態(tài)。開關(guān)手電筒的代碼如下:

+ (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的手電筒狀態(tài)
        [device unlockForConfiguration];// 修改device屬性之后unlock
    }
}

手電筒開關(guān)(按鈕)封裝在QiCodePreviewView中,QiCodeManager中通過QiCodePreviewViewDelegate的-codeScanningView:didClickedTorchSwitch:方法獲取手電筒開關(guān)的點(diǎn)擊事件,并做相應(yīng)的邏輯處理。代碼如下:

// QiCodePreviewViewDelegate
- (void)codeScanningView:(QiCodePreviewView *)scanningView didClickedTorchSwitch:(UIButton *)switchButton {
    
    switchButton.selected = !switchButton.selected;
    
    [QiCodeManager switchTorch:switchButton.selected];
    _lightObserverHasCalled = switchButton.selected;
}

綜上,掃描二維碼/條形碼的功能就實(shí)現(xiàn)完了。此外,QiCodeManager中還封裝了生成二維碼/條形碼的方法,下篇文章介紹。


示例源碼:QiQRCode可從GitHub的QiShare開源庫中獲取。


推薦文章:
iOS 了解Xcode Bitcode
iOS 重繪之drawRect
iOS 編寫高質(zhì)量Objective-C代碼(八)
iOS KVC與KVO簡(jiǎn)介

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容