记录下现在的一些想法,将来遗忘的时候方便查阅。
二维码是app中非常常见的一个功能,且现在很多app的核心功能都需要用到二维码(支付宝、微信扫码支付),因此花点时间将相关的知识点纪录下来。
原理简介
二维码(2-dimensional bar code)是用某种特定的几何图形按一定规律在平面(二维方向上)分布的黑白相间的图形记录数据符号信息的;在代码编制上巧妙地利用构成计算机内部逻辑基础的“0”、“1”比特流的概念,使用若干个与二进制相对应的几何形体来表示文字数值信息,通过图象输入设备或光电扫描设备自动识读以实现信息自动处理:它具有条码技术的一些共性:每种码制有其特定的字符集;每个字符占有一定的宽度;具有一定的校验功能等。同时还具有对不同行的信息自动识别功能、及处理图形旋转变化点。目前我国广泛使用的二维码为源于日本的快速响应码(QRCode)。
iOS7以前,app中主要使用ZXing/ZBar这两个开源库来实现二维码相关功能。iOS7以后,AVFundation支持了二维码,且在扫描灵敏度和性能上来说都优于第三方框架。本文主要针对使用原生框架实现二维码相关功能所需知识点进行整理。虽然功能并不复杂,但在优化及整理方面还是耗费了不少的时间。
需要实现的功能
- 二维码扫描
- 添加扫描音效
- 添加闪光灯开关
- 二维码生成/自定义颜色
- 为二维码添加Logo
- 二维码识别(app内、相册中图片识别)
为了方便自定义扫码界面的UI,我们将功能部分尽量从控制器抽离封装成一个工具类JYQRCodeTool
二维码扫描(iOS7.0以上可用)
需要用到的类:
#import <AVFoundation/AVFoundation.h>
@interface JYQRCodeTool () <AVCaptureMetadataOutputObjectsDelegate>
/* 输入设备 */
@property(nonatomic,strong)AVCaptureDevice *jyDevice;
/* 输入数据源 */
@property(nonatomic,strong)AVCaptureDeviceInput *jyInput;
/* 输出数据源 */
@property(nonatomic,strong)AVCaptureMetadataOutput *jyOutput;
/* 会话层 中间桥梁 负责把捕获的音视频数据输出到输出设备中 */
@property(nonatomic,strong)AVCaptureSession *jySession;
/* 相机拍摄预览图层 */
@property(nonatomic,strong)AVCaptureVideoPreviewLayer *jyPreview;
@end
对于不需要自定义的UI(如添加遮罩,相机预览层),我们也把它们封装到了工具类中,由于对UI进行操作需要获取控制器,我们在添加一个属性:
#import <UIKit/UIKit.h>
//这里使用weak,防止循环引用
@property(nonatomic,weak)UIViewController *bindVC;
添加一个初始化方法,在初始化时绑定扫码界面的控制器:
.h
#import <Foundation/Foundation.h>
@interface JYQRCodeTool : NSObject
/**
* 初始化二维码扫描工具
*
* @param controller 扫描界面对应的控制器
**/
+ (instancetype)toolsWithBindingController:(UIViewController *)controller;
@end
.m
+ (instancetype)toolsWithBindingController:(UIViewController *)controller
{
return [[self alloc] initWithBindingController:controller];
}
- (instancetype)initWithBindingController:(UIViewController *)controller
{
if (self = [super init]) {
if (_bindVC != controller) {
_bindVC = controller;
}
}
return self;
}
接下来添加一个方法,初始化扫码功能:
.h
/**
* 初始化扫描二维码功能,自动确定扫描范围,范围外的部分加遮罩
*
* @param rect 扫描范围
**/
- (void)jy_setUpCaptureWithRect:(CGRect)rect;
/**
* 初始化扫描二维码功能,添加完成后回调
*
* @param rect 扫描范围
* @param successCB 完成后回调
**/
- (void)jy_setUpCaptureWithRect:(CGRect)rect success:(void(^)())successCB;
.m
-(void)jy_setUpCaptureWithRect:(CGRect)rect
{
return [self jy_setUpCaptureWithRect:rect success:nil];
}
- (void)jy_setUpCaptureWithRect:(CGRect)rect success:(void(^)())successCB
{
//添加遮罩
[self addCropRect:rect];
CGSize cSize = [UIScreen mainScreen].bounds.size;
//计算rectOfInterest 注意x,y交换位置
CGRect rectOfInterest = CGRectMake(rect.origin.y/cSize.height, rect.origin.x/cSize.width, rect.size.height/cSize.height,rect.size.width/cSize.width);
//判断是否可以调用相机
if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera])
{
[self setUpCaptureWithRect:rectOfInterest succees:successCB];
}
else{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"设备不支持该功能" message:nil delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil];
[alert show];
}
}
我们使用CAShapeLayer来为扫码范围外的部分添加遮罩,具体的方法就是在layer上画两个矩形,一个rect为扫码范围,另一个rect为屏幕尺寸,然后将两个矩形之间的部分填充为黑色,并设置透明度,这样扫码范围外的遮罩就做好了:
//添加遮罩
- (void)addCropRect:(CGRect)cropRect{
CAShapeLayer *cropLayer = [[CAShapeLayer alloc] init];
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, nil, cropRect);
CGPathAddRect(path, nil, _bindVC.view.bounds);
[cropLayer setFillRule:kCAFillRuleEvenOdd];
[cropLayer setPath:path];
[cropLayer setFillColor:[UIColor blackColor].CGColor];
[cropLayer setOpacity:0.6];
[_bindVC.view.layer addSublayer:cropLayer];
}
接下来是初始化扫码功能的具体实现:
- (void)setUpCaptureWithRect:(CGRect)rectOfInterest succees:(void(^)())successCB
{
AVAuthorizationStatus authStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
//判断相机权限
if (authStatus == AVAuthorizationStatusDenied) {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"相机权限已关闭" message:@"请在设置->隐私->相机中,允许app访问相机。" delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil];
[alert show];
}
else{
dispatch_async(dispatch_get_main_queue(), ^{
//初始化输入流,设备为相机
_jyDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
_jyInput = [AVCaptureDeviceInput deviceInputWithDevice:_jyDevice error:nil];
//初始化输出流,设置代理,在主线程里刷新
_jyOutput = [[AVCaptureMetadataOutput alloc] init];
[_jyOutput setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];
//初始化会话层,添加输入/输出流
_jySession = [[AVCaptureSession alloc] init];
[_jySession setSessionPreset:([UIScreen mainScreen].bounds.size.height<500)?AVCaptureSessionPreset640x480:AVCaptureSessionPresetHigh];
[_jySession addInput:_jyInput];
[_jySession addOutput:_jyOutput];
//设置输出对象类型为QRCode,设置坐标
_jyOutput.metadataObjectTypes = @[AVMetadataObjectTypeQRCode];
_jyOutput.rectOfInterest = rectOfInterest;
//初始化相机预览层
_jyPreview = [AVCaptureVideoPreviewLayer layerWithSession:_jySession];
_jyPreview.videoGravity = AVLayerVideoGravityResizeAspectFill;
_jyPreview.frame = [[UIScreen mainScreen] bounds];
[_bindVC.view.layer insertSublayer:_jyPreview atIndex:0];
//开启会话层,启用扫码功能
[_jySession startRunning];
//初始化完成回调,可在此移除UI上的loading界面
if (successCB) {
successCB();
}
});
}
}
这里有几个注意点说明一下,首先是rectOfInterest属性,这个属性控制扫码范围,默认值为CGRectMake(0, 0, 1, 1);即全屏扫描,因为扫码默认是横屏,所以该值坐标系原点在右上角,与UIView的坐标系不同,x,y坐标需要互换,且取值是按照摄像头分辨率来取的比例,而不是屏幕的宽高比例。由于4英寸以上机型宽高比与摄像头保持一致,所以我们针对3.5英寸机型做一下适配,这样使用屏幕宽高比也不会有很大误差。
[_jySession setSessionPreset:([UIScreen mainScreen].bounds.size.height<500)?AVCaptureSessionPreset640x480:AVCaptureSessionPresetHigh];
然后调用该方法前我们对坐标进行了转换处理:
CGSize cSize = [UIScreen mainScreen].bounds.size;
//计算rectOfInterest 注意x,y交换位置
CGRect rectOfInterest = CGRectMake(rect.origin.y/cSize.height, rect.origin.x/cSize.width, rect.size.height/cSize.height,rect.size.width/cSize.width);
由于初始化这部分比较耗时,测试中明显感觉到了卡顿感严重,所以我刚开始想到用多线程来处理,不过相机预览层又对UI执行了操作,无法放进子线程。最后我们使用了这种方法:
dispatch_async(dispatch_get_main_queue(), ^{
}
在主线程中异步调用主队列添加任务,不开辟新线程,不会阻塞主线程,添加的任务会在主线程中的任务执行完后再开始执行。这样既不会造成卡顿又可以正常初始化,只是要多花一点点的时间,因为主线程串行,无法做到切换页面的同时进行初始化,只能等到页面加载完再进行初始化,这里可以在UI上做一个loading界面,初始化完成后在block中移除loading以优化体验。关于这里想详细了解的同学可以看我的另一篇文章 iOS 多线程之GCD相关知识点梳理
为了可以在扫码界面进行扫码成功后的数据解析,我们还需要传递AVCaptureMetadataOutput的delegate。在头文件中声明一个协议:
.h
@protocol JYQRCodeDelegate <NSObject>
/**
* 扫描成功回调
**/
- (void)jy_willGetOutputMataDataObject;
/**
* 数据处理完毕回调
*
* @param outPutString 返回的数据
**/
- (void)jy_didGetOutputMataDataObjectToString:(NSString *)outPutString;
@end
@property(nonatomic,weak) id <JYQRCodeDelegate> delegate;
.m
#pragma mark - Delegate
-(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection
{
if (metadataObjects.count > 0) {
//关闭会话
[_jySession stopRunning];
//播放扫描音效
[self playScanSoundsWithName:_scanVoiceName];
if ([_delegate respondsToSelector:@selector(jy_willGetOutputMataDataObject)]) {
//扫描成功后的回调,在这里显示loading
[_delegate jy_willGetOutputMataDataObject];
}
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//将数据转为NSString
AVMetadataMachineReadableCodeObject *readableObj = metadataObjects.firstObject;
NSString *outPutString = readableObj.stringValue;
dispatch_async(dispatch_get_main_queue(), ^{
if ([_delegate respondsToSelector:@selector(jy_didGetOutputMataDataObjectToString:)]) {
//获得数据的回调,在这里自行确定解析逻辑
[_delegate jy_didGetOutputMataDataObjectToString:outPutString];
}
});
});
}
}
最后添加两个方法,便于外界启用/禁用扫码功能:
.h
/**
* 启用扫描功能
**/
- (void)jy_startScaning;
/**
* 禁用扫描功能
**/
- (void)jy_stopScaning;
.m
- (void)jy_startScaning;
{
if (_jySession && !_jySession.isRunning) {
[_jySession startRunning];
}
}
-(void)jy_stopScaning
{
if (_jySession && _jySession.isRunning) {
[_jySession stopRunning];
}
}
到了这里,扫码功能部分就算封装好了。接下来创建一个扫码视图控制器来使用:
#import "JYQRScanController.h"
#import "JYQRCodeTool.h"
@interface JYQRScanController () <JYQRCodeDelegate>
@property(nonatomic,strong)JYQRCodeTool *jyQRTool;
@end
//LazyLoad
-(JYQRCodeTool *)jyQRTool
{
if (!_jyQRTool) {
_jyQRTool = [JYQRCodeTool toolsWithBindingController:self];
_jyQRTool.delegate = self;
}
return _jyQRTool;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor blackColor];
// self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"相册" style:UIBarButtonItemStylePlain target:self action:@selector(openPhotoLibrary)];
self.navigationItem.title = @"扫一扫";
CGSize cSize = [UIScreen mainScreen].bounds.size;
//自定义扫描范围
CGSize scanSize = CGSizeMake(cSize.width * 2.8/4, cSize.width * 2.8/4);
CGRect scanRect = CGRectMake((cSize.width - scanSize.width) / 2, (cSize.height - scanSize.height) / 2, scanSize.width, scanSize.height);
//初始化扫码功能
[self.jyQRTool jy_setUpCaptureWithRect:scanRect];
}
#pragma mark - Delegate
-(void)jy_willGetOutputMataDataObject
{
}
-(void)jy_didGetOutputMataDataObjectToString:(NSString *)outPutString
{
//对扫描获得的数据进行处理
}
iOS10当中需要对隐私权限进行设置,由于我们用到了相机(相册、麦克风等设备同理),需要在info.plist中添加对应字段,否则程序运行会报错:
QQ20170630-170907@2x.png运行一下,效果如图:
扫一扫自定义扫码界面
我们用一句代码开启了扫码功能,接下来我们可以对扫码框部分来进行个性化设置,一般我们看到的app中扫码框的四角有标记确定范围,中间有扫描线动画,下面就来简单的装饰一下。
创建一个JYScanRectView,继承于UIView:
-(instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
self.layer.borderColor = [UIColor darkGrayColor].CGColor;
self.layer.borderWidth = 1.0;
[self customScanCorners];
[self customScanLine];
[self customLoadingView];
}
return self;
}
可以使用CAShapeLayer+UIBezierPath添加四角标记:
//添加四角标识
-(void)customScanCorners
{
CGFloat cWidth = self.bounds.size.width;
CGFloat cHeight = self.bounds.size.height;
NSArray *pointArray = @[@{@"top":[NSValue valueWithCGPoint:CGPointMake(2, 20)],
@"mid":[NSValue valueWithCGPoint:CGPointMake(2, 2)],
@"end":[NSValue valueWithCGPoint:CGPointMake(20, 2)]},
@{@"top":[NSValue valueWithCGPoint:CGPointMake(cWidth - 20, 2)],
@"mid":[NSValue valueWithCGPoint:CGPointMake(cWidth - 2, 2)],
@"end":[NSValue valueWithCGPoint:CGPointMake(cWidth - 2, 20)]},
@{@"top":[NSValue valueWithCGPoint:CGPointMake(cWidth - 2, cHeight - 20)],
@"mid":[NSValue valueWithCGPoint:CGPointMake(cWidth - 2, cHeight - 2)],
@"end":[NSValue valueWithCGPoint:CGPointMake(cWidth - 20, cHeight - 2)]},
@{@"top":[NSValue valueWithCGPoint:CGPointMake(20, cHeight - 2)],
@"mid":[NSValue valueWithCGPoint:CGPointMake(2, cHeight - 2)],
@"end":[NSValue valueWithCGPoint:CGPointMake(2, cHeight - 20)]},];
for (NSInteger i = 0; i < pointArray.count; i++) {
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.lineWidth = 3.0;
shapeLayer.strokeColor = [UIColor greenColor].CGColor;
shapeLayer.fillColor = [UIColor clearColor].CGColor;
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:[pointArray[i][@"top"] CGPointValue]];
[path addLineToPoint:[pointArray[i][@"mid"] CGPointValue]];
[path addLineToPoint:[pointArray[i][@"end"] CGPointValue]];
shapeLayer.path = path.CGPath;
[self.layer addSublayer:shapeLayer];
}
}
用一个UIView作为扫描线,动画部分选择CABaseAnimation(如果有扫描线图片,可以用UIImageView):
@property(nonatomic,strong)UIView *scanView;
//添加扫描线
-(void)customScanLine
{
CGFloat cWidth = self.bounds.size.width;
_scanView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, cWidth, 2)];
_scanView.backgroundColor = [UIColor greenColor];
_scanView.alpha = 0.0;
[self addSubview:_scanView];
}
-(void)startScanAnim
{
if (![_scanView.layer animationForKey:@"ScanAnim"]) {
_scanView.alpha = 1.0;
CGFloat cHeight = self.bounds.size.height;
CABasicAnimation *moveAnimation = [CABasicAnimation animationWithKeyPath:@"transform.translation.y"];
moveAnimation.fromValue = @0;
moveAnimation.toValue = [NSNumber numberWithFloat:cHeight - 2];
moveAnimation.duration = 2.4;
moveAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
CABasicAnimation *fadeInAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"];
fadeInAnimation.fromValue = @0;
fadeInAnimation.toValue = @1;
fadeInAnimation.duration = 0.6;
CABasicAnimation *fadeOutAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"];
fadeOutAnimation.fromValue = @1;
fadeOutAnimation.toValue = @0;
fadeOutAnimation.duration = 0.6;
fadeOutAnimation.beginTime = 1.8;
CAAnimationGroup *group = [CAAnimationGroup animation];
group.animations = @[moveAnimation,fadeInAnimation,fadeOutAnimation];
group.duration = 2.4;
group.repeatCount = HUGE_VALF;
group.removedOnCompletion = NO;
group.fillMode = kCAFillModeForwards;
[_scanView.layer addAnimation:group forKey:@"ScanAnim"];
}
}
-(void)stopScanAnim
{
if ([_scanView.layer animationForKey:@"ScanAnim"]) {
[_scanView.layer removeAllAnimations];
_scanView.alpha = 0.0;
}
}
关于CoreAnimation这里不作详细介绍,感兴趣的同学可以移步 CAAnimation核心动画
添加loading视图:
@property(nonatomic,strong)UIView *loadingView;
@property(nonatomic,strong)UIActivityIndicatorView *actView;
@property(nonatomic,strong)UILabel *loadingLabel;
//添加loading视图
-(void)customLoadingView
{
_loadingView = [[UIView alloc] initWithFrame:self.bounds];
_loadingView.backgroundColor = [UIColor blackColor];
[self addSubview:_loadingView];
_actView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
_actView.center = CGPointMake(self.bounds.size.width / 2, self.bounds.size.height / 2);
[_actView startAnimating];
[self addSubview:_actView];
_loadingLabel = [[UILabel alloc] initWithFrame:CGRectMake(20, self.bounds.size.height / 2 + 25, self.bounds.size.width - 40, 30)];
_loadingLabel.textColor = [UIColor whiteColor];
_loadingLabel.font = [UIFont systemFontOfSize:15];
_loadingLabel.textAlignment = NSTextAlignmentCenter;
_loadingLabel.text = @"处理中,请稍候";
[self addSubview:_loadingLabel];
}
在.h中声明一个属性用来控制扫描线动画以及loading视图的显示:
#import <UIKit/UIKit.h>
@interface JYScanRectView : UIView
@property(nonatomic,getter=isLoading)BOOL loading;
@end
#pragma mark - Override Setter & Getters
-(void)setLoading:(BOOL)loading
{
if (_loading != loading) {
_loading = loading;
_loadingView.alpha = _loading?0.6:0.0;
_actView.alpha = _loading;
_loadingLabel.alpha = _loading;
if (!loading) {
[self startScanAnim];
}
else{
[self stopScanAnim];
}
}
}
回到控制器,创建一个扫描框:
@property(nonatomic,strong)JYScanRectView *jyScanRectView;
@property(nonatomic,strong)UILabel *scanLabel;
//创建扫描框
-(void)setUpRectViewWithRect:(CGRect)scanRect
{
_jyScanRectView = [[JYScanRectView alloc] initWithFrame:scanRect];
[self.view addSubview:_jyScanRectView];
_scanLabel = [[UILabel alloc] initWithFrame:CGRectMake(_jyScanRectView.frame.origin.x, _jyScanRectView.frame.origin.y + _jyScanRectView.frame.size.height + 5, _jyScanRectView.frame.size.width, 30)];
_scanLabel.textColor = [UIColor whiteColor];
_scanLabel.font = [UIFont systemFontOfSize:13];
_scanLabel.textAlignment = NSTextAlignmentCenter;
_scanLabel.text = @"将二维码/条码放入框内,即可自动扫描";
[self.view addSubview:_scanLabel];
}
在-viewDidLoad中,这样进行设置:
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor blackColor];
//self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"相册" style:UIBarButtonItemStylePlain target:self action:@selector(openPhotoLibrary)];
self.navigationItem.title = @"扫一扫";
CGSize cSize = [UIScreen mainScreen].bounds.size;
CGSize scanSize = CGSizeMake(cSize.width * 2.8/4, cSize.width * 2.8/4);
CGRect scanRect = CGRectMake((cSize.width - scanSize.width) / 2, (cSize.height - scanSize.height) / 2, scanSize.width, scanSize.height);
[self setUpRectViewWithRect:scanRect];
_jyScanRectView.loading = YES;
[self.jyQRTool jy_setUpCaptureWithRect:scanRect success:^{
_jyScanRectView.loading = NO;
}];
}
运行一下,效果如图:
扫一扫一般来说,二维码扫描成功获得的是一个字符串类型的URL,我们简单的做一下解析:
#pragma mark - Delegate
-(void)jy_didGetOutputMataDataObjectToString:(NSString *)outPutString
{
//对扫描获得的数据进行处理
[self visitWebViewWithUrl:outPutString];
}
-(void)visitWebViewWithUrl:(NSString *)url
{
WebViewController *webVC = [[WebViewController alloc] init];
webVC.urlStr = url;
[self.navigationController pushViewController:webVC animated:YES];
}
优化一下扫描成功后等待数据处理的界面,声明一个属性用来控制扫码和Loading界面的开关:
@property(nonatomic,getter=isScanActive)BOOL scanActive;
#pragma mark - Override Setter & Getters
//启用/禁用扫描
-(void)setScanActive:(BOOL)scanActive
{
if (_scanActive != scanActive) {
_scanActive = scanActive;
if (_scanActive) {
_jyScanRectView.loading = NO;
[self.jyQRTool jy_startScaning];
}
else{
_jyScanRectView.loading = YES;
[self.jyQRTool jy_stopScaning];
}
}
}
#pragma mark - Delegate
-(void)jy_willGetOutputMataDataObject
{
self.scanActive = NO;
}
在viewWillAppear中,将属性设为YES,恢复扫码功能:
-(void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
self.scanActive = YES;
}
当然这么做以后,第一次进入扫码页面时loading图就不出现了,我们可以在viewDidLoad中将实例变量初始化为YES来解决,注意这里不要使用self.scanActive,直接用实例变量赋值不会调用setter方法:
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor blackColor];
//self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"相册" style:UIBarButtonItemStylePlain target:self action:@selector(openPhotoLibrary)];
self.navigationItem.title = @"扫一扫";
_scanActive = YES;
// ...
}
到了这里,扫码功能算是基本实现了,虽然细节部分讲的比较啰嗦,但还是觉得有必要写出来的。我们运行一下看看最终的效果:
扫一扫播放扫描音效
我们可以使用AudioToolbox这个框架来实现播放自定义音效,基本原理是使用一个soundID将自定义短音频注册到系统声音服务,然后使用这个ID来播放对应的短音频。关于AudioToolbox的详细介绍可以移步 iOS音效播放(AudioToolbox)
具体实现方法如下,在JYQRCodeTool中:
.h
//扫描音效文件名,其值为nil时不播放扫描音,默认为nil
@property(nonatomic,copy)NSString *scanVoiceName;
.m
#import <AudioToolbox/AudioToolbox.h>
//播放扫描音效
- (void)playScanSoundsWithName:(NSString *)soundName
{
if (soundName) {
// 获取音频文件路径
NSURL *url = [[NSBundle mainBundle] URLForResource:soundName withExtension:nil];
// 加载音效文件并创建 SoundID
SystemSoundID soundID = 0;
AudioServicesCreateSystemSoundID((__bridge CFURLRef)url, &soundID);
// 设置播放完成回调
// AudioServicesAddSystemSoundCompletion(soundID, NULL, NULL, soundCompleteCallback, NULL);
// 播放音效
// 带有震动
// AudioServicesPlayAlertSound(_soundID);
// 无振动
AudioServicesPlaySystemSoundWithCompletion(soundID, ^{
AudioServicesDisposeSystemSoundID(soundID);
});
// 销毁 SoundID
// AudioServicesDisposeSystemSoundID(soundID);
}
}
需要注意的是,这里的ID最好使用1000~2000范围外的数字,这个范围内是系统短音效的ID,目前我只知道1007是系统的三全音,其他的ID代表什么大家可以自己去试试看。
需要添加扫描音效时,将音频文件拖入工程,然后在LazyLoad中设置scanVoiceName属性为文件名即可
//LazyLoad
-(JYQRCodeTool *)jyQRTool
{
if (!_jyQRTool) {
_jyQRTool = [JYQRCodeTool toolsWithBindingController:self];
_jyQRTool.scanVoiceName = @"sound.wav";
_jyQRTool.delegate = self;
}
return _jyQRTool;
}
添加闪光灯开关
/**
* 控制闪光灯开关
*
* @param lock 闪光灯开关,YES时开启,NO时关闭,默认为NO
**/
- (void)jy_controlTheFlashLight:(BOOL)lock;
- (void)jy_controlTheFlashLight:(BOOL)lock
{
if (lock) {
AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
NSError *error =nil;
if([device hasTorch]) {
BOOL locked = [device lockForConfiguration:&error];
if(locked) {
device.torchMode= AVCaptureTorchModeOn;
[device unlockForConfiguration];
}
}
}
else{
AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
if([device hasTorch]) {
[device lockForConfiguration:nil];
[device setTorchMode:AVCaptureTorchModeOff];
[device unlockForConfiguration];
}
}
}
二维码生成/自定义颜色(iOS7.0以上可用)
二维码生成这部分参考了别人的文章,基本上完全沿用了作者的代码及思路,根据自己的需求稍作修改,原文地址 https://blog.yourtion.com/custom-cifilter-qrcode-generator.html
iOS7以后,CoreImage框架中有一个CIFilter类可以帮助我们将数据转化为二维码,实现过程很简单。由于二维码生成这部分不需要用到控制器也不需要工具类中的任何属性,我们可以写成类方法封装进JYQRCodeTool中:
/**
* 将字符串转成二维码
*
* @param string 字符串
* @param size 二维码图片尺寸
*
* @return 二维码图片
**/
+ (UIImage *)jy_createQRCodeWithString:(NSString *)string size:(CGFloat)size;
+ (UIImage *)jy_createQRCodeWithString:(NSString *)string size:(CGFloat)size
{
return [self createNonInterpolatedUIImageFormCIImage:[self createQRForString:string] withSize:size];
}
//生成二维码
+ (CIImage *)createQRForString:(NSString *)qrString {
NSData *stringData = [qrString dataUsingEncoding:NSUTF8StringEncoding];
// 创建filter
CIFilter *qrFilter = [CIFilter filterWithName:@"CIQRCodeGenerator"];
// 设置内容和纠错级别
[qrFilter setValue:stringData forKey:@"inputMessage"];
[qrFilter setValue:@"M" forKey:@"inputCorrectionLevel"];
// 返回CIImage
return qrFilter.outputImage;
}
//缩放二维码尺寸
+ (UIImage *)createNonInterpolatedUIImageFormCIImage:(CIImage *)image withSize:(CGFloat) size {
CGRect extent = CGRectIntegral(image.extent);
CGFloat scale = MIN(size/CGRectGetWidth(extent), size/CGRectGetHeight(extent));
// 创建bitmap;
size_t width = CGRectGetWidth(extent) * scale;
size_t height = CGRectGetHeight(extent) * scale;
CGColorSpaceRef cs = CGColorSpaceCreateDeviceGray();
CGContextRef bitmapRef = CGBitmapContextCreate(nil, width, height, 8, 0, cs, (CGBitmapInfo)kCGImageAlphaNone);
CIContext *context = [CIContext contextWithOptions:nil];
CGImageRef bitmapImage = [context createCGImage:image fromRect:extent];
CGContextSetInterpolationQuality(bitmapRef, kCGInterpolationNone);
CGContextScaleCTM(bitmapRef, scale, scale);
CGContextDrawImage(bitmapRef, extent, bitmapImage);
// 保存bitmap到图片
CGImageRef scaledImage = CGBitmapContextCreateImage(bitmapRef);
CGContextRelease(bitmapRef);
CGImageRelease(bitmapImage);
return [UIImage imageWithCGImage:scaledImage];
}
可以看到生成的部分有两步,第一步将NSString转成data,然后转成CIImage,由于CIImage直接转成UIImage后会出现失真的情况,画面会变模糊,所以还需要根据imageView的尺寸进行缩放,缩放部分用到了Quartz2D相关知识点,这里暂不去深究。
如果想改变二维码的颜色,可以用下面的方法,以1个像素为单位,遍历图片中所有的像素点,将黑色改为设置的颜色,将白色改为透明,改成透明的好处是,我们可以为二维码添加自定义背景及边框,类似微信中的二维码样式:
/**
* 自定义二维码颜色
*
* @param qrImage 二维码图片
* @param red RGB通道-R
* @param green RGB通道-G
* @param blue RGB通道-B
*
* @return 二维码图片
**/
+ (UIImage *)jy_customQRCodeWithImage:(UIImage *)qrImage colorWithRed:(CGFloat)red andGreen:(CGFloat)green andBlue:(CGFloat)blue;
+ (UIImage *)jy_customQRCodeWithImage:(UIImage *)qrImage colorWithRed:(CGFloat)red andGreen:(CGFloat)green andBlue:(CGFloat)blue
{
return [self imageBlackToTransparent:qrImage withRed:red andGreen:green andBlue:blue];
}
//颜色填充
void ProviderReleaseData (void *info, const void *data, size_t size){
free((void*)data);
}
+ (UIImage*)imageBlackToTransparent:(UIImage*)image withRed:(CGFloat)red andGreen:(CGFloat)green andBlue:(CGFloat)blue{
const int imageWidth = image.size.width;
const int imageHeight = image.size.height;
size_t bytesPerRow = imageWidth * 4;
uint32_t *rgbImageBuf = (uint32_t *)malloc(bytesPerRow *imageHeight);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = CGBitmapContextCreate(rgbImageBuf, imageWidth, imageHeight, 8, bytesPerRow, colorSpace,kCGBitmapByteOrder32Little | kCGImageAlphaNoneSkipLast);
CGContextDrawImage(context, CGRectMake(0, 0, imageWidth, imageHeight), image.CGImage);
// 遍历像素
int pixelNum = imageWidth * imageHeight;
uint32_t *pCurPtr = rgbImageBuf;
for (int i = 0; i < pixelNum; i++, pCurPtr++){
if ((*pCurPtr & 0xFFFFFF00) < 0x99999900) // 将白色变成透明
{
// 改成下面的代码,会将图片转成想要的颜色
uint8_t* ptr = (uint8_t*)pCurPtr;
ptr[3] = red; //0~255
ptr[2] = green;
ptr[1] = blue;
}
else
{
uint8_t* ptr = (uint8_t*)pCurPtr;
ptr[0] = 0;
}
}
// 输出图片
CGDataProviderRef dataProvider = CGDataProviderCreateWithData(NULL, rgbImageBuf, bytesPerRow * imageHeight, ProviderReleaseData);
CGImageRef imageRef = CGImageCreate(imageWidth, imageHeight, 8, 32, bytesPerRow, colorSpace,
kCGImageAlphaLast | kCGBitmapByteOrder32Little, dataProvider,
NULL, true, kCGRenderingIntentDefault);
CGDataProviderRelease(dataProvider);
UIImage* resultUIImage = [UIImage imageWithCGImage:imageRef];
// 清理空间
CGImageRelease(imageRef);
CGContextRelease(context);
CGColorSpaceRelease(colorSpace);
return resultUIImage;
}
为生成的二维码添加Logo
添加Logo这部分依然要用到绘图的相关知识,大致的思路是,先对Logo图片作圆角边框处理,然后使用绘图将生成的二维码与Logo画到一张画布上,最后输出画好的图片。需要注意的是,Logo需要放在中心位置,大小不要超过二维码尺寸的1/4,只要在这个范围内,基本上不会影响二维码的识别。具体代码如下:
/**
* 给二维码添加Logo
*
* @param qrImage 二维码图片
* @param avatarImage Logo图片
* @param ratio Logo圆角比例(0~1)0为无圆角,1为圆形
*
* @return 二维码图片
**/
+ (UIImage *)jy_customQRCodeWithImage:(UIImage *)qrImage addAvatarImage:(UIImage *)avatarImage cornerRatio:(CGFloat)ratio;
+ (UIImage *)jy_customQRCodeWithImage:(UIImage *)qrImage addAvatarImage:(UIImage *)avatarImage cornerRatio:(CGFloat)ratio
{
return [self imagewithQRImage:qrImage addAvatarImage:avatarImage ofTheSize:qrImage.size cornerRatio:ratio];
}
//添加logo
+ (UIImage *)imagewithQRImage:(UIImage *)qrImage addAvatarImage:(UIImage *)avatarImage ofTheSize:(CGSize)size cornerRatio:(CGFloat)ratio
{
if (!avatarImage) {
return qrImage;
}
BOOL opaque = 0.0;
// 获取当前设备的scale
CGFloat scale = [UIScreen mainScreen].scale;
// 创建画布Rect
CGRect qrRect = CGRectMake(0, 0, size.width, size.height);
// 头像大小 _不能大于_ 画布的1/4 (这个大小之内的不会遮挡二维码的有效信息)
CGFloat avatarWidth = (size.width/5.0);
CGFloat avatarHeight = avatarWidth;
//调用一个新的切割绘图方法 crop image add cornerRadius (裁切头像图片为圆角,并添加bored 返回一个newimage)
avatarImage = [self clipCornerRadius:avatarImage withSize:CGSizeMake(avatarWidth, avatarHeight) cornerRatio:ratio];
// 设置头像的位置信息
CGPoint position = CGPointMake(size.width/2.0, size.height/2.0);
CGRect avatarRect = CGRectMake(position.x-(avatarWidth/2.0), position.y-(avatarHeight/2.0), avatarWidth, avatarHeight);
// 设置画布信息
UIGraphicsBeginImageContextWithOptions(size, opaque, scale);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);{// 开启画布
// 翻转context (画布)
CGContextTranslateCTM(context, 0, size.height);
CGContextScaleCTM(context, 1, -1);
// 根据 bgRect 用二维码填充视图
CGContextDrawImage(context, qrRect, qrImage.CGImage);
// 根据newAvatarImage 填充头像区域
CGContextDrawImage(context, avatarRect, avatarImage.CGImage);
}CGContextRestoreGState(context);// 提交画布
// 从画布中提取图片
UIImage *resultImage = UIGraphicsGetImageFromCurrentImageContext();
// 释放画布
UIGraphicsEndImageContext();
return resultImage;
}
//logo圆角设置
+ (UIImage *)clipCornerRadius:(UIImage *)image withSize:(CGSize)size cornerRatio:(CGFloat)ratio
{
// 白色border的宽度
CGFloat outerWidth = size.width/15.0;
// 黑色border的宽度
CGFloat innerWidth = outerWidth/10.0;
// 根据传入的ratio,设置圆角
CGFloat corenerRadius = size.width/2.0 * ratio;
// 为context创建一个区域
CGRect areaRect = CGRectMake(0, 0, size.width, size.height);
UIBezierPath *areaPath = [UIBezierPath bezierPathWithRoundedRect:areaRect byRoundingCorners:UIRectCornerAllCorners cornerRadii:CGSizeMake(corenerRadius, corenerRadius)];
// 因为UIBezierpath划线是双向扩展的 初始位置就不会是(0,0)
// path的位置就应该是你画的宽度的中间, 这个需要自己手动计算一下。
// origin position
CGFloat outerOrigin = outerWidth/2.0;
CGFloat innerOrigin = innerWidth/2.0 + outerOrigin/1.2;
CGRect outerRect = CGRectInset(areaRect, outerOrigin, outerOrigin);
CGRect innerRect = CGRectInset(outerRect, innerOrigin, innerOrigin);
// 要进行rect之间的计算,我想 "CGRectInset" 是一个不错的选择。
// 外层path
UIBezierPath *outerPath = [UIBezierPath bezierPathWithRoundedRect:outerRect byRoundingCorners:UIRectCornerAllCorners cornerRadii:CGSizeMake(outerRect.size.width/2.0 * ratio, outerRect.size.width/2.0 * ratio)];
// 内层path
UIBezierPath *innerPath = [UIBezierPath bezierPathWithRoundedRect:innerRect byRoundingCorners:UIRectCornerAllCorners cornerRadii:CGSizeMake(innerRect.size.width/2.0 * ratio, innerRect.size.width/2.0 * ratio)];
// 要保证"内外层"的吻合,那就要进行比例相等,就能达到形状的完全匹配
// 创建上下文
UIGraphicsBeginImageContextWithOptions(size, NO, [UIScreen mainScreen].scale);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);{
// 翻转context
CGContextTranslateCTM(context, 0, size.height);
CGContextScaleCTM(context, 1, -1);
// context 添加 区域path -> 进行裁切画布
CGContextAddPath(context, areaPath.CGPath);
CGContextClip(context);
// context 添加 背景颜色,避免透明背景会展示后面的二维码不美观的。(当然也可以对想遮住的区域进行clear操作,但是我当时写的时候还没有想到)
CGContextAddPath(context, areaPath.CGPath);
UIColor *fillColor = [UIColor colorWithRed:0.85 green:0.85 blue:0.85 alpha:1];
CGContextSetFillColorWithColor(context, fillColor.CGColor);
CGContextFillPath(context);
// context 执行画头像
CGContextDrawImage(context, innerRect, image.CGImage);
// context 添加白色的边框 -> 执行填充白色画笔
CGContextAddPath(context, outerPath.CGPath);
CGContextSetStrokeColorWithColor(context, [UIColor whiteColor].CGColor);
CGContextSetLineWidth(context, outerWidth);
CGContextStrokePath(context);
// context 添加黑色的边界 -> 执行填充黑色画笔
CGContextAddPath(context, innerPath.CGPath);
CGContextSetStrokeColorWithColor(context, [UIColor blackColor].CGColor);
CGContextSetLineWidth(context, innerWidth);
CGContextStrokePath(context);
}CGContextRestoreGState(context);
UIImage *radiusImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return radiusImage;
}
然后创建一个生成二维码的页面,具体代码就不贴出来了
生成二维码时,这样调用:
//生成二维码
-(void)generateQRCode
{
[self controlBtnsEnabled:NO];
[self resignTextFields];
//根据view尺寸将字符串转为二维码
UIImage *qrImage = [JYQRCodeTool jy_createQRCodeWithString:_textField.text size:_qrCodeView.bounds.size.width];
NSArray <NSNumber *> *colorArr = @[[NSNumber numberWithFloat:_redField.text.floatValue],
[NSNumber numberWithFloat:_greenField.text.floatValue],
[NSNumber numberWithFloat:_blueField.text.floatValue]];
//获取到RGB参数,根据颜色改变view背景色,否则会因为色差过小的问题导致无法识别
BOOL isDarkBG = (colorArr[1].floatValue > 200);
_qrCodeView.backgroundColor = isDarkBG?[UIColor blackColor]:[UIColor whiteColor];
//为生成的二维码添加颜色
qrImage = [JYQRCodeTool jy_customQRCodeWithImage:qrImage colorWithRed:colorArr[0].floatValue andGreen:colorArr[1].floatValue andBlue:colorArr[2].floatValue];
if (_logoSwitch.isOn) {
//为生成的二维码添加Logo
qrImage = [JYQRCodeTool jy_customQRCodeWithImage:qrImage addAvatarImage:[UIImage imageNamed:@"logo"] cornerRatio:_logoSlider.value];
}
_qrCodeView.image = qrImage;
}
效果图:
生成二维码
关于二维码阴影
一种非常简单的设置二维码阴影的方式就是给二维码所在的UIImageView加阴影:
_qrCodeView.layer.shadowColor = [UIColor grayColor].CGColor;
_qrCodeView.layer.shadowRadius = 1.5;
_qrCodeView.layer.shadowOpacity = 1.0;
_qrCodeView.layer.shadowOffset = CGSizeMake(0, 0);
需要注意的是,imageView的背景色要设置成透明才有效果,因为CALayer是根据内容来绘制阴影的,包括边框和背景色,如果背景色不为透明,绘制出的阴影只有边框外一圈。
二维码阴影需要保存带有阴影的二维码图片时,需要将view范围内的部分截图进行保存,因为阴影不属于图片上的一部分,直接保存view上的图片是没有阴影效果的。
-(void)savePhoto
{
UIImage *image = [self clipImageFromView:_qrBGView];
UIImageWriteToSavedPhotosAlbum(image, self, @selector(image:didFinishSavingWithError:contextInfo:), nil);
}
-(UIImage *)clipImageFromView:(UIView *)view
{
UIGraphicsBeginImageContextWithOptions(view.bounds.size, NO, 0.0);
CGContextRef context = UIGraphicsGetCurrentContext();
[view.layer renderInContext:context];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
二维码识别(iOS8.0以上可用)
二维码识别部分比较简单,直接给出代码:
/**
* 识别图片中的二维码
*
* @param sourceImage 需要识别的图片
*
* @return 识别获得的数据
**/
+ (NSString *)jy_detectorQRCodeWithSourceImage:(UIImage *)sourceImage;
+(NSString *)jy_detectorQRCodeWithSourceImage:(UIImage *)sourceImage
{
return [self detectorQRCodeImageWithSourceImage:sourceImage];
}
+ (NSString *)detectorQRCodeImageWithSourceImage:(UIImage *)sourceImage
{
// 0.创建上下文
CIContext *context = [[CIContext alloc] init];
// 1.创建一个探测器
CIDetector *detector = [CIDetector detectorOfType:CIDetectorTypeQRCode context:context options:@{CIDetectorAccuracy: CIDetectorAccuracyLow}];
// 2.直接开始识别图片,获取图片特征
CIImage *imageCI = [[CIImage alloc] initWithImage:sourceImage];
NSArray <CIFeature *> *features = [detector featuresInImage:imageCI];
// 3.读取特征
CIFeature *feature = features.firstObject;
NSString *msgString = nil;
if ([feature isKindOfClass:[CIQRCodeFeature class]]) {
CIQRCodeFeature *tempFeature = (CIQRCodeFeature *)feature;
msgString = tempFeature.messageString;
}
// 4.传递数据给外界
return msgString;
}
需要识别app中的图片时,可以这样调用:
-(void)readQRCode
{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
UIImage *qrImage = [self clipImageFromView:_qrBGView];
NSString *urlStr = [JYQRCodeTool jy_detectorQRCodeWithSourceImage:qrImage];
NSLog(@"%@",urlStr);
if (!urlStr) {
UIImage *scaleImage = [JYQRCodeTool jy_getImage:qrImage scaleToSize:CGSizeMake(200, 200)];
urlStr = [JYQRCodeTool jy_detectorQRCodeWithSourceImage:scaleImage];
}
dispatch_async(dispatch_get_main_queue(), ^{
//对识别出的数据进行处理
if (urlStr) {
WebViewController *webVC = [[WebViewController alloc] init];
webVC.urlStr = urlStr;
[self.navigationController pushViewController:webVC animated:YES];
}
else{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"结果" message:@"未识别到有效信息" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
[alert show];
}
});
});
}
识别相册中的图片同理,不过需要注意的是,图片中二维码的大小可能会影响识别,测试中发现有一些图片无法识别,主要都是因为二维码尺寸太大,将尺寸改小之后又可以识别了,而有些图片原本可以识别,改小后又不能识别了。于是这里做了2次识别处理,先识别原图,如果无法识别,将图片改小后再识别一次,如果还是没有,直接返回nil。
识别相册中的二维码:
-(void)openPhotoLibrary
{
if (![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypePhotoLibrary])
{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"照片权限已关闭" message:@"请到设置->隐私->照片中,允许app访问相册" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
[alert show];
}
else{
//开启相册前禁用扫码,防止相机扫到二维码后异常跳转
[self.jyQRTool jy_stopScaning];
UIImagePickerController *pickerCtr = [[UIImagePickerController alloc] init];
pickerCtr.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
pickerCtr.delegate = self;
[self presentViewController:pickerCtr animated:YES completion:nil];
}
}
-(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info
{
self.callBackFromPhoto = YES;
self.scanActive = NO;
[picker dismissViewControllerAnimated:YES completion:^{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
UIImage *pickImage = [info objectForKey:UIImagePickerControllerOriginalImage];
NSString *urlStr = [JYQRCodeTool jy_detectorQRCodeWithSourceImage:pickImage];
if (!urlStr) {
UIImage *scaleImage = [JYQRCodeTool jy_getImage:pickImage scaleToSize:CGSizeMake(200, 200)];
urlStr = [JYQRCodeTool jy_detectorQRCodeWithSourceImage:scaleImage];
}
dispatch_async(dispatch_get_main_queue(), ^{
//对获得的数据进行处理
if (urlStr) {
[self visitWebViewWithUrl:urlStr];
}
else{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"结果" message:@"未识别到有效信息" delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil];
[alert show];
}
});
});
}];
}
-(void)imagePickerControllerDidCancel:(UIImagePickerController *)picker
{
[picker dismissViewControllerAnimated:YES completion:^{
//启用扫码
[self.jyQRTool jy_startScaning];
}];
}
-(void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex
{
self.scanActive = YES;
}
由于相册入口放在扫码界面中,最后我们需要针对相册弹出及返回时UI的刷新逻辑做一下优化:
@property(nonatomic,getter=isCallBackFromPhoto)BOOL callBackFromPhoto;
-(void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
//如果不是从相册选择了图片返回,启用扫码功能,否则显示loading
if (!self.isCallBackFromPhoto) {
self.scanActive = YES;
}
else{
self.callBackFromPhoto = NO;
}
}
在成功获取图片的delegate中(-imagePickerController: didFinishPickingMediaWithInfo:),设置self.callBackFromPhoto = YES。
最终效果图:
生成二维码 扫一扫如果不考虑对iOS8以下做适配,那么以上内容应该足够满足app中二维码扫描的需求了。
demo地址 https://github.com/JiYuwei/QRCodeDemo
彩蛋:写完本文后没事翻阅了别人的文章,找到这么一篇 你的二维码那么酷,我一定是用了假二维码
其中关于二维码美化的部分看的我眼花缭乱 & 头皮发麻。。。
显然这个时代,原始的黑白二维码已经无法满足人们的审美需求了。
关于这些二维码的美化原理,我现在是毫无头绪。也许在将来,如果有时间和精力的话,会去尝试破解这些谜题。各位看官如果有什么想法,欢迎在下方留言讨论。
网友评论