#一、iOS实现原生扫码的意义
二维码扫码功能对于现在的iOS App开发来说是非常重要的。
通常为了节省开发时间,很多开发者会采用ZXing和ZBar等第三方SDK进行开发。
这样的好处是快速便捷,但是缺点也是在于如果要自定义一部分功能,可能对源码进行第二次开发来说比较辛苦。
如果采用的其他开发者提供的进一步封装的扫码功能,又有可能因为编码风格或者缺少注释变得晦涩难懂。
所以掌握原生二维码扫码是非常重要的,优点也是非常明显:
1、扫码识别效率高
2、使用灵活,自定义UI方便
3、便于维护
#二、核心功能介绍
##1、扫码功能:
原生扫码的功能主要采用的是AVFoundation类库下的AVCaptureMetadataOutput,这个输出类可以实现对二维码,条形码等等二维码的的直接识别。
需要注意的是,如果想用开发Mac版本的二维码扫码功能, AVCaptureMetadataOutput是不能实现的,这个类只支持iOS的扫码功能。
如果想要实现Mac上的二维码扫码功能可以参考我的另一篇文章:《基于MacOSX平台下的二维码扫码功能》
链接地址:http://blog.csdn.net/super_dl_csdn/article/details/76460745
具体的其他注意事项和细节,将会在后续的代码部分进行解释。
##2、蒙版功能:
对于二维码扫码来说,实现一个黑色的半透明蒙版,并且限定扫描区域是十分有必要的。
这对于引导用户正确的扫码,提高用户体验度和扫码效率是非常有用的。
但是这个功能并非核心功能,而且需要配合后面的限定扫描区域进一步使用,如果并非必要,可以不作为主要的功能点。
##3、限定扫描区域和动画功能:
如果不限行扫码的区域,对于启动扫码后的视频采集将会以全部屏幕的形势展现出来,这是不合理的。
限定一个扫描区域也有利于用户有目的将二维码放置于合适的位置,也有利于提高识别的效率。
添加扫码动画和适当的进行UI修饰,可以提高用户体验度,同时可是实时告知用户扫码功能是否启用。
#三、项目核心代码实现
整个代码分为两个大类:视图控制器部分代码和扫码类功能代码。具体的源码将会在后续给出链接,项目的结构如下:
- ViewController:利用StoryBoard生成的根视图控制器,用于跳转扫码界面。
- QRScanForIOS:二维码扫码类包,用于二维码扫码功能的实现,蒙版图层和扫描动画的封装。
- QRManager:二维码扫码功能类,利用AVFoundation实现原生扫码功能
- QMaskView:创建半透明的黑色蒙版视图
- QRScanAnimationView:二维码扫码限制区域的界面设置和动画封装
- QRScanForIOSHeader:类包头文件,包含该头文件即可使用相关功能
- QRViewController:二维码扫码视图控制器,用于展现扫码功能界面
##1、扫码功能实现
1)扫码头文件功能的实现:
//返回类型的block
typedef void(^finishBlock)(BOOL finish,NSError * error);
/**
初始化扫码Manange
@param delegate 代理
@param block 返回Block
*/
- (void)initQrManagerWithDelegateL:(id)delegate
finishInitBlock:(finishBlock)block;
/**
设置扫码区域的相关参数
@param supView 父视图
@param viewFrame 扫码区域的大小
*/
- (void)setPreviewLayerWithSupview:(UIView *)supView
withViewFrame:(CGRect)viewFrame;
//开始扫描
- (void)startScan;
//停止扫描
- (void)stopScan;
- 扫码功能实例方法的实现:
@interface QRManager ()
@property (nonatomic,strong) AVCaptureSession *session;
@property (nonatomic,strong) AVCaptureMetadataOutput *metadataOutput;
@end
/**
初始化扫码Manange
@param delegate 代理
@param block 返回Block
*/
- (void)initQrManagerWithDelegateL:(id)delegate
finishInitBlock:(finishBlock)block
{
//创建上下文启动器
_session = [[AVCaptureSession alloc] init];
//设置Device
AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
NSError *error;
AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice:device error:&error];
if (deviceInput) {
//理论上这里应该添加一个判断看看能不能添加input
[_session addInput:deviceInput];
//创建一个输出类型
self.metadataOutput = [[AVCaptureMetadataOutput alloc] init];
//设置代理,扫描结果将会利用代理返回
[self.metadataOutput setMetadataObjectsDelegate:delegate queue:dispatch_get_main_queue()];
// 这行代码要在设置 metadataObjectTypes 前
[_session addOutput:self.metadataOutput];
// 设置了识别类型为QRCode,枚举还有其他乐行可以进一步添加
self.metadataOutput.metadataObjectTypes = @[AVMetadataObjectTypeQRCode];
//设置成功了返回YES
block(YES,nil);
}
else
{
NSLog(@"%@", error);
//失败,打印Error
block(NO,error);
}
}
/**
设置扫码区域的相关参数
@param supView 父视图
@param viewFrame 扫码区域的大小
*/
- (void)setPreviewLayerWithSupview:(UIView *)supView
withViewFrame:(CGRect)viewFrame
{
//设置扫码区域
AVCaptureVideoPreviewLayer *previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:_session];
previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
//默认的是全部的界面,这样整个背景都是摄像头信息
previewLayer.frame = CGRectMake(0, 84, supView.frame.size.width, supView.frame.size.height-84);
[supView.layer insertSublayer:previewLayer atIndex:0];
//重点1:采集到的摄像头很多情况是顺时针旋转90度的,造成这个的原因可能是AVFoundation本身的原因,需要进行下面步骤的修正:
//1、获取到AVCaptureVideoPreviewLayer的方向并进行修正
AVCaptureConnection *previewLayerConnection=previewLayer.connection;
//2、判断并修正
if ([previewLayerConnection isVideoOrientationSupported])
{
[previewLayerConnection setVideoOrientation:[[UIApplication sharedApplication] statusBarOrientation]];
}
//重点2:全屏幕扫码不是我们需要想要的功能,我们需要限制扫码的范围就要使用rectOfInterest。
//然而这个属性的frame是一个比例系数,范围是[0,1];很多人采用试的方式是不合理的,利用metadataOutputRectOfInterestForRect方法可以成功的将AVCaptureVideoPreviewLayer所在的父视图中的Frame转换成对应的rectOfInterest的frame。
//1、将传进来的需要限制区域的viewFrame转换成AVCaptureVideoPreviewLayer需要的Frame
CGRect intertRect = [previewLayer metadataOutputRectOfInterestForRect:viewFrame];
//2、设置限定区域
self.metadataOutput.rectOfInterest = intertRect;
}
- (void)startScan
{
[self.session startRunning];
}
- (void)stopScan
{
[self.session stopRunning];
}
##2、蒙版的制作
蒙版的制作本项目采用的是创建一个继承自UIView的子类,然后利用贝塞尔曲线实现,头文件的内容如下:
**
根据蒙版的大小,视图扫码区域的大小创建蒙版
@param maskFrame 蒙版在父视图中的大小
@param scanFrame 扫码区域的大小
@return 返回蒙版View
*/
- (instancetype)initMaskViewWithFrame:(CGRect)maskFrame
withScanFrame:(CGRect)scanFrame;
view的.m文件中实现如下:
/**
根据蒙版的大小,视图扫码区域的大小创建蒙版
@param maskFrame 蒙版在父视图中的大小
@param scanFrame 扫码区域的大小
@return 返回蒙版View
*/
- (instancetype)initMaskViewWithFrame:(CGRect)maskFrame
withScanFrame:(CGRect)scanFrame
{
self = [super initWithFrame:maskFrame];
if (self) {
self.backgroundColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.5];
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height)];
[maskPath appendPath:[[UIBezierPath bezierPathWithRoundedRect:scanFrame cornerRadius:1] bezierPathByReversingPath]];
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
maskLayer.path = maskPath.CGPath;
self.layer.mask = maskLayer;
}
return self;
}
##3、限制框的设置和动画制作
这里仅仅是举了一个例子,采用一个定时器去执行动画,当然有更好的方法。
头文件的部分代码如下:
/**
开始动画
*/
- (void)startAnimation;
/**
结束动画
*/
- (void)stopAnimation;
动画类的.m文件实现如下:
@interface QRScanAnimationView ()
{
UIImageView * line_imageView_ ;
NSTimer * animation_timer_;
int addOrCut_;
}
@end
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
//使用CGContextRef重写drawRect方法会产生一个默认的黑色的北京,需要在初始化方法中提前设置为clearcolor
[self setBackgroundColor:[UIColor clearColor]];
//线移动的imageView
line_imageView_=[[UIImageView alloc]initWithImage:[UIImage imageNamed:@"qrcode_Scan_weixin_Line@2x.png"]];
[self addSubview:line_imageView_];
//初始位置为当前视图距离顶部的四分之一处
[line_imageView_ setFrame:CGRectMake(0, self.bounds.size.height/4, self.bounds.size.width, 20)];
}
return self;
}
// 覆盖drawRect方法,你可以在此自定义绘画和动画
- (void)drawRect:(CGRect)rect
{
//An opaque type that represents a Quartz 2D drawing environment.
//一个不透明类型的Quartz 2D绘画环境,相当于一个画布,你可以在上面任意绘画
CGFloat weight_ = self.frame.size.width; //视图宽度
CGFloat height_ = self.frame.size.height; //视图高度
CGFloat view_height_ = 10;
CGFloat view_weight_ = 10; //纵向线段宽度
CGFloat view_long_ = weight_/10; //线段长度
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetRGBFillColor (context, 0, 1, 0, 1.0);//设置填充颜色
CGContextSetRGBStrokeColor(context,0, 1, 0, 1.0);//画笔线的颜色
CGContextSetLineWidth(context, view_height_);
//上,左,顶
CGPoint aPoints[2];//坐标点
aPoints[0] =CGPointMake(0, view_height_/2);//坐标1
aPoints[1] =CGPointMake(view_long_, view_height_/2);//坐标2
CGContextAddLines(context, aPoints, 2);//添加线
CGContextDrawPath(context, kCGPathStroke); //根据坐标绘制路径
//上,左,左
aPoints[0] =CGPointMake(view_weight_/2, 0);//坐标1
aPoints[1] =CGPointMake(view_weight_/2 , view_long_);//坐标2
CGContextAddLines(context, aPoints, 2);//添加线
CGContextDrawPath(context, kCGPathStroke); //根据坐标绘制路径
//上,右,顶
aPoints[0] =CGPointMake(weight_-view_long_,view_height_/2);//坐标1
aPoints[1] =CGPointMake(weight_, view_height_/2);//坐标2
CGContextAddLines(context, aPoints, 2);//添加线
CGContextDrawPath(context, kCGPathStroke); //根据坐标绘制路径
//上,右,右
aPoints[0] =CGPointMake(weight_-view_weight_/2, 0);//坐标1
aPoints[1] =CGPointMake(weight_-view_weight_/2 , view_long_);//坐标2
CGContextAddLines(context, aPoints, 2);//添加线
CGContextDrawPath(context, kCGPathStroke); //根据坐标绘制路径
//下,左,左
aPoints[0] =CGPointMake(view_weight_/2, height_-view_long_);//坐标1
aPoints[1] =CGPointMake(view_weight_/2 , height_);//坐标2
CGContextAddLines(context, aPoints, 2);//添加线
CGContextDrawPath(context, kCGPathStroke); //根据坐标绘制路径
//下,左,底
aPoints[0] =CGPointMake(0, height_-view_height_/2);//坐标1
aPoints[1] =CGPointMake(view_long_ , height_-view_height_/2);//坐标2
CGContextAddLines(context, aPoints, 2);//添加线
CGContextDrawPath(context, kCGPathStroke); //根据坐标绘制路径
//下,右,右
aPoints[0] =CGPointMake(weight_-view_weight_/2, height_-view_long_);//坐标1
aPoints[1] =CGPointMake(weight_-view_weight_/2 , height_);//坐标2
CGContextAddLines(context, aPoints, 2);//添加线
CGContextDrawPath(context, kCGPathStroke); //根据坐标绘制路径
//下,右,底
aPoints[0] =CGPointMake(weight_-view_long_, height_-view_height_/2);//坐标1
aPoints[1] =CGPointMake(weight_ , height_-view_height_/2);//坐标2
CGContextAddLines(context, aPoints, 2);//添加线
CGContextDrawPath(context, kCGPathStroke); //根据坐标绘制路径
}
/**
开始动画
*/
- (void)startAnimation
{
if (animation_timer_) {
[animation_timer_ invalidate];
}
//创建一个定时器,这种创建方式需要手动将timer放到runloop中
animation_timer_=[NSTimer timerWithTimeInterval:0.01 repeats:YES block:^(NSTimer * _Nonnull timer) {
if (line_imageView_.frame.origin.y>=self.frame.size.height*3/4) {
addOrCut_=-1;
}
else if (line_imageView_.frame.origin.y<=self.frame.size.height/4)
{
addOrCut_=1;
}
[line_imageView_ setFrame:CGRectMake(line_imageView_.frame.origin.x, line_imageView_.frame.origin.y+addOrCut_, line_imageView_.frame.size.width, line_imageView_.frame.size.height)];
}];
[[NSRunLoop mainRunLoop]addTimer:animation_timer_ forMode:NSDefaultRunLoopMode];
}
- (void)stopAnimation
{
[animation_timer_ invalidate];
}
##4、QRViewController中的设置和调用
在需要调用扫码功能的界面中,可以进行一下设置就可以实现扫码功能。
//这里采用了Masonry自动布局
#import "Masonry.h"
@interface QRViewController ()
{
CGRect scan_frame_;
QRManager * qr_scan_manager_; //扫码控制中心
QRMaskView * qr_mask_view_; //顶部蒙版视图
QRScanAnimationView * qr_scan_animation_view_; //扫码动画视图
}
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor=[UIColor whiteColor];
//首先创建限制区域的大小和frame,如果需要修改限制区域的位置,只需要修改此处的frame即可
qr_scan_animation_view_=[[QRScanAnimationView alloc]initWithFrame: CGRectMake(self.view.center.x-self.view.frame.size.height/4, self.view.center.y-self.view.frame.size.height/4, self.view.frame.size.height/2, self.view.frame.size.height/2)];
[self.view addSubview:qr_scan_animation_view_];
//这个是用来设置扫描区域的frame的,这个frame需要注意的是,必须是AVCaptureVideoPreviewLayer所在的layer上的frame,在当前软件中,因为顶部有一个topview,所以应该要在原有的self.view.center.y的基础上上移20+64个像素
scan_frame_ = CGRectMake(qr_scan_animation_view_.frame.origin.x,
qr_scan_animation_view_.frame.origin.y-84,
qr_scan_animation_view_.bounds.size.height,
qr_scan_animation_view_.bounds.size.height) ;
//创建蒙版的View
qr_mask_view_=[[QRMaskView alloc]initMaskViewWithFrame:CGRectMake(0, 84, self.view.frame.size.width, self.view.frame.size.height - 84) withScanFrame:scan_frame_];
[self.view addSubview:qr_mask_view_];
//初始化二维码扫码功能模块单利
qr_scan_manager_ = [QRManager sharedManager];
//设置相关参数
[qr_scan_manager_ initQrManagerWithDelegateL:self finishInitBlock:^(BOOL finish, NSError *error) {
if (finish) {
//开启成功,开始设置扫码参数并开始进行扫码
[qr_scan_manager_ setPreviewLayerWithSupview:self.view withViewFrame:scan_frame_];
}
else
{
NSString * qr_error_string_ =[NSString stringWithFormat:@"错误信息:%@",error];
UIAlertController * alertController=[UIAlertController alertControllerWithTitle:@"扫描启动失败!" message:qr_error_string_ preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleCancel handler:nil];
[alertController addAction:cancelAction];
[self presentViewController:alertController animated:YES completion:nil];
}
}];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
//界面进入后需要开始进行扫码
[qr_scan_manager_ startScan];
//开启动画
[qr_scan_animation_view_ startAnimation];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear: animated];
//界面退出前停止扫码
[qr_scan_manager_ stopScan];
//停止动画
[qr_scan_animation_view_ stopAnimation];
}
pragma mark - AVCaptureMetadataOutputObjectsDelegate
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection
{
// id 类型不能点语法,所以要先去取出数组中对象
AVMetadataMachineReadableCodeObject *object = [metadataObjects lastObject];
if (object == nil) return;
if ([object.type isEqualToString:AVMetadataObjectTypeQRCode] )
{
NSLog(@"得到的qr字符串为:%@",object.stringValue);
//进一步的操作
// .....
}
}
#四、总结
以上是基本的程序设计思路,下面附上代码的链接。
代码可以直接下载下来使用。
https://e.coding.net/SupDongLei/QRScanForIOS.git
PS:感觉自己和二维码比较有缘2333333(手动滑稽)