Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

iOS 课程期末作业项目,我选择做了个商品码扫描枪App。对我这个没有 iOS 开发经验的人来说这的确有点难度。熬了三天几乎从零开始(我一直都是做 JavaEE 开发,对编程语言已经有了不少的基本认识,对编程基础也已经掌握了)写出来了这个 App 。

我对 iOS 开发的基础经验来源于 Android 开发,毕竟基于 Java ,很多概念学习起来比较轻松。诸如视图、控制器等等。移动端 App 开发很注重 MVC 模式,我开发网站也全是基于这个模式。

##需要涉及的东西

###机械可阅读码
条形码和二维码都属于同一种东西,叫 Machine Readable Codes,翻译过来就是机器可阅读码。基本原理就是把文字编码成机器容易识别的编码,然后打印出来,方便通过摄像头或者其他光学识别器来识别。其本身就是一个信息的容器,跟 BASE64 和其他编码技术只有载体上的区别。

###设备输入输出

接下来解释一下 iOS 里关于视频捕获的类

  • AVCaptureSession
  • AVCaptureDevice
  • AVCaptureDeviceInput
  • 输出

AVCaptureSession 是一个用于管理捕获输入输出会话的类。新建一个会话,就像跟系统说“我要准备抓点东西了”。至于要抓什么东西则是需要我们接下来说明的。抓点东西,那么这个行为肯定有两个要点:从哪里输入,以及输出到哪去。

从哪里输入,需要有一个来源, AVCaptureDevice 便是干这个用的。 AVCaptureDevices 是指代一个系统输入设备的类,这些设备可以是话筒、摄像头等等,通过其静态方法 devicesWithMediaType 能够利用设备的类型来获取一个设备列表。在我的 App 里有一个方法专门获取后置摄像头设备

//Get Back-end camera
- (AVCaptureDevice *) backendCamera{
    
    //通过一个类型来获取设备列表
    NSArray *cameras = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]; 
    
    //遍历这个数组获取符合条件的设备
    for (AVCaptureDevice *camera in cameras) {
    
        if([camera position] == AVCaptureDevicePositionBack){
            //如果匹配则返回
            NSLog(@"Camera found:%ld", (long)camera.position);
            return camera;
        }
    }
    return NULL;
}

有了目标设备就可以拿着这个设备创建一个输入来源了

//这里省略 ScanningHandler 的初始化方法
//获取后置摄像头
AVCaptureDevice *theCamera = [self backendCamera];

//申请一个输入来源
_cameraInput = [[AVCaptureDeviceInput alloc] initWithDevice:theCamera error:nil];

输入有了,接下来就需要有个输出了。输出到哪也是由一些类来指定。在我这个 App 里,输出有两个

  • 视频预览框
  • 机械码识别器

视频预览的输出是由 AVCaptureVideoPreviewLayer 来管理。输出有两点:输入来源和输出目的地,在这个类里目的地是一个 View

//创建一个输出层,从捕获会话里获取输入
_cameraPreviewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:_session];

UIView *view = _viewApplicated;
CALayer *viewLayer = [view layer];
    
[viewLayer setMasksToBounds:YES];
    
CGRect bounds = [view bounds];

//设定预览层的大小为指定 View 的大小
[_cameraPreviewLayer setFrame:bounds];
//设定预览层的大小显示方式,这个值是让预览画面充满视图,显示的区域的中心是摄像头画面的中心
[_cameraPreviewLayer setVideoGravity:AVLayerVideoGravityResizeAspectFill];
    
//把这个层塞到目的视图里面。
[viewLayer insertSublayer:_cameraPreviewLayer below:[[viewLayer sublayers] objectAtIndex:0]];

搞定了预览之后还有给机械识别码的输出

 //创建一个输出,从会话获取输入
_captureMetadataOutput = [[AVCaptureMetadataOutput alloc] init];

//这个输出有一个回调,当识别到指定的机械码的时候会把数据返回并调用这个函数
//queue参数是指这个读取器要在哪个线程上执行,这里直接使用主线程。
[_captureMetadataOutput setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];

//设置捕获区域,如果不设置的话默认识别全部
//虽然上面已经限定了 Camera 的预览区域,但是返回的数据还是摄像头捕获到的所有图像。
//这个范围参数跟普通的 View 的范围参数不一样,它的值是一个占比。

[_captureMetadataOutput setRectOfInterest:CGRectMake(0.375, 0, 0.25, 1)];

//把这个输出放到会话里好让输出有地方出来
[_session addOutput:_captureMetadataOutput];
[self captureBarCode];

##结构

现在来说一下这个 App 的结构,从可视化开始讲起就是 View 。这个 App 我设计只有一个 View ,里面包含了一个视频的预览窗口(用于让用户能够比较轻松地瞄准目标机器码)、一个列表视图(TableView)以及一个包含了切换器和“编辑列表”按钮的工具栏。

接下来是管理这个 View 的 Controller。里面包含了一些界面的逻辑代码。为了方便,我直接在这里实现了一个 TableView 的控制器,也就是主页面和列表视图共用一个视图控制器。

为了让 ViewController 更加简洁,我写了一个 ScanningHandler 来负责管理摄像头视频数据的获取和分析。对于主视图来说,它需要关心的只是让摄像头的预览数据能够显示在指定区域里,以及如何获得扫描得来的数据。因此,把关于扫描的设置以及管理扫描设置的相关方法整合到一个类里面会方便很多。

按照 MVC 的设计模式,我把数据获取部分按照 Java 的方式,做了一个 CommodityRepository(其实就是DAO) ,负责接收一个商品的编码并返回一个 Commodity(商品) 对象。

因为习惯问题,我这个项目也是做了国际化的,关于国际化如何实现我不累赘,在文章后面的链接里有。

##遇到的一些问题

在获取到一个 MetadataOutput 之后,它的扫描范围是整个摄像头的范围,需要手动设置这个范围。如果是二维码的话,这个不影响效率,但是是条形码的话,这样做会大大降低扫描的速度。最好的做法是设置一个。但这个范围跟 CameraPreviewLayout 不一样,它的范围尺寸是一个比值,例如要扫描摄像头获取的图像的上半部分,则设置宽度为 1 ,高度为 0.5 。如何让其定位到 CameraPreviewLayout 还是一个难点。

CameraPreviewLayout 的 AVLayerVideoGravityResizeAspectFill 选项,会让摄像头画面居中,然后宽度拉伸到跟 View 一样,所以显示出来的位置是摄像头画面的中间部分。

关闭和打开捕获的时候我做了个小动画,这个动画我是创建了一个毛玻璃效果视图,然后通过动画调整器透明度。

- (IBAction)scannerControlSwitch:(UISwitch *)sender {
    if ([sender isOn]) {
        
        _visualEffectView.alpha = 1;
        [UIView animateWithDuration:0.2 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
            _visualEffectView.alpha = 0;
        } completion:^(BOOL finished) {
            [_scanningHandler startCapturing];
            [_visualEffectView setHidden:YES];
        }];
        
    }else{
        _visualEffectView.alpha = 0;
        [_visualEffectView setHidden:NO];
        [UIView animateWithDuration:0.2 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
            _visualEffectView.alpha = 1;
        } completion:^(BOOL finished) {
            [_scanningHandler stopCapturing];
        }];
    }
}

##参考资料

评论