当前位置: 移动技术网 > 移动技术>移动开发>IOS > iOS 二维码扫描相关功能实现

iOS 二维码扫描相关功能实现

2019年07月23日  | 移动技术网移动技术  | 我要评论

写在前面

最近项目要实现相机扫描二维码功能,具体要求:1、扫描框 2、扫描动画 3、相册识别二维码 4、声音反馈。

记得之前用过三方库做过类似功能,但是也是知其然不知其所以然,然后今天自己用原生api简单封装了一个二维码扫描控件。

项目结构介绍

控件封装后主要结构如图:

如图中代码目录,vender里面放的是uiview+frame分类,resource里面放的是图片声音资源,tzimagepickercontroller是第三方相册,用来获取相册中的二维码识别的。主要的就是以qr开头的文件,我们具体说一说。

qrcode.h

这个文件主要放的是各个文件的头文件,方便在各处调用

#import "qrcodescanmanager.h"
#import #import "qrlightmanager.h"
#import "qrcodescanview.h"
#import "qrcodehelper.h"

qrlightmanager

这个类是用来开启关闭闪光灯的

/**
 打开手电筒
 */
+ (void)openflashlight;
/**
 关闭手电筒
 */
+ (void)closeflashlight;
#pragma mark 打开手电筒
+ (void)openflashlight {
 avcapturedevice *capturedevice = [avcapturedevice defaultdevicewithmediatype:avmediatypevideo];
 nserror *error = nil;
 if ([capturedevice hastorch]) {
  bool locked = [capturedevice lockforconfiguration:&error];
  if (locked) {
   capturedevice.torchmode = avcapturetorchmodeon;
   [capturedevice unlockforconfiguration];
  }
 }
}
#pragma mark 关闭手电筒
+ (void)closeflashlight {
 avcapturedevice *device = [avcapturedevice defaultdevicewithmediatype:avmediatypevideo];
 if ([device hastorch]) {
  [device lockforconfiguration:nil];
  [device settorchmode:avcapturetorchmodeoff];
  [device unlockforconfiguration];
 }
}

qrcodescanview

这个类是将这个界面单独封装出来,便于自定义

在.h文件中有个枚举用来标识二维码扫描四周角标的位置:

typedef enum : nsuinteger {
 cornerloactiondefault,//默认与边框同中心点
 cornerloactioninside,//在边框线内部
 cornerloactionoutside,//在边框线外部
} cornerloaction;

自定义view各个属性:

@property (nonatomic, strong) uicolor *bordercolor;/** 边框颜色*/
@property (nonatomic, assign) cornerloaction cornerlocation;/** 边角位置 默认default*/
@property (nonatomic, strong) uicolor *cornercolor;/** 边角颜色 默认正保蓝#32d2dc*/
@property (nonatomic, assign) cgfloat cornerwidth;/** 边角宽度 默认2.f*/
@property (nonatomic, assign) cgfloat backgroundalpha;/** 扫描区周边颜色的alpha 默认0.5*/
@property (nonatomic, assign) cgfloat timeinterval;/** 扫描间隔 默认0.02*/
@property (nonatomic, strong) uibutton *lightbtn;/** 闪光灯*/

暴露外部调用的方法:

/**
 添加定时器
 */
- (void)addtimer;
/**
 移除定时器
 */
- (void)removetimer;
/**
 根据灯光判断
 */
- (void)lightbtnchangewithbrightnessvalue:(cgfloat)brightnessvalue;

初始化默认值:

- (void)initilize {
 self.backgroundcolor = [[uicolor blackcolor] colorwithalphacomponent:0.5];
 self.bordercolor = [uicolor whitecolor];
 _cornerlocation = cornerloactiondefault;
 _cornercolor = [uicolor colorwithred:50/255.0f green:210/255.0f blue:220/255.0f alpha:1.0];
 _cornerwidth = 2.0;
 self.timeinterval = 0.02;
 _backgroundalpha = 0.5;
  
 [self addsubview:self.lightbtn];
}

重写view的drawrect方法,在这个方法里面绘制scanview的边框和边角

//空白区域设置
 [[[uicolor blackcolor] colorwithalphacomponent:self.backgroundalpha] setfill];
 uirectfill(rect);
 //获取上下文,并设置混合模式 -> kcgblendmodedestinationout
 cgcontextref context = uigraphicsgetcurrentcontext();
 cgcontextsetblendmode(context, kcgblendmodedestinationout);
 //设置空白区
 uibezierpath *bezierpath = [uibezierpath bezierpathwithrect:cgrectmake(borderx + 0.5 * borderlinew, bordery+ 0.5 * borderlinew, borderw - borderlinew, borderh - borderlinew)];
 [bezierpath fill];
 //执行混合模式
 cgcontextsetblendmode(context, kcgblendmodenormal);
  
 //边框设置
 uibezierpath *borderpath = [uibezierpath bezierpathwithrect:cgrectmake(borderx, bordery, borderw, borderh)];
 borderpath.linecapstyle = kcglinecapbutt;
 borderpath.linewidth = borderlinew;
 [self.bordercolor set];
 [borderpath stroke];
  
 cgfloat cornerlength = 20;
 //左上角小图标
 uibezierpath *lefttoppath = [uibezierpath bezierpath];
 lefttoppath.linewidth = self.cornerwidth;
 [self.cornercolor set];
  
 cgfloat insideexcess = fabs(0.5 * (self.cornerwidth - borderlinew));
 cgfloat outsideexcess = 0.5 * (borderlinew + self.cornerwidth);
 if (self.cornerlocation == cornerloactioninside) {
  [lefttoppath movetopoint:cgpointmake(borderx + insideexcess, bordery + cornerlength + insideexcess)];
  [lefttoppath addlinetopoint:cgpointmake(borderx + insideexcess, bordery + insideexcess)];
  [lefttoppath addlinetopoint:cgpointmake(borderx + cornerlength + insideexcess, bordery + insideexcess)];
 } else if (self.cornerlocation == cornerloactionoutside) {
  [lefttoppath movetopoint:cgpointmake(borderx - outsideexcess, bordery + cornerlength - outsideexcess)];
  [lefttoppath addlinetopoint:cgpointmake(borderx - outsideexcess, bordery - outsideexcess)];
  [lefttoppath addlinetopoint:cgpointmake(borderx + cornerlength - outsideexcess, bordery - outsideexcess)];
 } else {
  [lefttoppath movetopoint:cgpointmake(borderx, bordery + cornerlength)];
  [lefttoppath addlinetopoint:cgpointmake(borderx, bordery)];
  [lefttoppath addlinetopoint:cgpointmake(borderx + cornerlength, bordery)];
 }
 [lefttoppath stroke];

增加定时器以及开始动画:

- (void)addtimer {
 [self addsubview:self.scanningline];
 self.timer = [nstimer timerwithtimeinterval:self.timeinterval target:self selector:@selector(beginanimaiton) userinfo:nil repeats:yes];
 [[nsrunloop mainrunloop] addtimer:_timer formode:nsrunloopcommonmodes];
}
#pragma mark 动画
- (void)beginanimaiton {
 static bool isorignpostion = yes;
  
 if (isorignpostion) {
  _scanningline.y = 0;
  isorignpostion = no;
  [uiview animatewithduration:self.timeinterval animations:^{
   self->_scanningline.y += 2;
  } completion:nil];
 } else {
  if (_scanningline.frame.origin.y >= 0) {
   cgfloat scancontent_maxy = self.frame.size.width;
   if (_scanningline.y >= scancontent_maxy - 10) {
    _scanningline.y = 0;
    isorignpostion = yes;
   } else {
    [uiview animatewithduration:0.02 animations:^{
     self->_scanningline.y += 2;
      
    } completion:nil];
   }
  } else {
   isorignpostion = !isorignpostion;
  }
 }
}

闪光灯按钮点击事件,开启关闭闪光灯:

- (void)lightbtnclick:(uibutton *)btn {
 btn.selected = !btn.selected;
 if (btn.selected) {
  [qrlightmanager openflashlight];
 } else {
  [qrlightmanager closeflashlight];
 }
}

qrcodescanmanager

这个单例用来控制所有的关于二维码扫描的事件

初始开启session 会话

//设置二维码读取率 数据类型 当前控制器
- (void)setupsessionpreset:(nsstring *)sessionpreset metadataobjecttypes:(nsarray *)metadataobjecttypes currentcontroller:(uiviewcontroller *)currentcontroller {
 if (sessionpreset == nil || metadataobjecttypes == nil || currentcontroller == nil) {
  nsexception *excp = [nsexception exceptionwithname:@"excp" reason:@"setupsessionpreset:metadataobjecttypes:currentcontroller: 方法中的 sessionpreset 参数不能为空" userinfo:nil];
  [excp raise];
 }
  
 //1、获取摄像设备
 avcapturedevice *device = [avcapturedevice defaultdevicewithmediatype:avmediatypevideo];
  
 //2、创建设备输入流
 avcapturedeviceinput *deviceinput = [avcapturedeviceinput deviceinputwithdevice:device error:nil];
  
 //3、创建数据输出流
 avcapturemetadataoutput *metadataoutput = [[avcapturemetadataoutput alloc] init];
 [metadataoutput setmetadataobjectsdelegate:self queue:dispatch_get_main_queue()];
  
 //3(1)、创建设备输出流
 self.videodataoutput = [[avcapturevideodataoutput alloc] init];
 [_videodataoutput setsamplebufferdelegate:self queue:dispatch_get_main_queue()];
  
 // 设置扫描范围(每一个取值0~1,以屏幕右上角为坐标原点)
 // 注:微信二维码的扫描范围是整个屏幕,这里并没有做处理(可不用设置); 如需限制扫描范围,打开下一句注释代码并进行相应调试
 // metadataoutput.rectofinterest = cgrectmake(0.05, 0.2, 0.7, 0.6);
  
 //4、创建会话对象
 _session = [[avcapturesession alloc] init];
 //会话采集率:avcapturesessionpresethigh
 _session.sessionpreset = sessionpreset;
  
 //5、添加设备输出流到会话对象
 [_session addoutput:metadataoutput];
 //5(1)添加设备输出流到会话对象;与3(1)构成识别光纤强弱
 [_session addoutput:_videodataoutput];
  
 //6、添加设备输入流到会话对象
 [_session addinput:deviceinput];
  
 //7、设置数据输出类型,需要将数据输出添加到会话后,才能指定元数据类型,否则会报错
 // 设置扫码支持的编码格式(如下设置条形码和二维码兼容)
 // @[avmetadataobjecttypeqrcode, avmetadataobjecttypeean13code, avmetadataobjecttypeean8code, avmetadataobjecttypecode128code]
 metadataoutput.metadataobjecttypes = metadataobjecttypes;
  
 //8、实例化预览图层,传递_session是为了告诉图层将来显示什么内容
 _videopreviewlayer = [avcapturevideopreviewlayer layerwithsession:_session];
 //保持纵横比;填充层边界
 _videopreviewlayer.videogravity = avlayervideogravityresizeaspectfill;
 cgfloat x = 0;
 cgfloat y = 0;
 cgfloat w = [uiscreen mainscreen].bounds.size.width;
 cgfloat h = [uiscreen mainscreen].bounds.size.height;
 _videopreviewlayer.frame = cgrectmake(x, y, w, h);
 [currentcontroller.view.layer insertsublayer:_videopreviewlayer atindex:0];
  
 //9、启动会话
 [_session startrunning];
  
}

block:

typedef void(^getbrightnessblock)(cgfloat brightness);//用来向外部传递捕获到的亮度值以便于识别何时开启闪光灯,
typedef void(^scanblock)(nsarray *metadataobjects);//捕获到的结果集合
//亮度回调
- (void)brightnesschange:(getbrightnessblock)getbrightnessblock;
//扫描结果
- (void)scanresult:(scanblock)scanblock;
- (void)startrunning {
 [_session startrunning];
}
- (void)stoprunning {
 [_session stoprunning];
}
需要遵循:/**
 重置根据光线强弱值打开手电筒 delegate方法
 */
- (void)resetsamplebufferdelegate;
/**
 取消根据光线强弱值打开手电筒的delegate方法
 */
- (void)cancelsamplebufferdelegate;
- (void)resetsamplebufferdelegate {
 [_videodataoutput setsamplebufferdelegate:self queue:dispatch_get_main_queue()];
}
- (void)cancelsamplebufferdelegate {
 [_videodataoutput setsamplebufferdelegate:nil queue:dispatch_get_main_queue()];
}
#pragma mark 播放扫描提示音
- (void)playsoundname:(nsstring *)name {
 nsstring *audiofile = [[nsbundle mainbundle] pathforresource:name oftype:nil];
 nsurl *fileurl = [nsurl fileurlwithpath:audiofile];
 systemsoundid soundid = 0;
 audioservicescreatesystemsoundid((__bridge cfurlref)(fileurl), &soundid);
 audioservicesaddsystemsoundcompletion(soundid, null, null, soundcompletecallback, null);
 audioservicesplaysystemsound(soundid); // 播放音效
}
void soundcompletecallback(systemsoundid soundid, void *clientdata){
  
}

注:这里只是截取部分重要代码,具体功能我会将代码放到github上,代码里注释也写的很明白

使用功能

开启、结束定时器

开启、关闭session会话

启用、移除samplebuffer代理

- (void)viewwillappear:(bool)animated {
 [super viewwillappear:animated];
 [self.scanview addtimer];
 [_scanmanager startrunning];
 [_scanmanager resetsamplebufferdelegate];
}
- (void)viewwilldisappear:(bool)animated {
 [super viewwilldisappear:animated];
 [self.scanview removetimer];
 [_scanmanager stoprunning];
 [_scanmanager cancelsamplebufferdelegate];
}

初始化scanmanager

- (void)setupscanmanager {
 self.scanmanager = [qrcodescanmanager sharedmanager];
  
 nsarray *arr = @[avmetadataobjecttypeqrcode, avmetadataobjecttypeean13code, avmetadataobjecttypeean8code, avmetadataobjecttypecode128code];
 [_scanmanager setupsessionpreset:avcapturesessionpreset1920x1080 metadataobjecttypes:arr currentcontroller:self];
  
 __weak __typeof(self)weakself = self;
 //光扫描结果回调
 [_scanmanager scanresult:^(nsarray *metadataobjects) {
  if (metadataobjects != nil && metadataobjects.count > 0) {
   [weakself.scanmanager playsoundname:@"sound.caf"];
   //obj 为扫描结果
   avmetadatamachinereadablecodeobject *obj = metadataobjects[0];
   nsstring *url = [obj stringvalue];
   nslog(@"---url = :%@", url);
  } else {
   nslog(@"暂未识别出扫描的二维码");
  }
 }];
  
 //光纤变化回调
 [_scanmanager brightnesschange:^(cgfloat brightness) {
  [weakself.scanview lightbtnchangewithbrightnessvalue:brightness];
 }];
  
}

从相册识别二维码:

//借助第三方相册
- (void)albumbtnclick {
 tzimagepickercontroller *pickercontroller = [[tzimagepickercontroller alloc] initwithmaximagescount:1 delegate:self];
  
 __weak __typeof(self)weakself = self;
  
 [pickercontroller setdidfinishpickingphotoshandle:^(nsarray *photos, nsarray *assets, bool isselectoriginalphoto) {
  uiimage *image = photos[0];
  // cidetector(cidetector可用于人脸识别)进行图片解析,从而使我们可以便捷的从相册中获取到二维码
  // 声明一个 cidetector,并设定识别类型 cidetectortypeqrcode
  // 识别精度
  cidetector *detector = [cidetector detectoroftype:cidetectortypeqrcode context:nil options:@{cidetectoraccuracy:cidetectoraccuracyhigh}];
   
  //取得识别结果
  nsarray *features = [detector featuresinimage:[ciimage imagewithcgimage:image.cgimage]];
   
  nsstring *resultstr;
  if (features.count == 0) {
   nslog(@"暂未识别出二维码");
  } else {
   for (int index = 0; index < [features count]; index++) {
    ciqrcodefeature *feature = [features objectatindex:index];
    resultstr = feature.messagestring;
   }
   nslog(@"---url:%@", resultstr);
  }
 }];
 [self presentviewcontroller:pickercontroller animated:yes completion:nil];
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持移动技术网。

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网