自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来配合扫描二维码,给用户一种好的体验。
界面布局
主要代码
#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二维码扫描与生成(优化启动卡顿)