Swift 二维码识别
ykgrwerfgt
8年前
<p>二维码识别是很常见的app功能,为了更方便的在每一个使用二维码功能地方都能更快的实现,把二维码功能写入到了一个自定义的 View 里面,使用的时候和普通的 UIView 是一样的。效果如图(因为是模拟器运行的,所以摄像头看不到,用真机的时候就正常了):</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/2db25e39ea701b620c972aabb5dcffd8.jpg"></p> <p>二维码效果图</p> <p>这篇文章只是为了快速实现效果,更细的知识点,比如自定义控件中更详细的内容不累述。</p> <p>二维码识别分为三部实现:</p> <ul> <li>自定义 UIView ,实现方形的扫描区域</li> <li>实现摄像头捕捉</li> <li>扫描的横线动画</li> </ul> <h2><strong>自定义UIView</strong></h2> <p>首先新建一个类继承自 UIView</p> <pre> <code class="language-swift">class QRScannerView: UIView</code></pre> <p>接着实现两个重要的方法:</p> <pre> <code class="language-swift">required init(coder aDecoder: NSCoder)// 这个方法实现的目的是,我们在storyboard文件中使用这个View的时候,会直接显示出来效果。 override func drawRect(rect: CGRect)// 这个实现的目的是绘制我们要显示的内容</code></pre> <p>这里简单说一下这个 init(coder aDecoder: NSCoder) ,这个构造函数不是必须的,但是为了达到跟原生控件一样的效果:在布局的时候可以直接在布局文件中看到效果,实现这个构造函数就很重要了。第二个方法是实现二维码区域表现出来的视图样式的主要地方,这里可以绘制各</p> <p>种图形和样式。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/b4aff64879f5ed99c0e1115ba0a7c728.jpg"></p> <p>在布局中直接展示效果</p> <p>两个方法实现的代码如下:</p> <pre> <code class="language-swift">required init(coder aDecoder: NSCoder) { super.init(coder: aDecoder) self.initView() } override func drawRect(rect: CGRect) { let centerRect = getScannerRect(rect) //获取画图上下文 let context:CGContextRef = UIGraphicsGetCurrentContext(); CGContextSetAllowsAntialiasing(context, true) // 填充整个控件区域 CGContextSetFillColorWithColor(context, mBackgroundColor.CGColor) CGContextFillRect(context, rect) //移动坐标 let x = rect.size.width/2 let y = rect.size.height/2 var center = CGPointMake(x,y) // 中间扣空 CGContextClearRect(context, centerRect) // 绘制正方形框 CGContextSetStrokeColorWithColor(context, UIColor.whiteColor().CGColor) CGContextSetLineWidth(context, mLineSize) CGContextAddRect(context, centerRect) CGContextDrawPath(context, kCGPathStroke) // 绘制4个角 let cornerWidth = centerRect.width/mCornerLineRatio; let cornerHeight = centerRect.height/mCornerLineRatio; let cornerWidth = CGFloat(10) let cornerHeight = CGFloat(10) CGContextSetLineWidth(context, mCornerLineSize) CGContextSetStrokeColorWithColor(context, UIColor.greenColor().CGColor) // 绘制左上角 CGContextMoveToPoint(context, centerRect.origin.x, centerRect.origin.y + cornerHeight) CGContextAddLineToPoint(context, centerRect.origin.x, centerRect.origin.y) CGContextAddLineToPoint(context, centerRect.origin.x + cornerWidth, centerRect.origin.y) // 绘制右上角 CGContextMoveToPoint(context, centerRect.origin.x + centerRect.size.width - cornerWidth, centerRect.origin.y) CGContextAddLineToPoint(context, centerRect.origin.x + centerRect.size.width, centerRect.origin.y) CGContextAddLineToPoint(context, centerRect.origin.x + centerRect.size.width, centerRect.origin.y + cornerHeight) // 绘制右下角 CGContextMoveToPoint(context, centerRect.origin.x + centerRect.size.width, centerRect.origin.y + centerRect.size.height - cornerHeight) CGContextAddLineToPoint(context, centerRect.origin.x + centerRect.size.width, centerRect.origin.y + centerRect.size.height) CGContextAddLineToPoint(context, centerRect.origin.x + centerRect.size.width - cornerWidth, centerRect.origin.y + centerRect.size.height) // 绘制左下角 CGContextMoveToPoint(context, centerRect.origin.x, centerRect.origin.y + centerRect.size.height - cornerHeight) CGContextAddLineToPoint(context, centerRect.origin.x, centerRect.origin.y + centerRect.size.height) CGContextAddLineToPoint(context, centerRect.origin.x + cornerWidth, centerRect.origin.y + centerRect.size.height) CGContextDrawPath(context, kCGPathStroke) }</code></pre> <p>现在可以看到 init(coder aDecoder: NSCoder) 这个方法初始化了一些数据,这些数据同样需要展示到布局中,所以在这里来做这件事情。</p> <p>所有绘制的代码需要在 drawRect(rect: CGRect) 中实现,绘制的步骤分成了以下几步:</p> <ul> <li>填充控件背景</li> <li>在背景中扣一个透明的洞</li> <li>在背景之上绘制正方形框</li> <li>绘制4个角</li> </ul> <p>绘制的画笔跟现实中一样的,后面绘制的会覆盖前面绘制的,如果有交集的话。</p> <p>到此自定义控件的界面已经完成,这个时候可以看到有一个方形的框在屏幕上了,具体样式看上图。</p> <h2><strong>捕捉摄像头数据</strong></h2> <p>AVFoundation来捕捉摄像头数据,并处理二维码解析出来的数据。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/ef586d4b8b89a0e325d3269c287c93c5.jpg"></p> <p>摄像头获取数据效果</p> <p>摄像头采集很简单,只需要使用ios提供的API就能很容易的实现。在这个例子中,有一个初始化方法主要用来做摄像头数据采集的:</p> <pre> <code class="language-swift">/** 初始化相机捕捉 **/ func initCapture(captureView:UIView, delegate:AVCaptureMetadataOutputObjectsDelegate) { mCaptureView = captureView let captureDevice = AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo) var error: NSError? let input: AnyObject! = AVCaptureDeviceInput.deviceInputWithDevice(captureDevice, error: &error) if (error != nil) { println("\(error?.localizedDescription)") } else { let captureViewFrame = captureView.frame mCaptureSession = AVCaptureSession() mCaptureSession?.addInput(input as! AVCaptureInput) let captureMetadataOutput = AVCaptureMetadataOutput() let screenHeight = captureViewFrame.height; let screenWidth = captureViewFrame.width; let cropRect = self.frame; captureMetadataOutput.rectOfInterest = CGRectMake(cropRect.origin.y / screenHeight,cropRect.origin.x / screenWidth,cropRect.size.height / screenHeight,cropRect.size.width / screenWidth) mCaptureSession?.addOutput(captureMetadataOutput) captureMetadataOutput.setMetadataObjectsDelegate(delegate, queue: dispatch_get_main_queue()) captureMetadataOutput.metadataObjectTypes = supportedBarCodes mVideoPreviewLayer = AVCaptureVideoPreviewLayer(session: mCaptureSession) mVideoPreviewLayer?.videoGravity = AVLayerVideoGravityResizeAspectFill mVideoPreviewLayer?.frame = mCaptureView!.frame mCaptureView!.layer.addSublayer(mVideoPreviewLayer) addMaskLayer() } }</code></pre> <p>这里这些API不光是适用于二维码,包括条形码等等都可以处理,主要是通过这个属性来过滤:</p> <pre> <code class="language-swift">captureMetadataOutput.metadataObjectTypes = supportedBarCodes</code></pre> <p>这个属性可以有很多值:</p> <pre> <code class="language-swift">[AVMetadataObjectTypeQRCode, AVMetadataObjectTypeCode128Code, AVMetadataObjectTypeCode39Code, AVMetadataObjectTypeCode93Code, AVMetadataObjectTypeUPCECode, AVMetadataObjectTypePDF417Code, AVMetadataObjectTypeEAN13Code, AVMetadataObjectTypeAztecCode]</code></pre> <p>这里主要介绍二维码,所以只用其中的一个。</p> <p>对UIView的layer层级做一个简要说明:使用了两个layer,一个是摄像头捕捉的layer: videoPreviewLayer ,一个是蒙板layer这个layer的作用就是在中间扣一个白色的洞,让扫描框之外的区域看起来颜色更暗。初始化这个蒙板的代码如下:</p> <pre> <code class="language-swift">/** 获取蒙板 **/ private func getMaskLayer(rect:CGRect) -> CAShapeLayer { let layer = CAShapeLayer.new() setMaskLayer(layer, rect: rect) return layer }</code></pre> <p>有了这两个蒙板只要按照顺序添加 layer 到 UIView 就好了。这里要注意的一个地方就是到目前为止,如果直接在布局文件中,放入控件,比如底上的那行字,这个时候运行,你是看不到这行字的,原因就是这行字的层级比蒙板层级要低,所以被挡住了,所以我们在添加完蒙板后,我们把父控件的每一个子控件移动到最顶层,当然移动的时候要排除我们这个二维码View:</p> <pre> <code class="language-swift">/** 把所有的其他图层移动到最顶层 **/ private func moveAllToFront(view:UIView) { for var i = 0; i < view.subviews.count; ++i { if let view: QRScannerView = view.subviews[i] as? QRScannerView { } else { view.bringSubviewToFront(view.subviews[i] as! UIView) } } }</code></pre> <p>到此摄像头捕捉部分就完成了,下面介绍怎么添加横线移动的动画。</p> <h2><strong>添加扫描线动画</strong></h2> <p>添加动画代码比较简单,就直接贴了:</p> <pre> <code class="language-swift">/** 开始横线移动 **/ func startLineRunning() { let rect = self.bounds let lineFrame = self.mMoveLine?.frame UIView.animateWithDuration(1.5 ,animations: { self.mMoveLine?.frame.origin.y = rect.height }){(Bool) in self.mMoveLine!.frame.origin.y = 0 self.startLineRunning() } }</code></pre> <p>完成动画后,我们需要在启动摄像头捕捉的时候让动画启动,当然也需要可以停止:</p> <pre> <code class="language-swift">/** 开始捕捉视频 **/ func startRunning() { mCaptureSession?.startRunning() mMoveLine?.hidden = false if self.mLineAnimationEnable { self.mLineAnimationEnable = false self.startLineRunning() } } /** 停止捕捉视频 **/ func stopRunning() { mMoveLine?.hidden = true mCaptureSession?.stopRunning() }</code></pre> <h2><strong>实例代码:</strong></h2> <p>实现好了 QRScannerView 后怎么使用呢?</p> <ul> <li>在controller实现协议: <pre> <code class="language-swift">AVCaptureMetadataOutputObjectsDelegate</code></pre> </li> <li>其他代码包含启动、初始化、和摄像头捕捉的数据处理,这里主要是 func captureOutput(captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [AnyObject]!, fromConnection connection: AVCaptureConnection!) 会获取到捕捉到的二维码数据: <pre> <code class="language-swift">override func viewDidAppear(animated: Bool) { scanner.startRunning() isCaputure = false } override func viewDidDisappear(animated: Bool) { scanner.stopRunning() } override func viewWillAppear(animated: Bool) { scanner.initCapture(self.view, delegate: self) } var isCaputure = false /** 捕捉回调 **/ func captureOutput(captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [AnyObject]!, fromConnection connection: AVCaptureConnection!) { var resultString = "" if metadataObjects == nil || metadataObjects.count == 0 { resultString = "scanner error" } else { let metadataObj = metadataObjects[0] as! AVMetadataMachineReadableCodeObject if self.scanner.supportedBarCodes.filter({ $0 == metadataObj.type }).count > 0 { let barCodeObject = self.scanner.videoPreviewLayer?.transformedMetadataObjectForMetadataObject(metadataObj as AVMetadataMachineReadableCodeObject) as! AVMetadataMachineReadableCodeObject resultString = barCodeObject.stringValue } } print("isCaputure \(isCaputure)") if !isCaputure { isCaputure = true self.requestValidate(resultString) } }</code></pre> </li> </ul> <h2><strong>总结</strong></h2> <p>要实现任何一个自定义控件也好其他app功能也好,永远都是数据和界面分离的思维,界面什么样子数据管不着,数据什么样子界面管不着,至于说联系起来的方式就很多了,常用的一种就是界面需要什么样子的数据,数据就怎么提供,还可以在中间添加适配器,不管什么数据都转化成界面需要的数据结构。</p> <p> </p> <p>来自:http://www.jianshu.com/p/b0865b82a4dc</p> <p> </p>