Swift 二维码识别

ykgrwerfgt 7年前
   <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>