iOS原生二维码扫码实现(含蒙版和扫码动画)

#一、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;

  1. 扫码功能实例方法的实现:

@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(手动滑稽)

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值