最近复习,之前项目需求要实现一个二维码扫描的课程签到的功能。这里简单总结一下。
根据文件的名字,相信也能猜出这俩文件实现的功能。
首先说一下二维码扫描View的实现
#import <UIKit/UIKit.h> @interface CaptureRectView : UIView - (void)setupView; @end这个接口里的方法被第一个文件调用的。
- (void)setupView { [UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ [self setNeedsDisplay]; } completion:^(BOOL finished) { }]; }该方法的具体实现。就是设置动画一些参数。在.m文件中还有私有方法,主要实现绘图和条形码的动画效果。
//初始化设置view - (id)initWithFrame:(CGRect)frame{} //设置条形码的动画效果 - (void)lineScanning { CGRect animationRect = self.scanningImageView.frame;//条状 animationRect.origin.y += CGRectGetWidth(self.bounds) - CGRectGetMinX(animationRect) * 2 - CGRectGetHeight(animationRect); [UIView beginAnimations:nil context:nil]; [UIView setAnimationDelay:0];//延迟 [UIView setAnimationDuration:1.2]; [UIView setAnimationCurve:UIViewAnimationCurveLinear];//路径 速度线性 [UIView setAnimationRepeatCount:HUGE_VALF];//无限 [UIView setAnimationRepeatAutoreverses:NO];//自动反向执行 self.scanningImageView.hidden = NO; self.scanningImageView.frame = animationRect;//最终位置 加一段高度 [UIView commitAnimations]; } //对二维码的区域边界以及四个角画图 - (void)drawRect:(CGRect)rect { CGContextRef context = UIGraphicsGetCurrentContext(); CGContextSetFillColorWithColor(context, self.backgroundColor.CGColor); CGContextFillRect(context, rect); CGRect clearRect; CGFloat paddingX; CGContextClearRect(context, clearRect); CGContextSaveGState(context); UIImage *topLeftImage = [UIImage imageNamed:@"capture_1"]; UIImage *topRightImage = [UIImage imageNamed:@"capture_2"]; UIImage *bottomLeftImage = [UIImage imageNamed:@"capture_3"]; UIImage *bottomRightImage = [UIImage imageNamed:@"capture_4"]; CGFloat padding = 0.5; CGFloat originPadding = 0; CGContextMoveToPoint(context, CGRectGetMinX(clearRect) + originPadding, CGRectGetMinY(clearRect) + originPadding); CGContextAddLineToPoint(context, CGRectGetMaxX(clearRect) - originPadding, CGRectGetMinY(clearRect) + originPadding); CGContextAddLineToPoint(context, CGRectGetMaxX(clearRect) - originPadding, CGRectGetMaxY(clearRect) - originPadding); CGContextAddLineToPoint(context, CGRectGetMinX(clearRect) + originPadding, CGRectGetMaxY(clearRect) - originPadding); CGContextAddLineToPoint(context, CGRectGetMinX(clearRect) + originPadding, CGRectGetMinY(clearRect) + originPadding); CGContextSetLineWidth(context, padding); CGContextSetStrokeColorWithColor(context, [UIColor whiteColor].CGColor); CGContextStrokePath(context); [topLeftImage drawInRect:CGRectMake(clearRect.origin.x - 2.0, clearRect.origin.y - 2.0, topLeftImage.size.width, topLeftImage.size.height)]; } //lazy load条形码的设置以及文本的设置 -(UIImageView *)scanningImageView {} -(UILabel*)QRCodeTipLabel{}这里主要就是对UIView动画的学习以及绘图用的CGContex,是由Core Graphics来进行绘图
Core Graphics
既然说到绘图,最近也在复习,那就谈谈这个iOS绘图的方法。在iOS开发,用于绘图的主要有Core Graphics和UIkit。
由上图不难看出,Core Graphics比UIkit要靠底层,UIkit是依赖Core Graphics的,UIkit继承UIResponse能够提供跟用户交互的功能,而Core Graphics是基础,只有在试图的基础上才能进行其他的功能。
Core Graphics和UIKit在实际使用中也存在以下这些差异:
1)Core Graphics其实是一套基于C的API框架,使用了Quartz作为绘图引擎。这也就意味着Core Graphics不是面向对象的。 2)Core Graphics需要一个图形上下文(Context)。所谓的图形上下文(Context),说白了就是一张画布。这一点非常容易理解,Core Graphics提供了一系列绘图API,自然需要指定在哪里画图。因此很多API都需要一个上下文(Context)参数。 3)Core Graphics的图形上下文(Context)是堆栈式的。只能在栈顶的上下文(画布)上画图。
Core Graphics的基本使用
为了使用CoreGraphics来绘图,最简单的方法就是自定义一个类继承自UIView,并重写子类的drawRect方法。在这个方法中绘制图形。 Core Graphics必须一个画布,才能把东西画在这个画布上。在drawRect方法方法中,我们可以直接获取当前栈顶的上下文(Context)。
正如上面我在项目中写的一样。
CGContextRef context = UIGraphicsGetCurrentContext();创建画布,然后就是可以根据项目的需求画各种各样的图形了,例如三角形啊,正方形啊等等,我项目中画的就是个矩形,我们扫描二维码时用的那个显示区域。
CGContextMoveToPoint(context, CGRectGetMinX(clearRect) + originPadding, CGRectGetMinY(clearRect) + originPadding);这个就是画边界的线条的,我们还可以用自定义的图片,来增加美观。例如:
UIImage *topLeftImage = [UIImage imageNamed:@"capture_1"]; [topLeftImage drawInRect:CGRectMake(clearRect.origin.x - 2.0, clearRect.origin.y - 2.0, topLeftImage.size.width, topLeftImage.size.height)];在左上角加上我们自定义的图片。
Core Graphics绘图的步骤:
获取上下文(画布) 创建路径(自定义或者调用系统的API)并添加到上下文中。 进行绘图内容的设置(画笔颜色、粗细、填充区域颜色、阴影、连接点形状等) 开始绘图(CGContextDrawPath) 释放路径(CGPathRelease)
Core Graphics还有增加阴影,添加模糊效果,等很强大的功能。
UIKit
UIKit 像UIImage、NSString(绘制文本)、UIBezierPath(绘制形状)、UIColor都知道如何绘制自己。这些类提供了功能有限但使用方便的方法来让我们完成绘图任务。一般情况下,UIKit就是我们所需要的。
使用UIKit,你只能在当前上下文中绘图,所以如果你当前处于UIGraphicsBeginImageContextWithOptions函数或drawRect:方法中,你就可以直接使用UIKit提供的方法进行绘图。如果你持有一个context:参数,那么使用UIKit提供的方法之前,必须将该上下文参数转化为当前上下文。幸运的是,调用UIGraphicsPushContext 函数可以方便的将context:参数转化为当前上下文,记住最后别忘了调用UIGraphicsPopContext函数恢复上下文环境。
对UIView设置圆角
- (void)drawRect:(CGRect)rect { CGContextRef ctx = UIGraphicsGetCurrentContext(); CGContextSetStrokeColorWithColor(ctx, [UIColor blackColor].CGColor); CGContextSetLineWidth(ctx, 3); UIBezierPath *path; path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(100, 100, 100, 100) byRoundingCorners:(UIRectCornerTopLeft |UIRectCornerTopRight) cornerRadii:CGSizeMake(10, 10)]; [path stroke]; }CoreAnimation
在扫描二维码的时候,必须要让条形码移动,不然给用户体验超级不好,所以要用到该框架的一些方法,来对条形码的参数进行设置达到我们想要的效果。
Core Animation是直接作用在CALayer上的(并非UIView上)非常强大的跨Mac OS X和iOS平台的动画处理API,Core Animation的动画执行过程都是在后台操作的,不会阻塞主线程。
这个图很好的帮我们理解CoreAnimation底层是怎样实现的。
1)首先得有CALayer(因为CoreAnimation是作用在CALayer上的)
2)初始化一个CAAnimation对象,并设置一些动画相关属性
3)通过调用CALayer的addAnimation:forKey:方法,增加CAAnimation对象到CALayer中,这样就能开始执行动画了
4)通过调用CALayer的removeAnimationForKey:方法可以停止CALayer中的动画
使用核心动画,你只需要设置一些参数比如起点和终点,剩下的帧核心动画为你自动完成。
核心动画类有以下分类:
提供显示内容的图层类。 动画和计时类。 布局和约束类。 事务类,在原子更新的时候组合图层类。
1)图层类:
核心动画的核心基础,它提供了一套抽象的概念(假如你使用过NSView或者UIView的话,你一定会对它很熟悉)。CALayer是整个图层类的基础,它是所有核心动画图层类的父类。
虽然核心动画的图层和 Cocoa的视图在很大程度上没有一定的相似性,但是他们两者最大的区别是,图层不会直接渲染到屏幕上。
在模型-视图-控制器(model-view-controller)概念里面NSView和UIView是典型的视图部分,但是在核心动画里面图层是模型部分。图层封装了几何、时间、可视化属性,同时它提供了图层现实的内容,但是实际显示的过程则不是由它来完成。三个重要的子类
(1)CAScrollLayer:它是CALayer的一个子类,用来显示layer的某一部分,一个CAScrollLayer对象的滚动区域是由其子层的布局来定义的。CAScrollLayer没有提供键盘或者鼠标事件,也没有提供明显的滚动条。
(2)CATextLayer:它是一个很方便就可以从string和attributed string创建layer的content的类。
(3)CATiledLayer:它允许在增量阶段显示大和复杂的图像。
2)动画类:
CAPropertyAnimation :是一个抽象的子类,它支持动画的显示图层的关键路 径中指定的属性一般不直接使用,而是使用它的子类,CABasicAnimation,CAKeyframeAnimation. 在它的子类里修改属性来运行动画。
CABasicAnimation: 简单的为图层的属性提供修改。 很多图层的属性修改默认会执行这个动画类。比如大小,透明度,颜色等属性 支持关键帧动画,你可以指定的图层属性的关键路径动画,包括动画的每个阶段的价值,以及关键帧时间和计时功能的一系列值。在 动画运行是,每个值被特定的插入值替代。核心动画 和 Cocoa Animation 同时使用这些动画类。
3)布局管理器类:
Application Kit 的视图类相对于 superlayer 提供了经典的“struts and springs”定位 模型。图层类兼容这个模型,同时 Mac OS X 上面的核心动画提供了一套更加灵活 的布局管理机制,它允许开发者自己修改布局管理器。核心动画的 CAConstraint 类 是一个布局管理器,它可以指定子图层类限制于你指定的约束集合。每个约束 (CAConstraint 类的实例封装)描述层的几何属性(左,右,顶部或底部的边缘或水 平或垂直中心)的关系,关系到其同级之一的几何属性层或 superlayer。通用的布局管理器和约束性布局管理器将会在“布局核心动画的图层”部分讨论。
4) 事务管理类 :
图层的动画属性的每一个修改必然是事务的一个部分。CATransaction 是核心动画里面负责协调多个动画原子更新显示操作。事务支持嵌套使用。
[UIView beginAnimations:nil context:nil]; [UIView setAnimationDelay:0];//延迟 [UIView setAnimationDuration:1.2]; [UIView setAnimationCurve:UIViewAnimationCurveLinear];//路径 速度线性 [UIView setAnimationRepeatCount:HUGE_VALF];//无限 [UIView setAnimationRepeatAutoreverses:NO];//自动反向执行 self.scanningImageView.hidden = NO;具体代码可参考我开始项目中的具体例子。
AVFoundation
AVCaptureSession 管理输入(AVCaptureInput)和输出(AVCaptureOutput)流,包含开启和停止会话方法。
AVCaptureDeviceInput 是AVCaptureInput的子类,可以作为输入捕获会话,用AVCaptureDevice实例初始化。
AVCaptureDevice 代表了物理捕获设备如:摄像机。用于配置等底层硬件设置相机的自动对焦模式。
AVCaptureMetadataOutput 是AVCaptureOutput的子类,处理输出捕获会话。捕获的对象传递给一个委托实现AVCaptureMetadataOutputObjectsDelegate协议。协议方法在指定的派发队列(dispatch queue)上执行。
AVCaptureVideoPreviewLayerCALayer的一个子类,显示捕获到的相机输出流。
1) 需要导入:AVFoundation Framework 包含头文件:
#import <AVFoundation/AVFoundation.h>设置 AVCaptureSession 和 AVCaptureVideoPreviewLayer 成员
2)创建会话,读取输入流
//创建会话 AVCaptureSession *asession = [[AVCaptureSession alloc] init]; if ([asession canSetSessionPreset:AVCaptureSessionPresetHigh]) { [asession setSessionPreset:AVCaptureSessionPresetHigh];//采集质量 } else { [asession setSessionPreset:AVCaptureSessionPresetLow];//采集质量 } //获取AVCaptureDevice实例 _captureDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; NSError *error = nil; if ([_captureDevice lockForConfiguration:&error]) { if ([_captureDevice isFocusModeSupported:AVCaptureFocusModeAutoFocus]) { [_captureDevice setFocusMode:AVCaptureFocusModeAutoFocus]; } [_captureDevice unlockForConfiguration]; } //初始化输入流 AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:_captureDevice error:&error]; if (error) { return; } if (!input) { return; } //初始化输出流 AVCaptureMetadataOutput *output = [[AVCaptureMetadataOutput alloc] init]; // 使用主线程队列,相应比较同步,使用其他队列,相应不同步,容易让用户产生不好的体验 [output setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()]; //添加输入流,添加输出流 if ([asession canAddInput:input]) { [asession addInput:input]; } if ([asession canAddOutput:output]) { [asession addOutput:output]; } //设置元数据 [output setMetadataObjectTypes:[NSArray arrayWithObjects:AVMetadataObjectTypeQRCode, nil]]; //创建输出对象 AVCaptureVideoPreviewLayer *preview = [AVCaptureVideoPreviewLayer layerWithSession:asession]; [preview setVideoGravity:AVLayerVideoGravityResizeAspectFill]; [preview setFrame:self.view.bounds]; [self.view.layer insertSublayer:preview atIndex:0]; self.previewLayer = preview; //开始会话 dispatch_async(dispatch_get_global_queue(0, 0),^{ [asession startRunning]; }); self.session = asession;3)停止读取
[self.view addSubview:self.nextStepView]; self.captureRectView = nil; [self.previewLayer removeFromSuperlayer]; //异步关闭,不影响主线程 dispatch_async(dispatch_get_global_queue(0, 0),^{ [self.session stopRunning]; self.session = nil; _captureDevice = nil;4)获取捕获数据处理结果 输出代理方法
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection { // 3. 设置界面显示扫描结果 if (metadataObjects.count > 0) { AVMetadataMachineReadableCodeObject *obj = metadataObjects[0]; if ([[obj type] isEqualToString:AVMetaObjectTypeQrCode]) { if ([obj.stringValue hasPrefix:@"http"] || [obj.stringValue rangeOfString:@"."].location != NSNotFound){ //配置url向服务器发送签到请求 } else { elf.nextStepLabel.text = [NSString stringWithFormat:@"二维码识别结果:\r\n%@",obj.stringValue]; } else } //不是二维码,请重新扫描 }当然要真正完成项目的功能还有很多细节,比如怎么阻止频繁扫描,怎么判断是否已经签到成功。因为项目原因,大家感兴趣可以自己写个小Demo试试。