自iOS7以後,iOS掃描二維碼不需要藉助於第三方框架了,蘋果在AVFoundation中原生支援了掃描二維碼的API,主要涉及到5個類,這5個類在自定義相機或者視訊時也用得上,網上有很多介紹,這5個類分別為:
AVCaptureSession:媒體捕獲會話,負責把捕獲的音視訊資料輸出到輸出裝置中。
AVCaptureDevice:輸入裝置,如麥克風、攝像頭。
AVCaptureDeviceInput:裝置輸入資料管理物件,可以根據AVCaptureDevice建立對應的AVCaptureDeviceInput物件,該物件將會被新增到AVCaptureSession中管理。
AVCaptureOutput:輸出資料管理物件,用於接收各類輸出資料,有很多子類,每個子類用途都不一樣,該物件將會被新增到AVCaptureSession中管理。
AVCaptureVideoPreviewLayer:相機拍攝預覽圖層,是CALayer的子類,使用該物件可以實時檢視拍照或視訊錄製效果,設定好尺寸後需要新增到父view的layer中。
複製程式碼
我在參考了網上的很多部落格並自己摸索了以後,寫了一個具體的實現案例,過程中遇到很多坑,在此記錄並分享一下。
執行環境:Xcode 8.3.2 + iOS 8. 4真機、iOS 10.3.1真機
一、核心步驟:
1、建立AVCaptureSession會話 2、建立AVCaptureDevice裝置 3、建立輸入AVCaptureDeviceInput與輸出裝置AVCaptureMetadataOutput,並新增到上面的會話中 4、建立預覽層 5、設定掃描區域
二、實現
從上面的描述看,除了預覽層,其他的和UI介面似乎沒什麼關係,但是實際開發中,掃描介面一般都是設計的比較人性化的,如支付寶、微信等,中間都有一個小框,有個線上下掃,這個其實就是用UI來配合掃描二維碼,給使用者一種好的體驗。
1、介面佈局
2、主要程式碼
#import "ViewController.h"
#import <AVFoundation/AVFoundation.h>
@interface ViewController () <AVCaptureMetadataOutputObjectsDelegate, CALayerDelegate>
/**
* UI
*/
@property (weak, nonatomic) IBOutlet UIView *scanView;
@property (weak, nonatomic) IBOutlet UIImageView *scanline;
@property (weak, nonatomic) IBOutlet UILabel *result;
/**
* 掃描區域的高度約束值(寬度一致)
*/
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *scanViewH;
/**
* 掃描線的頂部約束值
*/
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *scanlineTop;
/**
* 掃描線的高度
*/
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *scanlineH;
@property(nonatomic, strong) CALayer *maskLayer;
/**
* 五個類
*/
@property(nonatomic, strong) AVCaptureDevice *device;
@property(nonatomic, strong) AVCaptureDeviceInput *input;
@property(nonatomic, strong) AVCaptureMetadataOutput *output;
@property(nonatomic, strong) AVCaptureSession *session;
@property(nonatomic, strong) AVCaptureVideoPreviewLayer *layer;
@end
@implementation ViewController
#pragma mark - 懶載入
-(AVCaptureDevice *)device{
if (_device == nil) {
_device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
}
return _device;
}
-(AVCaptureDeviceInput *)input{
if (_input == nil) {
_input = [AVCaptureDeviceInput deviceInputWithDevice:self.device error:nil];
}
return _input;
}
-(AVCaptureMetadataOutput *)output{
if (_output == nil) {
_output = [[AVCaptureMetadataOutput alloc]init];
[_output setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];
}
return _output;
}
#pragma mark - ViewController生命週期
/**
* 執行掃描動畫
*/
-(void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
[self startAnim];
}
/**
* 註冊進入前臺通知 保證下次進來還有掃描動畫
*/
-(void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
//註冊程式進入前臺通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector (startAnim) name: UIApplicationWillEnterForegroundNotification object:nil];
}
/**
* 移除通知
*/
-(void)viewWillDisappear:(BOOL)animated{
[super viewWillDisappear:animated];
//解除程式進入前臺通知
[[NSNotificationCenter defaultCenter] removeObserver:self name: UIApplicationWillEnterForegroundNotification object:nil];
}
- (void)viewDidLoad {
[super viewDidLoad];
//1、建立會話
AVCaptureSession *session = [[AVCaptureSession alloc]init];
if ([session canSetSessionPreset:AVCaptureSessionPresetHigh]) {
[session setSessionPreset:AVCaptureSessionPresetHigh];
}
//2、新增輸入和輸出裝置
if([session canAddInput:self.input]){
[session addInput:self.input];
}
if([session canAddOutput:self.output]){
[session addOutput:self.output];
}
//3、設定這次掃描的資料型別
self.output.metadataObjectTypes = self.output.availableMetadataObjectTypes;
//4、建立預覽層
AVCaptureVideoPreviewLayer *layer = [AVCaptureVideoPreviewLayer layerWithSession:session];
layer.frame = self.view.bounds;
[self.view.layer insertSublayer:layer atIndex:0];
//5、建立周圍的遮罩層
CALayer *maskLayer = [[CALayer alloc]init];
maskLayer.frame = self.view.bounds;
//此時設定的顏色就是中間掃描區域最終的顏色
maskLayer.backgroundColor = [UIColor colorWithRed:0.1 green:0.1 blue:0.1 alpha:0.2].CGColor;
maskLayer.delegate = self;
[self.view.layer insertSublayer:maskLayer above:layer];
//讓代理方法呼叫 將周圍的蒙版顏色加深
[maskLayer setNeedsDisplay];
//6、關鍵設定掃描的區域 方法一:自己計算
// CGFloat x = (self.view.bounds.size.width - self.scanViewH.constant) * 0.5;
//
// CGFloat y = (self.view.bounds.size.height- self.scanViewH.constant) * 0.5;
//
// CGFloat w = self.scanViewH.constant;
//
// CGFloat h = w;
//
//
// self.output.rectOfInterest = CGRectMake(y/self.view.bounds.size.height, x/self.view.bounds.size.width, h/self.view.bounds.size.height, w/self.view.bounds.size.width);
//6、關鍵設定掃描的區域,方法二:直接轉換,但是要在 AVCaptureInputPortFormatDescriptionDidChangeNotification 通知裡設定,否則 metadataOutputRectOfInterestForRect: 轉換方法會返回 (0, 0, 0, 0)。
__weak __typeof(&*self)weakSelf = self;
[[NSNotificationCenter defaultCenter] addObserverForName:AVCaptureInputPortFormatDescriptionDidChangeNotification object:nil queue:[NSOperationQueue currentQueue] usingBlock: ^(NSNotification *_Nonnull note) {
weakSelf.output.rectOfInterest = [weakSelf.layer metadataOutputRectOfInterestForRect:self.scanView.frame];
}];
//7、開始掃描
[session startRunning];
self.session = session;
self.layer = layer;
self.maskLayer = maskLayer;
}
-(void)dealloc{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark - 代理方法
/**
* 如果掃描到了二維碼 回撥該代理方法
*/
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection{
if(metadataObjects.count > 0 && metadataObjects != nil){
AVMetadataMachineReadableCodeObject *metadataObject = [metadataObjects lastObject];
NSString *result = metadataObject.stringValue;
self.result.text = result;
[self.session stopRunning];
[self.scanline removeFromSuperview];
}
}
/**
* 蒙版中間一塊要空出來
*/
-(void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx{
if (layer == self.maskLayer) {
UIGraphicsBeginImageContextWithOptions(self.maskLayer.frame.size, NO, 1.0);
//蒙版新顏色
CGContextSetFillColorWithColor(ctx, [UIColor colorWithRed:0.1 green:0.1 blue:0.1 alpha:0.8].CGColor);
CGContextFillRect(ctx, self.maskLayer.frame);
//轉換座標
CGRect scanFrame = [self.view convertRect:self.scanView.frame fromView:self.scanView.superview];
//空出中間一塊
CGContextClearRect(ctx, scanFrame);
}
}
@end
#pragma mark - 自定義方法
/**
* 掃描的那條線動起來
*/
-(void)startAnim{
//如果是第二次進來 那麼動畫已經執行完畢 要重新開始動畫的話 必須讓約束歸位
if(self.scanlineTop.constant == self.scanViewH.constant - 4){
self.scanlineTop.constant -= self.scanViewH.constant - 4;
[self.view layoutIfNeeded];
}
//執行動畫
[UIView animateWithDuration:3.0 delay:0 options:UIViewAnimationOptionRepeat animations:^{
self.scanlineTop.constant = self.scanViewH.constant - 4;
[self.view layoutIfNeeded];
} completion:nil];
}
複製程式碼
Info.plist中 不同iOS版本需要新增相應的許可權
三、最終效果
總結
一、遇到的坑
1、設定了AutoLayout,想要做動畫,這時候動畫放在viewDidAppear
中執行,並且不要用bounds,frame來改變動畫,要用具體的約束,但是直接在UIView動畫中修改約束是沒效果的,需要在設定完約束以後,加上[self.view layoutIfNeeded];
。
2、設定掃描區域,也就是設定AVCaptureMetadataOutput
的rectOfInterest
屬性,它是一個CGRect型別,但是它的四個值和傳統的不一樣,是**(y,x,高,寬)**且是比例值,取值範圍為0~1。那麼有兩種方案,第一種需要自己計算具體位置的比例,如程式碼中註釋的那些。第二種方案用AVCaptureVideoPreviewLayer
的metadataOutputRectOfInterestForRect
方法,但是直接設定是沒有效果的,必須放到通知裡,如文中所示。
3、中間方塊是通過CALayer兩步實現的,第一步設定整個背景顏色,這個顏色根據中間想顯示的樣式來設定;第二步在代理方法裡面重新設定一次背景顏色,這個顏色根據除中間以外的區域來設定,然後將中間的挖掉。但是必須呼叫setNeedsDisplay
方法,否則代理方法不會呼叫。
二、參考文獻
1、iOS開發系列--音訊播放、錄音、視訊播放、拍照、視訊錄製
2、iOS開發 - 二維碼的掃描
3、iOS二維碼掃描與生成(優化啟動卡頓)