前面我们介绍了二维码的生成,现在我们介绍一下怎么扫描二维码;在iOS7之前,大部分应用中使用的二维码扫描是ZXing或者ZBar第三方的扫描框架。在iOS7之后,苹果自身提供了二维码的扫描功能,从效率上来说,原生的二维码远高于这些第三方框架,现在就看看类似与微信扫一扫的界面的实现;
实现扫描二维码,首先要调起系统摄像头创建视频会话,所以需要导入AVFoundation框架;还要用到以下几个类:
@property (strong,nonatomic)AVCaptureDevice *device; // 捕捉硬件设备
@property (strong,nonatomic)AVCaptureDeviceInput *input; // 外界成像输入配置
@property (strong,nonatomic)AVCaptureMetadataOutput *output; // 成像输出配置
@property (strong,nonatomic)AVCaptureSession *session; // 捕捉设置
@property (strong,nonatomic)AVCaptureVideoPreviewLayer *prelayer; // 图层布局层
@property (nonatomic, retain) UIImageView *line; // 扫描动画线
1、AVCaptureDevice:捕捉硬件设备;
2、AVCaptureDeviceInput: 外界成像输入配置。这个类用来表示输入数据的硬件设备,配置抽象设备的port;
3、AVCaptureMetadataOutput: 成像输出配置。这个支持二维码、条形码等图像数据的识别;
4、AVCaptureSession: 捕捉设置,此类作为硬件设备输入输出信息的桥梁,承担实时获取设备数据的责任;
5、AVCaptureVideoPreviewLayer: 图层类。用来快速呈现摄像头获取的原始数据二维码扫描功能的实现步骤是创建好会话对象,用来获取从硬件设备输入的数据,并实时显示在界面上;
下面是具体实现和创建方法,在创建前先检测下摄像头有没有允许开启:
- (void)setupCamera
{
AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
if (device==nil){
UIAlertView * alert = [[UIAlertView alloc]initWithTitle:@"温馨提示" message:@"摄像头未开启" delegate:nil cancelButtonTitle:@"确定" otherButtonTitles:nil, nil];
[alert show];
return;
}
// Device (获取手机设备的硬件 —— 摄像头)
_device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
// Input (获取手机摄像头的输入流设置)
_input = [AVCaptureDeviceInput deviceInputWithDevice:_device error:nil];
// Output (获取手机摄像头的输出流设置)
_output = [[AVCaptureMetadataOutput alloc]init];
// 设置输出流协议 AVCaptureMetadataOutputObjectsDelegate
[_output setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];
// Session 硬件配置设置
_session = [[AVCaptureSession alloc]init];
[_session setSessionPreset:AVCaptureSessionPresetHigh];
// 加入硬件配置的输入输出流
if ([_session canAddInput:self.input])
{
[_session addInput:self.input];
}
if ([_session canAddOutput:self.output])
{
[_session addOutput:self.output];
}
// 条码类型 AVMetadataObjectTypeQRCode
if ([self.output.availableMetadataObjectTypes containsObject:AVMetadataObjectTypeQRCode])
{
self.output.metadataObjectTypes = @[AVMetadataObjectTypeQRCode,AVMetadataObjectTypeEAN13Code];
}
// Preview
dispatch_async(dispatch_get_main_queue(), ^{
// 更新界面
_prelayer =[AVCaptureVideoPreviewLayer layerWithSession:self.session];
_prelayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
_prelayer.frame = CGRectMake(0, 64, SCREEN_WIDTH, SCREEN_HEIGHT-64);
[self.view.layer insertSublayer:self.prelayer atIndex:0];
// Start 开启摄像头运行
[_session startRunning];
});
}
设置摄像头拍照的范围就是_prelayer.frame
这个方法,注意,这是拍照的范围,并不是扫描的范围,要想限制二维码扫描区域,让用户能够准确对准某个二维码,不让整个屏幕都是扫描区域,像微信一样设置区域在框内,需要设置一个参数rectOfInterest
,这个参数有点特别,这个参数的rect 与平常设置的坐标系是完全相反的,原点坐标在右上角,即X与Y互换、W与H互换,这个属性的每一个值取值范围在0~1之间;先设置扫描区域的宽、高以及屏幕宽、高的宏定义:
#define SCREEN_WIDTH [UIScreen mainScreen].bounds.size.width
#define SCREEN_HEIGHT [UIScreen mainScreen].bounds.size.height
#define scanWidth (SCREEN_WIDTH - 120)
#define scanHeight scanWidth
#define scanX (SCREEN_WIDTH - scanWidth)/2
#define scanY (SCREEN_HEIGHT - scanHeight)/2
给output设置扫描区域:
//设置扫描区域
CGFloat x = scanX/SCREEN_WIDTH;
CGFloat y = scanY/SCREEN_HEIGHT;
CGFloat width = scanWidth/SCREEN_WIDTH;
CGFloat height = scanHeight/SCREEN_HEIGHT;
//y 与 x 互换 width 与 height 互换
[_output setRectOfInterest:CGRectMake(y,x, height, width)];
到这,扫描已经准备完毕,现在实现扫描动画,使线循环上下运动;首先定义一个全局变量,扫描线和定时器:
@property(assign, nonatomic) BOOL isUp;
@property(strong, nonatomic) NSTimer *timer;
初始化图片框和扫描线:
-(void)initView{
_isUp = NO;
UIImageView * imageView = [[UIImageView alloc]initWithFrame:CGRectMake(scanX, scanY, scanWidth, scanHeight)];
imageView.image = [UIImage imageNamed:@"watch_sao_kuang"];
[self.view addSubview:imageView];
_line = [[UIImageView alloc] initWithFrame:CGRectMake(scanX, scanY+5, scanWidth, 2)];
_line.image = [UIImage imageNamed:@"watch_sao_line"];
[self.view addSubview:_line];
_timer = [NSTimer scheduledTimerWithTimeInterval:0.01 target:self selector:@selector(repeatTime) userInfo:nil repeats:YES];
}
给定时器设置为0.01秒,线移动到上下间隔5个像素,实现动画:
-(void)repeatTime
{
CGRect rect = _line.frame;
if (_isUp == NO) {
rect.origin.y += 1;
if (rect.origin.y >= scanY + scanHeight - 5 - rect.size.height)
{
_isUp = YES;
}
} else {
rect.origin.y -= 1;
if (rect.origin.y <= scanY + 5)
{
_isUp = NO;
}
}
_line.frame = rect;
}
运行效果如下:
看看上面的微信扫一扫界面,似乎还差一步,就是非扫描区域和扫描区域颜色有些区分,颜色暗一点,有同学说这个简单,在非扫描区域覆盖上、下、左、右4个View就行,是的,可以这样做,我第一次写项目也是这么做的,不难,就是坐标算起来稍微复杂点,在这里介绍一种更为简单的方法,使用
CAShapeLayer
动画,CAShapeLayer
是一个通过矢量图形而不是 bitmap 来绘制的图层子类。可以指定颜色、线宽等属性,用CGPath 来定义想要绘制的图形,最后 CAShapeLayer 就会自动渲染出来。主要思路是设置绘制的路径及颜色,然后进行绘画填充,代码如下:
- (void)setshapeLayer{
_shapeLayer = [[CAShapeLayer alloc] init];
CGMutablePathRef path = CGPathCreateMutable();
//添加蒙版
CGPathAddRect(path, nil, self.view.bounds);
CGPathAddRect(path, nil, CGRectMake(scanX, scanY, scanWidth, scanHeight));
//填充规则
_shapeLayer.fillRule = kCAFillRuleEvenOdd;
//绘制的路径
_shapeLayer.path = path;
//路径中的填充颜色
_shapeLayer.fillColor = [UIColor blackColor].CGColor;
//设置透明度
_shapeLayer.opacity = 0.5;
[_shapeLayer setNeedsDisplay];
[self.view.layer addSublayer:_shapeLayer];
}
有几个关键属性,就是添加蒙版CGPathAddRect
,可以看到我们添加了两次蒙版,为什么呢?因为除了中间的扫描区域之外,其他的全部要置灰,所以要添加两条路径,一条是屏幕大小,还有一条是扫描区域大小。然后将路径赋给CAShapeLayer的path属性;想清楚这个规则,这就要还得看看填充规则属性fillRule
,它有两个属性:kCAFillRuleNonZero
和kCAFillRuleEvenOdd
:
- kCAFillRuleNonZero:默认值,非零规则,当这个点作任意方法的射线,然后看射线和路径的交点方向,选择一个作为基准方向,如果方向一致则加1,方向不一致则减1。为0时,点不在路径内。
- kCAFillRuleEvenOdd:奇偶规则,当这个点作任意方法的射线,射线和路径的交点数量是奇数则认为点在内部。
我们选择的是kCAFillRuleEvenOdd
奇偶规则,意思就是添加蒙版的地方随意找个点画射线,与蒙版路径相交的点数是奇还是偶,奇数代表是内部,偶数代表是外部,内部填充颜色,外部不管。可能你还不是很理解,下面我画了一张图,更加直观:
由图可知,1、2代表大框除了小框的部分区域,就是我们的非扫描区域,看出无论从哪个方向画射线,交点数都是基数个,黄点代表焦点数,1有一个交点,2有三个交点;小框内部部分,就是我们的扫描区域,任意画出的射线都是二个交点,如线3;所以CAShapeLayer自动识别,实现了中间的扫描区域之外,其他的全部要置灰;得到的效果图如下:
所有的准备工作都做完了,现在是验收阶段了,只要我们包含了
<AVCaptureMetadataOutputObjectsDelegate>
代理就可以通过代理方法获取扫描到的东西了:
#pragma mark AVCaptureMetadataOutputObjectsDelegate
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection
{
NSString *stringValue;
// 元数据对象
if ([metadataObjects count] >0)
{
// 可读码对象
AVMetadataMachineReadableCodeObject * metadataObject = [metadataObjects objectAtIndex:0];
stringValue = metadataObject.stringValue;
// 扫描成功后调用,解析二维码
[_timer setFireDate:[NSDate distantFuture]];
[_session stopRunning];
NSLog(@"扫一扫结果:%@",stringValue);
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"扫描结果" message:stringValue preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"确认" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
if (_session != nil && _timer != nil) {
[_session startRunning];
[_timer setFireDate:[NSDate date]];
}
}]];
[self presentViewController:alert animated:YES completion:nil];
}
}
到这里扫描的页面全部完成了。有几点值得注意的地方,当扫描成功之后,已经执行了[_session stopRunning];
所以当我进入下个页面返回或者左滑又松开的时候页面会卡死,想要重新扫描,必须重新开启,还有定时器也需要重新开启,我们可以放在viewWillAppear
方法开启:
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"扫一扫";
self.view.backgroundColor = [UIColor whiteColor];
[self initView];
[self setshapeLayer];
[self setupCamera];
}
-(void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[_timer setFireDate:[NSDate date]];
[_session startRunning];
}
-(void)viewDidDisappear:(BOOL)animated{
[super viewDidDisappear:animated];
[_session stopRunning];
}
声明: 转载请注明出处http://www.jianshu.com/p/34c66cd69df9