最近复习,之前项目需求要实现一个二维码扫描的课程签到的功能。这里简单总结一下。
根据文件的名字,相信也能猜出这俩文件实现的功能。
首先说一下二维码扫描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试试。