ios 掃碼知識總結一

二維碼介紹

二維碼,3個回形大方塊,是爲了給相機定位,黑白塊,黑塊代表1,白塊代表0,8個一組,組成二進制信息。科普:二維碼原理是什麼?這個小視頻,簡單介紹二維碼原理。下圖是二維碼原理圖。

掃描二維碼

掃描二維碼,有2種方式,一種是採用google的ZXing代碼的oc版本(個人維護,已停止更新),一種是採用原生的掃碼方式,即自帶的AVFoundation.framework。我個人更喜歡採用原生的方式。

代碼解析

1.頭文件

//引入頭文件
#import <AVFoundation/AVFoundation.h>
// 作者自定義的View視圖, 繼承UIView
#import "ShadowView.h"

#define kWidth [UIScreen mainScreen].bounds.size.width
#define kHeight [UIScreen mainScreen].bounds.size.height
#define customShowSize CGSizeMake(200, 200);

2.定義屬性

// ScanCodeViewController是作者創建的VC , 用Navi推出, 寫入協議 (UIImagePickerControllerDelegate, UINavigationControllerDelegate 是爲了 可以直接掃碼圖庫中的二維碼, 在Navi右上角創建button)
@interface ScanCodeViewController ()<AVCaptureMetadataOutputObjectsDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate>

/** 輸入數據源 */
@property (nonatomic, strong) AVCaptureDeviceInput *input;
/** 輸出數據源 */
@property (nonatomic, strong) AVCaptureMetadataOutput *output;
/** 輸入輸出的中間橋樑 負責把捕獲的音視頻數據輸出到輸出設備中 */
@property (nonatomic, strong) AVCaptureSession *session;
/** 相機拍攝預覽圖層 */
@property (nonatomic, strong) AVCaptureVideoPreviewLayer *layerView;
/** 預覽圖層尺寸 */
@property (nonatomic, assign) CGSize layerViewSize;
/** 有效掃碼範圍 */
@property (nonatomic, assign) CGSize showSize;
/** 作者自定義的View視圖 */
@property (nonatomic, strong) ShadowView *shadowView;

@end

3.創建二維碼掃碼

-(void)creatScanQR{
  
/** 創建輸入數據源 */
        AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];  //獲取攝像設備
        self.input = [AVCaptureDeviceInput deviceInputWithDevice:device error:nil];  //創建輸出流

/** 創建輸出數據源 */
        self.output = [[AVCaptureMetadataOutput alloc] init];
        [self.output setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];  //設置代理 在主線程裏刷新

/** Session設置 */
        self.session = [[AVCaptureSession alloc] init];
        [self.session setSessionPreset:AVCaptureSessionPresetHigh];   //高質量採集
        [self.session addInput:self.input];
        [self.session addOutput:self.output];
        //設置掃碼支持的編碼格式
        self.output.metadataObjectTypes = @[AVMetadataObjectTypeQRCode,
                                        AVMetadataObjectTypeEAN13Code,
                                        AVMetadataObjectTypeEAN8Code,
                                        AVMetadataObjectTypeCode128Code];
/** 掃碼視圖 */
         //掃描框的位置和大小
        self.layerView = [AVCaptureVideoPreviewLayer layerWithSession:self.session];
        self.layerView.videoGravity = AVLayerVideoGravityResizeAspectFill;
        self.layerView.frame = CGRectMake(0, 64, self.view.frame.size.width, self.view.frame.size.height - 64);
        // 將掃描框大小定義爲屬行, 下面會有調用
        self.layerViewSize = CGSizeMake(_layerView.frame.size.width, _layerView.frame.size.height);

}

#pragma mark - 實現代理方法, 完成二維碼掃描
-(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection{
  
    if (metadataObjects.count > 0) {
       
         // 停止動畫, 看完全篇記得打開註釋, 不然掃描條會一直有動畫效果
        //[self.shadowView stopTimer];

        //停止掃描
        [self.session stopRunning];
        
        AVMetadataMachineReadableCodeObject * metadataObject = [metadataObjects objectAtIndex : 0 ];
        //輸出掃描字符串
        NSLog(@"%@",metadataObject.stringValue);
        UIAlertView * alert = [[UIAlertView alloc]initWithTitle:@"提示" message:[NSString stringWithFormat:@"%@", metadataObject.stringValue] delegate:nil cancelButtonTitle:@"確定" otherButtonTitles:nil, nil];
        [alert show];
    }
}

4.調用方法

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.view.backgroundColor = [UIColor whiteColor];
    
    //調用
    [self creatScanQR];
    //添加拍攝圖層
    [self.view.layer addSublayer:self.layerView];
    //開始二維碼
    [self.session startRunning];
    
    // Do any additional setup after loading the view.
}

至此,二維碼掃碼完成。運行,你會發現整個屏幕都可用掃碼,要定義掃碼範圍,請繼續往下看

#import <UIKit/UIKit.h>

@interface ShadowView : UIView

@property (nonatomic, assign) CGSize showSize;
- (void)stopTimer;

@end
#import "ShadowView.h"

@interface ShadowView ()

@property (nonatomic, strong) UIImageView *lineView;
@property (nonatomic, strong) NSTimer *timer;

@end

@implementation ShadowView

- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        
        self.backgroundColor = [UIColor clearColor];
        // 圖片下方附上
        self.lineView  = [[UIImageView alloc] init];
        self.lineView.image = [UIImage imageNamed:@"line"];
        [self addSubview:self.lineView];

    }
    return self;
}

-(void)playAnimation{

    [UIView animateWithDuration:2.4 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
        
        self.lineView .frame = CGRectMake((self.frame.size.width - self.showSize.width) / 2, (self.frame.size.height + self.showSize.height) / 2, self.showSize.width, 2);
        
    } completion:^(BOOL finished) {
        self.lineView .frame = CGRectMake((self.frame.size.width - self.showSize.width) / 2, (self.frame.size.height - self.showSize.height) / 2, self.showSize.width, 2);
    }];
}

- (void)stopTimer
{
    [_timer invalidate];
    _timer = nil;    
}

-(void)layoutSubviews{
    
    [super layoutSubviews];
    
    self.lineView .frame = CGRectMake((self.frame.size.width - self.showSize.width) / 2, (self.frame.size.height - self.showSize.height) / 2, self.showSize.width, 2);
    
    
    if (!_timer) {
        
        [self playAnimation];
        
        /* 自動播放 */
        self.timer = [NSTimer scheduledTimerWithTimeInterval:2.5 target:self selector:@selector(playAnimation) userInfo:nil repeats:YES];
        
    }
    
}

-(void)drawRect:(CGRect)rect{
    
    [super drawRect:rect];
    
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    
    // 整體顏色
    CGContextSetRGBFillColor(ctx, 0.15, 0.15, 0.15, 0.6);
    CGContextFillRect(ctx, rect);   //draw the transparent layer
        
    //中間清空矩形框
    CGRect clearDrawRect = CGRectMake((rect.size.width - self.showSize.width) / 2, (rect.size.height - self.showSize.height) / 2, self.showSize.width, self.showSize.height);
    CGContextClearRect(ctx, clearDrawRect);
   
    //邊框
    CGContextStrokeRect(ctx, clearDrawRect);
    CGContextSetRGBStrokeColor(ctx, 1, 1, 1, 1);  //顏色
    CGContextSetLineWidth(ctx, 0.5);             //線寬
    CGContextAddRect(ctx, clearDrawRect);       //矩形
    CGContextStrokePath(ctx);
    
    [self addCornerLineWithContext:ctx rect:clearDrawRect];
    
}

- (void)addCornerLineWithContext:(CGContextRef)ctx rect:(CGRect)rect{
    
    float cornerWidth = 4.0;
    
    float cornerLong = 16.0;
    
    //畫四個邊角 線寬
    CGContextSetLineWidth(ctx, cornerWidth);
    
    //顏色
    CGContextSetRGBStrokeColor(ctx, 83 /255.0, 239/255.0, 111/255.0, 1);//綠色
    
    //左上角
    CGPoint poinsTopLeftA[] = {CGPointMake(rect.origin.x + cornerWidth/2, rect.origin.y),
                               CGPointMake(rect.origin.x + cornerWidth/2, rect.origin.y + cornerLong)};
    
    CGPoint poinsTopLeftB[] = {CGPointMake(rect.origin.x, rect.origin.y + cornerWidth/2),
                               CGPointMake(rect.origin.x + cornerLong, rect.origin.y + cornerWidth/2)};
    
    [self addLine:poinsTopLeftA pointB:poinsTopLeftB ctx:ctx];
    
    
    //左下角
    CGPoint poinsBottomLeftA[] = {CGPointMake(rect.origin.x + cornerWidth/2, rect.origin.y + rect.size.height - cornerLong),
                                  CGPointMake(rect.origin.x + cornerWidth/2, rect.origin.y + rect.size.height)};
    
    CGPoint poinsBottomLeftB[] = {CGPointMake(rect.origin.x, rect.origin.y + rect.size.height - cornerWidth/2),
                                  CGPointMake(rect.origin.x + cornerLong, rect.origin.y + rect.size.height - cornerWidth/2)};
    
    [self addLine:poinsBottomLeftA pointB:poinsBottomLeftB ctx:ctx];
    
    
    //右上角
    CGPoint poinsTopRightA[] = {CGPointMake(rect.origin.x+ rect.size.width - cornerLong, rect.origin.y + cornerWidth/2),
                                CGPointMake(rect.origin.x + rect.size.width, rect.origin.y + cornerWidth/2 )};
    
    CGPoint poinsTopRightB[] = {CGPointMake(rect.origin.x+ rect.size.width - cornerWidth/2, rect.origin.y),
                                CGPointMake(rect.origin.x + rect.size.width- cornerWidth/2, rect.origin.y + cornerLong)};
    
    [self addLine:poinsTopRightA pointB:poinsTopRightB ctx:ctx];
    
    //右下角
    CGPoint poinsBottomRightA[] = {CGPointMake(rect.origin.x+ rect.size.width - cornerWidth/2, rect.origin.y+rect.size.height - cornerLong),
                                   CGPointMake(rect.origin.x- cornerWidth/2 + rect.size.width, rect.origin.y +rect.size.height )};
    
    CGPoint poinsBottomRightB[] = {CGPointMake(rect.origin.x+ rect.size.width - cornerLong, rect.origin.y + rect.size.height - cornerWidth/2),
                                   CGPointMake(rect.origin.x + rect.size.width, rect.origin.y + rect.size.height - cornerWidth/2 )};
    
    [self addLine:poinsBottomRightA pointB:poinsBottomRightB ctx:ctx];
    
    
    CGContextStrokePath(ctx);
}

- (void)addLine:(CGPoint[])pointA pointB:(CGPoint[])pointB ctx:(CGContextRef)ctx {
    
    CGContextAddLines(ctx, pointA, 2);
    CGContextAddLines(ctx, pointB, 2);
}


@end

接下來是掃描線動畫, 用NSTimer, 偶爾手機處理卡頓下, 就會被坑. 作者看到有用 CABasicAnimation 寫的, 覺得挺好. 所以 提供下, 該方法

-(void)addAnimationAboutScan{
    
    self.lineView.hidden = NO;
    CABasicAnimation *animation = [ShadowView moveYTime:2.5 fromY:[NSNumber numberWithFloat:0] toY:[NSNumber numberWithFloat:(self.showSize.height-1)] rep:OPEN_MAX];
    [self.lineView.layer addAnimation:animation forKey:@"LineAnimation"];       
}

+ (CABasicAnimation *)moveYTime:(float)time fromY:(NSNumber *)fromY toY:(NSNumber *)toY rep:(int)rep{

    CABasicAnimation *animationMove = [CABasicAnimation animationWithKeyPath:@"transform.translation.y"];
    [animationMove setFromValue:fromY];
    [animationMove setToValue:toY];
    animationMove.duration = time;
    animationMove.delegate = self;
    animationMove.repeatCount  = rep;
    animationMove.fillMode = kCAFillModeForwards;
    animationMove.removedOnCompletion = NO;
    animationMove.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    return animationMove;
}

- (void)removeAnimationAboutScan{

    [self.lineView.layer removeAnimationForKey:@"LineAnimation"];
    self.lineView.hidden = YES;
}

 

配置掃碼範圍

附官方介紹
rectOfInterest Property
A rectangle of interest for limiting the search area for visual metadata.
Discussion
The value of this property is a CGRect value that determines the object’s rectangle of interest for each frame of video.
The rectangle's origin is top left and is relative to the coordinate space of the device providing the metadata.
Specifying a rectangle of interest may improve detection performance for certain types of metadata. Metadata objects whose bounds do not intersect with the rectOfInterest will not be returned.
The default value of this property is a rectangle of (0.0, 0.0, 1.0, 1.0).

說明 :
看到矩形的原點是左上角, 但是真正測試 你會發現卻是在右上角, 因爲掃碼默認是 橫屏, 所以原右上角變成左上角, 原寬變成高, 原高變成寬. 取值是按照 攝像頭分辨率 來取的比例 而不是屏幕的寬高比例.

作者設置 AVCaptureSessionPresetHigh 所以機型分辨率均爲 1920×1080. 所以除了iPhone4 基本上 屏幕寬高比 符合 分辨率的比例. 會有些許誤差, 但影響不大. 如需支持包含iPhone4的所以機型 需要將 屏幕寬高與分辨率統一. 方法如下. 這樣便將 ShadowView 中間清空的矩形框有效掃碼範圍 對應上了.

/** 配置掃碼範圍 */
-(void)allowScanRect{

   
    /** 掃描是默認是橫屏, 原點在[右上角]
     *  rectOfInterest = CGRectMake(0, 0, 1, 1);
     *  AVCaptureSessionPresetHigh = 1920×1080   攝像頭分辨率
     *  需要轉換座標 將屏幕與 分辨率統一
     */
    
    //剪切出需要的大小位置
    CGRect shearRect = CGRectMake((self.layerViewSize.width - self.showSize.width) / 2,
                                  (self.layerViewSize.height - self.showSize.height) / 2,
                                  self.showSize.height,
                                  self.showSize.height);
    
    
    CGFloat deviceProportion = 1920.0 / 1080.0;
    CGFloat screenProportion = self.layerViewSize.height / self.layerViewSize.width;
    
    //分辨率比> 屏幕比 ( 相當於屏幕的高不夠)
    if (deviceProportion > screenProportion) {
        //換算出 分辨率比 對應的 屏幕高
        CGFloat finalHeight = self.layerViewSize.width * deviceProportion;
        // 得到 偏差值
        CGFloat addNum = (finalHeight - self.layerViewSize.height) / 2;

                                              // (對應的實際位置 + 偏差值)  /  換算後的屏幕高
        self.output.rectOfInterest = CGRectMake((shearRect.origin.y + addNum) / finalHeight,
                                                 shearRect.origin.x / self.layerViewSize.width,
                                                 shearRect.size.height/ finalHeight,
                                                 shearRect.size.width/ self.layerViewSize.width);
        
    }else{
        
        CGFloat finalWidth = self.layerViewSize.height / deviceProportion;
        
        CGFloat addNum = (finalWidth - self.layerViewSize.width) / 2;
        
        self.output.rectOfInterest = CGRectMake(shearRect.origin.y / self.layerViewSize.height,
                                                (shearRect.origin.x + addNum) / finalWidth,
                                                shearRect.size.height / self.layerViewSize.height,
                                                shearRect.size.width / finalWidth);
    }

}

讀取相冊中的二維碼

#pragma mark - 相冊中讀取二維碼
/* navi按鈕實現 */
-(void)takeQRCodeFromPic:(UIBarButtonItem *)leftBar{
    
    
    
    if ([[[UIDevice currentDevice] systemVersion] doubleValue] < 8) {
        
        UIAlertView * alert = [[UIAlertView alloc]initWithTitle:@"提示" message:@"請更新系統至8.0以上!" delegate:nil cancelButtonTitle:@"確定" otherButtonTitles:nil, nil];
        [alert show];
        
    }else{
        
        if([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypePhotoLibrary]){
            
            UIImagePickerController *pickerC = [[UIImagePickerController alloc] init];
            pickerC.delegate = self;
            
            pickerC.sourceType = UIImagePickerControllerSourceTypeSavedPhotosAlbum;  //來自相冊
            
            [self presentViewController:pickerC animated:YES completion:NULL];
            
        }else{
            
            UIAlertView * alert = [[UIAlertView alloc]initWithTitle:@"提示" message:@"設備不支持訪問相冊,請在設置->隱私->照片中進行設置!" delegate:nil cancelButtonTitle:@"確定" otherButtonTitles:nil, nil];
            [alert show];
        }
        
    }
    
    
}

- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
    //1.獲取選擇的圖片
    UIImage *image = info[UIImagePickerControllerEditedImage];
    
    if (!image) {
        image = info[UIImagePickerControllerOriginalImage];
    }
    //2.初始化一個監測器
    CIDetector*detector = [CIDetector detectorOfType:CIDetectorTypeQRCode context:nil options:@{ CIDetectorAccuracy : CIDetectorAccuracyHigh }];
    
    [picker dismissViewControllerAnimated:YES completion:^{
        
        //監測到的結果數組  放置識別完之後的數據
        NSArray *features = [detector featuresInImage:[CIImage imageWithCGImage:image.CGImage]];
        //判斷是否有數據(即是否是二維碼)
        if (features.count >=1) {
            /**結果對象 */
            CIQRCodeFeature *feature = [features objectAtIndex:0];
            NSString *scannedResult = feature.messageString;
            
            UIAlertView * alertView = [[UIAlertView alloc]initWithTitle:@"提示" message:scannedResult delegate:nil cancelButtonTitle:@"確定" otherButtonTitles:nil, nil];
            [alertView show];
            
        }
        else{
            UIAlertView * alertView = [[UIAlertView alloc]initWithTitle:@"提示" message:@"該圖片沒有包含二維碼!" delegate:nil cancelButtonTitle:@"確定" otherButtonTitles:nil, nil];
            [alertView show];
            
        }
    }];
}

以上代碼,重寫調用

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.view.backgroundColor = [UIColor whiteColor];
    
    //顯示範圍
     self.showSize = customShowSize;
    //調用
    [self creatScanQR];
    //添加拍攝圖層
    [self.view.layer addSublayer:self.layerView];
    //開始二維碼
    [self.session startRunning];
    //設置可用掃碼範圍
    [self allowScanRect];

   //添加上層陰影視圖
    self.shadowView = [[ShadowView alloc] initWithFrame:CGRectMake(0, 64, kWidth, kHeight - 64)];
    [self.view addSubview:self.shadowView];
    self.shadowView.showSize = self.showSize;
    
    
    //添加掃碼相冊按鈕
     self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"相冊中選" style:UIBarButtonItemStylePlain target:self action:@selector(takeQRCodeFromPic:)];
    
    // Do any additional setup after loading the view.
}

權限問題

權限問題 使用相機需要獲取相應權限, 如用戶未開啓, 可以設置提醒, 自行設置.
 

可用工具

補充一下第三方的掃碼庫:https://github.com/kingsic/SGQRCode

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章