Swift 二维码识别

作者: Carltony | 来源:发表于2016-11-22 22:15 被阅读0次

二维码识别是很常见的app功能,为了更方便的在每一个使用二维码功能地方都能更快的实现,把二维码功能写入到了一个自定义的View里面,使用的时候和普通的UIView是一样的。效果如图(因为是模拟器运行的,所以摄像头看不到,用真机的时候就正常了):

[图片上传失败...(image-f60e5b-1527174275780)]

这篇文章只是为了快速实现效果,更细的知识点,比如自定义控件中更详细的内容不累述。
二维码识别分为三部实现:

  • 自定义UIView,实现方形的扫描区域
  • 实现摄像头捕捉
  • 扫描的横线动画

自定义UIView

首先新建一个类继承自UIView

class QRScannerView: UIView

接着实现两个重要的方法:

required init(coder aDecoder: NSCoder)// 这个方法实现的目的是,我们在storyboard文件中使用这个View的时候,会直接显示出来效果。
override func drawRect(rect: CGRect)// 这个实现的目的是绘制我们要显示的内容

这里简单说一下这个init(coder aDecoder: NSCoder),这个构造函数不是必须的,但是为了达到跟原生控件一样的效果:在布局的时候可以直接在布局文件中看到效果,实现这个构造函数就很重要了。第二个方法是实现二维码区域表现出来的视图样式的主要地方,这里可以绘制各
种图形和样式。

在布局中直接展示效果

两个方法实现的代码如下:

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)
}

现在可以看到init(coder aDecoder: NSCoder)这个方法初始化了一些数据,这些数据同样需要展示到布局中,所以在这里来做这件事情。
所有绘制的代码需要在drawRect(rect: CGRect)中实现,绘制的步骤分成了以下几步:

  • 填充控件背景
  • 在背景中扣一个透明的洞
  • 在背景之上绘制正方形框
  • 绘制4个角

绘制的画笔跟现实中一样的,后面绘制的会覆盖前面绘制的,如果有交集的话。

到此自定义控件的界面已经完成,这个时候可以看到有一个方形的框在屏幕上了,具体样式看上图。

捕捉摄像头数据

AVFoundation来捕捉摄像头数据,并处理二维码解析出来的数据。


摄像头获取数据效果

摄像头采集很简单,只需要使用ios提供的API就能很容易的实现。在这个例子中,有一个初始化方法主要用来做摄像头数据采集的:

/**
初始化相机捕捉
**/
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()
    }
}

这里这些API不光是适用于二维码,包括条形码等等都可以处理,主要是通过这个属性来过滤:

captureMetadataOutput.metadataObjectTypes = supportedBarCodes

这个属性可以有很多值:

[AVMetadataObjectTypeQRCode, 
AVMetadataObjectTypeCode128Code, 
AVMetadataObjectTypeCode39Code, 
AVMetadataObjectTypeCode93Code, 
AVMetadataObjectTypeUPCECode, 
AVMetadataObjectTypePDF417Code, 
AVMetadataObjectTypeEAN13Code, 
AVMetadataObjectTypeAztecCode]

这里主要介绍二维码,所以只用其中的一个。

对UIView的layer层级做一个简要说明:使用了两个layer,一个是摄像头捕捉的layer:videoPreviewLayer,一个是蒙板layer这个layer的作用就是在中间扣一个白色的洞,让扫描框之外的区域看起来颜色更暗。初始化这个蒙板的代码如下:

/**
获取蒙板
**/
private func getMaskLayer(rect:CGRect) -> CAShapeLayer
{
    let layer = CAShapeLayer.new()
    setMaskLayer(layer, rect: rect)
    return layer
}

有了这两个蒙板只要按照顺序添加layerUIView就好了。这里要注意的一个地方就是到目前为止,如果直接在布局文件中,放入控件,比如底上的那行字,这个时候运行,你是看不到这行字的,原因就是这行字的层级比蒙板层级要低,所以被挡住了,所以我们在添加完蒙板后,我们把父控件的每一个子控件移动到最顶层,当然移动的时候要排除我们这个二维码View:

/**
把所有的其他图层移动到最顶层
**/
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)
        }
    }
}

到此摄像头捕捉部分就完成了,下面介绍怎么添加横线移动的动画。

添加扫描线动画

添加动画代码比较简单,就直接贴了:

/**
开始横线移动
**/
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()
    }
}

完成动画后,我们需要在启动摄像头捕捉的时候让动画启动,当然也需要可以停止:

/**
开始捕捉视频
**/
func startRunning()
{
    mCaptureSession?.startRunning()
    mMoveLine?.hidden = false
    if self.mLineAnimationEnable
    {
        self.mLineAnimationEnable = false
        self.startLineRunning()
    }
}
/**
停止捕捉视频
**/
func stopRunning()
{
    mMoveLine?.hidden = true
    mCaptureSession?.stopRunning()
}

实例代码:

实现好了QRScannerView后怎么使用呢?

  • 在controller实现协议:
AVCaptureMetadataOutputObjectsDelegate
  • 其他代码包含启动、初始化、和摄像头捕捉的数据处理,这里主要是func captureOutput(captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [AnyObject]!, fromConnection connection: AVCaptureConnection!)会获取到捕捉到的二维码数据:
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)
        }
    }

完整demo下载地址:DemoQRView

总结

要实现任何一个自定义控件也好其他app功能也好,永远都是数据和界面分离的思维,界面什么样子数据管不着,数据什么样子界面管不着,至于说联系起来的方式就很多了,常用的一种就是界面需要什么样子的数据,数据就怎么提供,还可以在中间添加适配器,不管什么数据都转化成界面需要的数据结构。

😊查看更多😊

不登高山,不知天之高也;不临深溪,不知地之厚也
感谢指点、交流、喜欢

相关文章

网友评论

    本文标题:Swift 二维码识别

    本文链接:https://www.haomeiwen.com/subject/volmpttx.html