iOS 8+ 原生API实现二维码扫描ViewController

最近在做的一个项目里面包含了扫码逻辑,因为之前公司的一些项目用的是旧的第三方框架,准确性和速度都与原生存在一定的差距,界面可变性局限大,维护成本高,不能忍😤,所以自己用原生API重写了一个扫码模块,Github具体代码可点击此处。官方的扫码API是在iOS8上推出的,如今系统迭代到了iOS10,iOS8以下系统的市场占有率已经可以忽略,并且APP适配现在也是从iOS8开始。

最终的实现效果:
2016121757403final.jpg
2016121728331final4.jpg
动画:
2016121772985scanDemo.gif

实现方法

这里使用ViewController进行实现,之前考虑过用View,因为生命周期的那里坑比较多,为了方便管理生命周期所以采用了ViewController进行实现。

工程设置

使用JBScanViewController,项目工程info.plist需要添加相机、相册权限(适配iOS 10)和将系统StatusBar的优先级调低,否则在相册选择图片进行裁切时会有20PX的下沉。
201612183772屏幕快照 2016-12-18 00.15.51.png

原生API的调用

头文件需要用到AVFoundation框架和MobileCoreServices,相机的操作依赖AVFoundation,MobileCoreServices则是在约束相册筛选类型时使用。

ViewController需要遵循AVCaptureMetadataOutputObjectsDelegateUINavigationControllerDelegateUIImagePickerControllerDelegate三个代理,AVCaptureMetadataOutputObjectsDelegate是接受相机输入流,其它两个则是调用系统相册。

首先需要声明中间Session
AVCaptureSession * session; //输入输出的中间桥梁
进行初始化摄像设备和扫码行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//获取摄像设备
AVCaptureDevice * device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
//创建输入流
AVCaptureDeviceInput * input = [AVCaptureDeviceInput deviceInputWithDevice:device error:nil];
//创建输出流
AVCaptureMetadataOutput * output = [[AVCaptureMetadataOutput alloc]init];
//设置代理 在主线程里刷新
[output setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];
//设置扫码范围(Y,X,HEIGHT,WIDTH)
output.rectOfInterest=CGRectMake(
((1-(VIEW_SIZE_WIDTH/VIEW_SIZE_HEIGHT*SCAN_RECT_RATIO))/2)+(SCAN_OFFSET/VIEW_SIZE_HEIGHT),
(1-SCAN_RECT_RATIO)/2,
VIEW_SIZE_WIDTH/VIEW_SIZE_HEIGHT*SCAN_RECT_RATIO,
SCAN_RECT_RATIO
);
//初始化链接对象
session = [[AVCaptureSession alloc]init];
//高质量采集率
[session setSessionPreset:AVCaptureSessionPresetHigh];
[session addInput:input];
[session addOutput:output];
//设置扫码支持的编码格式
output.metadataObjectTypes=@[AVMetadataObjectTypeQRCode,AVMetadataObjectTypeEAN13Code, AVMetadataObjectTypeEAN8Code, AVMetadataObjectTypeCode128Code];
AVCaptureVideoPreviewLayer * layer = [AVCaptureVideoPreviewLayer layerWithSession:session];
layer.videoGravity=AVLayerVideoGravityResizeAspectFill;
layer.frame=self.view.layer.bounds;
//隐藏引导界面
[acView stopAnimating];
label.hidden = YES;
//插入相机Layer
[self.view.layer insertSublayer:layer atIndex:0];
//开始捕获
[session startRunning];

扫码范围这里要计算好,特别需要注意的是此处坐标与正常CGRect不同,此处是(Y,X,HEIGHT,WIDTH)并且全部按比例(0-1)设定,设定了扫码范围后系统只会判断该范围内的数据,这样能显著仅提高效率和成功率,JBScanViewController设定了一些宏参数以灵活改变范围大小和偏移量可根据需求进行修改

1
2
3
4
5
#define VIEW_SIZE_HEIGHT  self.view.bounds.size.height  //当前View高度
#define VIEW_SIZE_WIDTH self.view.bounds.size.width //当前View宽度
#define SCAN_RECT_RATIO 0.7 //扫码区域与当前View宽的比例
#define SCAN_OFFSET -40 //扫描框偏移量(大于0往下,小于0往上)
#define SCAN_FRAME_ANIMATION_DURATION 0.2 //扫描框生成动画时长

执行完startRunning之后就开始进行扫描了,但是这里会涉及到生命周期的一些问题,如果将这一段代码在ViewDidLoad方法里面进行初始化则会有0.5-1秒的延时,因为加载相机会消耗大量的时间,所以界面会等到相机加载完之后才Push,为了不产生卡顿感,这里做法是将初始化方法放到ViewDidAppear方法中进行,先将页面Push,再初始化相机,这样会非常流畅,但是先push过来回有黑屏的界面,因为相机还在启动过程中,所以加上一层UI引导用户进行等待。

1
2
3
4
5
6
7
8
9
10
11
12
//启动提示和loading菊花
acView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
label = [UILabel new];
[acView startAnimating];
[acView setHidesWhenStopped:YES];
label.text = @"相机启动中...";
label.textColor = [UIColor whiteColor];
[label setTextAlignment:NSTextAlignmentCenter];
[self.view addSubview:acView];
[self.view addSubview:label];
acView.frame = CGRectMake((VIEW_SIZE_WIDTH-200)/2,(VIEW_SIZE_HEIGHT-200)/2-50, 200, 200);
label.frame = CGRectMake((VIEW_SIZE_WIDTH-200)/2,(VIEW_SIZE_HEIGHT-30)/2, 200, 30);

得到扫描结果之后会调用以下两个回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#pragma mark - AVCaptureMetadataOutputObjectsDelegate
-(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection{
if (metadataObjects.count>0) {
[session stopRunning];
AVMetadataMachineReadableCodeObject * metadataObject = [metadataObjects objectAtIndex : 0 ];
[self scanResultString:metadataObject.stringValue];
[self scanResult:metadataObject];
//输出扫描字符串
NSLog(@"%@",metadataObject.stringValue);
}
}

#pragma mark - UIImagePickerControllerDelegate
-(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info{
//获取选中的照片
UIImage *image = info[UIImagePickerControllerEditedImage];
if (!image) {image = info[UIImagePickerControllerOriginalImage];}
//初始化 将类型设置为二维码
CIDetector *detector = [CIDetector detectorOfType:CIDetectorTypeQRCode context:nil options:nil];
[picker dismissViewControllerAnimated:YES completion:^{
//设置数组,放置识别完之后的数据
NSArray *features = [detector featuresInImage:[CIImage imageWithData:UIImagePNGRepresentation(image)]];
//判断是否有数据(即是否是二维码)
if (features.count >= 1) {
//取第一个元素就是二维码所存放的文本信息
CIQRCodeFeature *feature = features[0];
NSString *scannedResult = feature.messageString;
//通过对话框的形式呈现(临时)
[self scanResultString:scannedResult];
[self scanResult:feature];
}else{
[self alertControllerMessage:@"未检测到二维码"];
}
}];
}

这里得到的结果会返回到我封装的两个方法中,每得到一次结果会同时调用这两个方法,可在里面进行数据操作

1
2
3
4
5
6
7
8
9
10
11
#pragma mark - ScanResult
/** 扫描获取字符串触发 */
- (void)scanResultString:(NSString*)result{
//提示扫描结果(演示)
[self alertControllerMessage:result];
}

/** 扫描获取对象触发 */
- (void)scanResult:(id)result{
NSLog(@">>>%@",result);
}

UI界面与动画

扫描框蒙版UI和动画

因为考虑到可集成性,JBScanViewController使用纯代码进行UI构建,主要使用贝塞尔曲线在Layer层进行动画。首先需要在相机层上方进行一层蒙版处理
20161218603042016-12-18 at 01.png

1
2
3
4
5
6
7
8
9
//扫描框
UIView *maskView = [[UIView alloc] initWithFrame:self.view.frame];
maskView.backgroundColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.55];
[self.view addSubview:maskView];
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, VIEW_SIZE_WIDTH, VIEW_SIZE_HEIGHT)];
[maskPath appendPath:[[UIBezierPath bezierPathWithRoundedRect:CGRectMake(VIEW_SIZE_WIDTH/2,VIEW_SIZE_HEIGHT/2+SCAN_OFFSET,0,0) cornerRadius:1] bezierPathByReversingPath]];
self.maskLayer = [[CAShapeLayer alloc] init];
self.maskLayer.path = maskPath.CGPath;
maskView.layer.mask = self.maskLayer;

将蒙版通过贝塞尔矩形路径镂空,通过控制蒙版Layer的Mask属性加上CABasicAnimation的Path动画使蒙版出现生成动画
2016121848035屏幕快照 2016-12-18 01.28.15.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/** 加载扫描框生成动画 */
- (void)loadScanAnimation{
UIBezierPath *maskFinalPath = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, VIEW_SIZE_WIDTH, VIEW_SIZE_HEIGHT)];
[maskFinalPath appendPath:[[UIBezierPath bezierPathWithRoundedRect:CGRectMake(
(VIEW_SIZE_WIDTH-VIEW_SIZE_WIDTH*SCAN_RECT_RATIO)/2,
(VIEW_SIZE_HEIGHT-VIEW_SIZE_WIDTH*SCAN_RECT_RATIO)/2+SCAN_OFFSET,
VIEW_SIZE_WIDTH*SCAN_RECT_RATIO,
VIEW_SIZE_WIDTH*SCAN_RECT_RATIO
) cornerRadius:1] bezierPathByReversingPath]];
CABasicAnimation *scanAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
scanAnimation.toValue = (id)maskFinalPath.CGPath;
scanAnimation.duration = SCAN_FRAME_ANIMATION_DURATION;
scanAnimation.fillMode = kCAFillModeForwards;
scanAnimation.removedOnCompletion = NO;
[self.maskLayer addAnimation:scanAnimation forKey:nil];
}

扫描框四角UI和动画

这时蒙版生成的动画已经完成,我们需要的还有边框与扫描线的UI和动画,将扫描框拆分为四个角进行绘制,每个角的绘制方向按顺时针绘制,并且X=Y=边框边长乘以边长与X的比例,所以当我们更改比例至0.5时就能实现全边框包裹。
20161218710472016-12-18 at 01.43.png
扫描框初始值(原点值)

1
2
3
4
5
6
7
8
9
//扫描框
UIView *maskView = [[UIView alloc] initWithFrame:self.view.frame];
maskView.backgroundColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.55];
[self.view addSubview:maskView];
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, VIEW_SIZE_WIDTH, VIEW_SIZE_HEIGHT)];
[maskPath appendPath:[[UIBezierPath bezierPathWithRoundedRect:CGRectMake(VIEW_SIZE_WIDTH/2,VIEW_SIZE_HEIGHT/2+SCAN_OFFSET,0,0) cornerRadius:1] bezierPathByReversingPath]];
self.maskLayer = [[CAShapeLayer alloc] init];
self.maskLayer.path = maskPath.CGPath;
maskView.layer.mask = self.maskLayer;

扫描框目标值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
生成扫描框四个拐角

@param color 拐角颜色
@param lineWidth 拐角线宽
@param ratio 线宽与当前扫描框宽度的比例
@return 四角Layer
*/
- (CAShapeLayer*)getFourCornerLayerColor:(UIColor*)color lineWidth:(float)lineWidth lineLenghRatio:(float)ratio{
//四角
CAShapeLayer *scanBoxLayer = [[CAShapeLayer alloc] init];
UIBezierPath *fourCorner = [UIBezierPath bezierPath];

/** 四个角的轨迹点全部按该角顺时针方向生成 */
//左上角
UIBezierPath *leftUpCorner= [UIBezierPath bezierPath];
[leftUpCorner moveToPoint:CGPointMake(VIEW_SIZE_WIDTH*(1-SCAN_RECT_RATIO)/2, (VIEW_SIZE_HEIGHT-VIEW_SIZE_WIDTH*SCAN_RECT_RATIO)/2+SCAN_OFFSET+VIEW_SIZE_WIDTH*SCAN_RECT_RATIO*ratio)];
[leftUpCorner addLineToPoint:CGPointMake(VIEW_SIZE_WIDTH*(1-SCAN_RECT_RATIO)/2, (VIEW_SIZE_HEIGHT-VIEW_SIZE_WIDTH*SCAN_RECT_RATIO)/2+SCAN_OFFSET)];
[leftUpCorner addLineToPoint:CGPointMake(VIEW_SIZE_WIDTH*((1-SCAN_RECT_RATIO)/2+SCAN_RECT_RATIO*ratio), (VIEW_SIZE_HEIGHT-VIEW_SIZE_WIDTH*SCAN_RECT_RATIO)/2+SCAN_OFFSET)];
//左下角
UIBezierPath *leftDownCorner= [UIBezierPath bezierPath];
[leftDownCorner moveToPoint:CGPointMake(VIEW_SIZE_WIDTH*(1-SCAN_RECT_RATIO)/2+VIEW_SIZE_WIDTH*SCAN_RECT_RATIO*ratio, (VIEW_SIZE_HEIGHT-VIEW_SIZE_WIDTH*SCAN_RECT_RATIO)/2+VIEW_SIZE_WIDTH*SCAN_RECT_RATIO+SCAN_OFFSET)];
[leftDownCorner addLineToPoint:CGPointMake(VIEW_SIZE_WIDTH*(1-SCAN_RECT_RATIO)/2, (VIEW_SIZE_HEIGHT-VIEW_SIZE_WIDTH*SCAN_RECT_RATIO)/2+VIEW_SIZE_WIDTH*SCAN_RECT_RATIO+SCAN_OFFSET)];
[leftDownCorner addLineToPoint:CGPointMake(VIEW_SIZE_WIDTH*(1-SCAN_RECT_RATIO)/2, VIEW_SIZE_HEIGHT/2-VIEW_SIZE_WIDTH*SCAN_RECT_RATIO*(ratio-0.5)+SCAN_OFFSET)];
//右上角
UIBezierPath *rightUpCorner= [UIBezierPath bezierPath];
[rightUpCorner moveToPoint:CGPointMake(VIEW_SIZE_WIDTH*(1-(1-SCAN_RECT_RATIO)/2)-VIEW_SIZE_WIDTH*SCAN_RECT_RATIO*ratio, (VIEW_SIZE_HEIGHT-VIEW_SIZE_WIDTH*SCAN_RECT_RATIO)/2+SCAN_OFFSET)];
[rightUpCorner addLineToPoint:CGPointMake(VIEW_SIZE_WIDTH*(1-(1-SCAN_RECT_RATIO)/2), (VIEW_SIZE_HEIGHT-VIEW_SIZE_WIDTH*SCAN_RECT_RATIO)/2+SCAN_OFFSET)];
[rightUpCorner addLineToPoint:CGPointMake(VIEW_SIZE_WIDTH*(1-(1-SCAN_RECT_RATIO)/2), VIEW_SIZE_HEIGHT/2-VIEW_SIZE_WIDTH*SCAN_RECT_RATIO*(0.5-ratio)+SCAN_OFFSET)];
//右下角
UIBezierPath *rightDownCorner= [UIBezierPath bezierPath];
[rightDownCorner moveToPoint:CGPointMake(VIEW_SIZE_WIDTH*(1-(1-SCAN_RECT_RATIO)/2), (VIEW_SIZE_HEIGHT-VIEW_SIZE_WIDTH*SCAN_RECT_RATIO)/2+SCAN_OFFSET-VIEW_SIZE_WIDTH*SCAN_RECT_RATIO*ratio+VIEW_SIZE_WIDTH*SCAN_RECT_RATIO)];
[rightDownCorner addLineToPoint:CGPointMake(VIEW_SIZE_WIDTH*(1-(1-SCAN_RECT_RATIO)/2), (VIEW_SIZE_HEIGHT-VIEW_SIZE_WIDTH*SCAN_RECT_RATIO)/2+VIEW_SIZE_WIDTH*SCAN_RECT_RATIO+SCAN_OFFSET)];
[rightDownCorner addLineToPoint:CGPointMake(VIEW_SIZE_WIDTH*(1-(1-SCAN_RECT_RATIO)/2)-VIEW_SIZE_WIDTH*SCAN_RECT_RATIO*ratio, (VIEW_SIZE_HEIGHT-VIEW_SIZE_WIDTH*SCAN_RECT_RATIO)/2+VIEW_SIZE_WIDTH*SCAN_RECT_RATIO+SCAN_OFFSET)];
//添加所有路径
[fourCorner appendPath:leftUpCorner];
[fourCorner appendPath:leftDownCorner];
[fourCorner appendPath:rightUpCorner];
[fourCorner appendPath:rightDownCorner];
//设置线宽和颜色
scanBoxLayer.lineWidth = lineWidth;
scanBoxLayer.strokeColor = color.CGColor;
scanBoxLayer.fillColor = nil;
//添加动画
UIBezierPath *initPath = [UIBezierPath bezierPathWithRect:CGRectMake(VIEW_SIZE_WIDTH/2, VIEW_SIZE_HEIGHT/2+SCAN_OFFSET, 0, 0)];
scanBoxLayer.path = initPath.CGPath;
CABasicAnimation *cornerAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
cornerAnimation.toValue = (id)fourCorner.CGPath;
cornerAnimation.duration = SCAN_FRAME_ANIMATION_DURATION;
cornerAnimation.fillMode = kCAFillModeForwards;
cornerAnimation.removedOnCompletion = NO;
[scanBoxLayer addAnimation:cornerAnimation forKey:nil];

return scanBoxLayer;
}

扫描线UI和动画

最后加上扫描线的UI和动画就能完成了,扫描线需要用到一个矩形Layer层加上渐变色,然后将其Mask属性添加椭圆Path遮罩,最后生成扫描线边缘半透明的效果,因为渐变层Frame属性不能添加动画,所以将动画添加到Position属性,通过改变起始点和终止点来完成从无到有的动画。扫描动画同理改变其Position的高度值,但是要特别注意removedOnCompletion属性,否则按home键退出时会停止扫描动画。
20161218835342016-12-18 at 02.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
生成扫描线

@param color 扫描线颜色
@param height 扫描线厚度
@param duration 扫描线动画时间
@return 扫描线Layer
*/
- (CAGradientLayer*)getScanLine:(UIColor*)color height:(float)height duration:(float)duration{
//扫描线生成和遮罩
CAShapeLayer *scanlineMask = [[CAShapeLayer alloc] init];
UIBezierPath *scanLineMaskPath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, VIEW_SIZE_WIDTH*SCAN_RECT_RATIO*0.95, height)];
scanlineMask.path = scanLineMaskPath.CGPath;
scanlineMask.strokeColor = color.CGColor;
scanlineMask.fillColor = color.CGColor;
CAGradientLayer *scanLineLayer = [CAGradientLayer layer];
scanLineLayer.frame = CGRectMake(0, 0, VIEW_SIZE_WIDTH*SCAN_RECT_RATIO*0.95, height);
scanLineLayer.position = CGPointMake(VIEW_SIZE_WIDTH/2, VIEW_SIZE_HEIGHT/2+SCAN_OFFSET);
scanLineLayer.colors = @[(__bridge id)[color colorWithAlphaComponent:0.05].CGColor,
(__bridge id)[color colorWithAlphaComponent:0.8].CGColor,
(__bridge id)[color colorWithAlphaComponent:0.05].CGColor];
scanLineLayer.locations = @[@(0.05), @(0.5), @(0.95)];
scanLineLayer.startPoint = CGPointMake(0.5, 0.5);
scanLineLayer.endPoint = CGPointMake(0.5, 0.5);
scanLineLayer.mask = scanlineMask;
//扫描线生成动画
CABasicAnimation *scanLinePositionAnimation = [CABasicAnimation animationWithKeyPath:@"position"];
scanLinePositionAnimation.toValue = [NSValue valueWithCGPoint:CGPointMake(VIEW_SIZE_WIDTH/2, (VIEW_SIZE_HEIGHT-VIEW_SIZE_WIDTH*SCAN_RECT_RATIO)/2+SCAN_OFFSET+height)];
CABasicAnimation *scanLineStartPointAnimation = [CABasicAnimation animationWithKeyPath:@"startPoint"];
scanLineStartPointAnimation.toValue = [NSValue valueWithCGPoint:CGPointMake(0, 0)];
CABasicAnimation *scanLineEndPointAnimation = [CABasicAnimation animationWithKeyPath:@"endPoint"];
scanLineEndPointAnimation.toValue = [NSValue valueWithCGPoint:CGPointMake(1, 0)];
CAAnimationGroup *initialAnimations = [CAAnimationGroup animation];
initialAnimations.animations = @[scanLinePositionAnimation,scanLineStartPointAnimation,scanLineEndPointAnimation];
initialAnimations.duration=SCAN_FRAME_ANIMATION_DURATION;
initialAnimations.removedOnCompletion = NO;
initialAnimations.fillMode = kCAFillModeForwards;
[scanLineLayer addAnimation:initialAnimations forKey:nil];
//扫描动画
CABasicAnimation *scanAnimation = [CABasicAnimation animationWithKeyPath:@"position"];
scanAnimation.toValue = [NSValue valueWithCGPoint:CGPointMake(VIEW_SIZE_WIDTH/2, (VIEW_SIZE_HEIGHT-VIEW_SIZE_WIDTH*SCAN_RECT_RATIO)/2+SCAN_OFFSET-height+VIEW_SIZE_WIDTH*SCAN_RECT_RATIO)];
scanAnimation.duration = duration;
scanAnimation.repeatCount = HUGE_VALF;
scanAnimation.removedOnCompletion = NO;
[scanLineLayer addAnimation:scanAnimation forKey:nil];
return scanLineLayer;
}

其它方法

提供了相册读取方法和闪光灯开启/关闭方法,可自行调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#pragma mark - CommonMethod
/** 相册读取图片方法 */
- (void)scanImage{
UIImagePickerController *imagrPicker = [[UIImagePickerController alloc]init];
imagrPicker.delegate = self;
imagrPicker.allowsEditing = YES;
imagrPicker.mediaTypes = [[NSArray alloc] initWithObjects:(NSString*)kUTTypeImage,nil];
//将来源设置为相册
imagrPicker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
[self presentViewController:imagrPicker animated:YES completion:nil];
}

/** 闪光灯开关方法 */
- (void)onOffFlashLight:(BOOL)on{
AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
if ([device hasTorch]) {

[device lockForConfiguration:nil];
if (on) {
[device setTorchMode:AVCaptureTorchModeOn];
}else{
[device setTorchMode:AVCaptureTorchModeOff];
}
[device unlockForConfiguration];
}
}

JBScanViewController大部分代码实现的功能是UI和动画,对整体的时长和美观度进行了考量,同时兼顾性能和界面流畅度,也抽离出常用属性以适应不同的项目需求。具体使用可下载Demo进行了解。