iOS开发-实现二维码扫一扫Scan及识别图片中二维码功能
在iOS开发中,会遇到扫一扫功能,扫一扫是使用摄像头扫码二维码或者条形码,获取对应二维码或条形码内容字符串。通过获得的字符串进行跳转或者打开某个页面开启下一步的业务逻辑。
https://blog.csdn.net/gloryFlow/article/details/132249830
一、使用前权限设置
扫一扫功能需要开启相机权限,需要在info.plist文件中添加NSCameraUsageDescription
例如:
<key>NSCameraUsageDescription</key>
<string>开启相机权限,活动扫一扫更快捷</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>开启定位权限</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>开启定位权限</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>开启定位权限</string>
<key>NSMicrophoneUsageDescription</key>
<string>开启麦克风权限</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>添加照片需要您的同意</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>开启照片权限</string>
这里还有其他权限,暂时扫一扫只需要NSCameraUsageDescription。
二、AVCaptureSession扫一扫功能
2.1 需要了解的几个类
- AVCaptureSession
AVCaptureSession是iOS提供的一个管理和协调输入设备到输出设备之间数据流的对象。
- AVCaptureDevice
AVCaptureDevice是指硬件设备。
- AVCaptureDeviceInput
AVCaptureDeviceInput是用来从AVCaptureDevice对象捕获Input数据。
- AVCaptureMetadataOutput
AVCaptureMetadataOutput是用来处理AVCaptureSession产生的定时元数据的捕获输出的。
- AVCaptureVideoDataOutput
AVCaptureVideoDataOutput是用来处理视频数据输出的。
- AVCaptureVideoPreviewLayer
AVCaptureVideoPreviewLayer是相机捕获的视频预览层,是用来展示视频的。
2.2 实现扫一扫功能
在熟悉几个类之后,我们可以初始化session了。
我们为AVCaptureSession添加功能实现所需要的input与output
/** 创建扫描器 */
- (void)loadScanView {AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice:device error:nil];AVCaptureMetadataOutput *metadataOutput = [[AVCaptureMetadataOutput alloc] init];[metadataOutput setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];if (self.scanConfig.scannerArea == SDScannerAreaDefault) {metadataOutput.rectOfInterest = CGRectMake([self.scannerView scannerOriginX]/self.view.frame.size.height, [self.scannerView scannerOriginY]/self.view.frame.size.width, [self.scannerView scannerWidth]/self.view.frame.size.height, [self.scannerView scannerWidth]/self.view.frame.size.width);}AVCaptureVideoDataOutput *videoDataOutput = [[AVCaptureVideoDataOutput alloc] init];[videoDataOutput setSampleBufferDelegate:self queue:dispatch_get_main_queue()];self.session = [[AVCaptureSession alloc]init];[self.session setSessionPreset:AVCaptureSessionPresetHigh];if ([self.session canAddInput:deviceInput]) {[self.session addInput:deviceInput];}if ([self.session canAddOutput:metadataOutput]) {[self.session addOutput:metadataOutput];}if ([self.session canAddOutput:videoDataOutput]) {[self.session addOutput:videoDataOutput];}metadataOutput.metadataObjectTypes = [SDQrScanTool metadataObjectType:self.scanConfig.scannerType];AVCaptureVideoPreviewLayer *videoPreviewLayer = [AVCaptureVideoPreviewLayer layerWithSession:_session];videoPreviewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;videoPreviewLayer.frame = self.view.layer.bounds;[self.videoPreView.layer insertSublayer:videoPreviewLayer atIndex:0];[self.session startRunning];
}
AVCaptureMetadataOutput实现了方法,设置了delegate
[metadataOutput setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];
实现AVCaptureMetadataOutputObjectsDelegate的代理方法
- (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection;
该方法每当输出捕获并发出新对象时,委托都会收到此消息,获得对应metadataObjectTypes。通过此方法,我们可以当扫描二维码时候获得对应的结果。
#pragma mark -- AVCaptureMetadataOutputObjectsDelegate
- (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection {// 获取扫一扫结果if (metadataObjects && metadataObjects.count > 0) {[self pauseScanning];AVMetadataMachineReadableCodeObject *metadataObject = metadataObjects[0];NSString *stringValue = metadataObject.stringValue;//AVMetadataMachineReadableCodeObject *obj = (AVMetadataMachineReadableCodeObject *)[self.lay transformedMetadataObjectForMetadataObject:metadataObjects.lastObject];//[self changeVideoScale:metadataObject];[self handleScanValue:stringValue];}
}
在AVCaptureVideoDataOutput实现了
[videoDataOutput setSampleBufferDelegate:self queue:dispatch_get_main_queue()];
这里实现了代理AVCaptureVideoDataOutputSampleBufferDelegate中方法
- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection;
当捕获新的视频采样缓冲区时,将使用captureOutput:didOutputSampleBuffer:fromConnection:delegate方法将其提供给采样缓冲区代理。
在此方法中,可以观察亮度值,决定是否需要开启灯光。
#pragma mark -- AVCaptureVideoDataOutputSampleBufferDelegate
/** 此方法会实时监听亮度值 */
- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {CFDictionaryRef metadataDict = CMCopyDictionaryOfAttachments(NULL,sampleBuffer, kCMAttachmentMode_ShouldPropagate);NSDictionary *metadata = [[NSMutableDictionary alloc] initWithDictionary:(__bridge NSDictionary*)metadataDict];CFRelease(metadataDict);NSDictionary *exifMetadata = [[metadata objectForKey:(NSString *)kCGImagePropertyExifDictionary] mutableCopy];// 亮度值float brightnessValue = [[exifMetadata objectForKey:(NSString *)kCGImagePropertyExifBrightnessValue] floatValue];if (![self.scannerView flashlightOn]) {if (brightnessValue < -1.0) {[self.scannerView showFlashlight:YES];} else {[self.scannerView hideFlashlight:YES];}}
}
2.3 设置metadataOutput的metadataObjectTypes
扫一扫时候,我们需要设置metadataOutput的metadataObjectTypes,决定扫描的二维码、条形码等。
/** 根据扫描器类型配置支持编码格式 */
+ (NSArray *)metadataObjectType:(SDScannerType)scannerType {switch (scannerType) {case SDScannerTypeQRCode:{return @[AVMetadataObjectTypeQRCode];}break;case SDScannerTypeBarCode:{return @[AVMetadataObjectTypeEAN13Code,AVMetadataObjectTypeEAN8Code,AVMetadataObjectTypeUPCECode,AVMetadataObjectTypeCode39Code,AVMetadataObjectTypeCode39Mod43Code,AVMetadataObjectTypeCode93Code,AVMetadataObjectTypeCode128Code,AVMetadataObjectTypePDF417Code];}break;case SDScannerTypeBoth:{return @[AVMetadataObjectTypeQRCode,AVMetadataObjectTypeEAN13Code,AVMetadataObjectTypeEAN8Code,AVMetadataObjectTypeUPCECode,AVMetadataObjectTypeCode39Code,AVMetadataObjectTypeCode39Mod43Code,AVMetadataObjectTypeCode93Code,AVMetadataObjectTypeCode128Code,AVMetadataObjectTypePDF417Code];}break;default:break;}
}
根据扫描器类型配置支持编码格式,我们根据不同类型进行设置。
2.4 扫描开启与关闭
扫一扫使用了摄像头,这里关闭开启需要通过AVCaptureSession来控制
恢复扫一扫功能
/**恢复扫一扫功能*/
- (void)resumeScanning {if (self.session) {[self.session startRunning];[self.scannerView startLineAnimation];}
}
暂停扫一扫
/**暂停扫一扫*/
- (void)pauseScanning {if (self.session) {[self.session stopRunning];[self.scannerView stopLineAnimation];}
}
2.5 开启扫一扫或者打开相册的权限检查
- 校验是否有相机权限
+ (void)checkCameraAuthorizationStatus:(void(^)(BOOL granted))permissionGranted
{AVAuthorizationStatus videoAuthStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];switch (videoAuthStatus) {// 已授权case AVAuthorizationStatusAuthorized:{permissionGranted(YES);}break;// 未询问用户是否授权case AVAuthorizationStatusNotDetermined:{// 提示用户授权[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {permissionGranted(granted);}];}break;// 用户拒绝授权或权限受限case AVAuthorizationStatusRestricted:case AVAuthorizationStatusDenied:{UIAlertView *alert = [[UIAlertView alloc]initWithTitle:@"请在”设置-隐私-相机”选项中,允许访问你的相机" message:nil delegate:nil cancelButtonTitle:@"确定" otherButtonTitles:nil];[alert show];permissionGranted(NO);}break;default:break;}
}
- 校验是否有相册权限
/** 校验是否有相册权限 */
+ (void)checkAlbumAuthorizationStatus:(void(^)(BOOL granted))permissionGranted {PHAuthorizationStatus photoAuthStatus = [PHPhotoLibrary authorizationStatus];switch (photoAuthStatus) {// 已授权case PHAuthorizationStatusAuthorized:{permissionGranted(YES);}break;// 未询问用户是否授权case PHAuthorizationStatusNotDetermined:{[PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {permissionGranted(status == PHAuthorizationStatusAuthorized);}];}break;// 用户拒绝授权或权限受限case PHAuthorizationStatusRestricted:case PHAuthorizationStatusDenied:{UIAlertView *alert = [[UIAlertView alloc]initWithTitle:@"请在”设置-隐私-相片”选项中,允许访问你的相册" message:nil delegate:nil cancelButtonTitle:@"确定" otherButtonTitles:nil];[alert show];permissionGranted(NO);}break;default:break;}}
2.6 扫描线条动画
实现扫一扫功能,我们需要实现一下扫一扫的动画效果,看起来界面更加提升用户体验。
我们使用基础动画CABasicAnimation来实现扫描线条的动画效果。
- 开启动画
/** 添加扫描线条动画 */
- (void)startLineAnimation {// 若已添加动画,则先移除动画再添加[self.scannerLineView.layer removeAllAnimations];CABasicAnimation *lineAnimation = [CABasicAnimation animationWithKeyPath:@"transform"];lineAnimation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeTranslation(0, [self scannerWidth] - kScannerLineHeight, 1)];lineAnimation.duration = 4;lineAnimation.repeatCount = MAXFLOAT;lineAnimation.autoreverses = YES; // 动画结束时执行逆动画[self.scannerLineView.layer addAnimation:lineAnimation forKey:scannerLineViewAnmationKey];// 重置动画运行速度为1.0self.scannerLineView.layer.speed = 2.0;
}
- 暂停动画
/** 暂停扫描器动画 */
- (void)stopLineAnimation {// 取出当前时间,转成动画暂停的时间CFTimeInterval pauseTime = [self.scannerLineView.layer convertTime:CACurrentMediaTime() fromLayer:nil];// 设置动画的时间偏移量,指定时间偏移量的目的是让动画定格在该时间点的位置self.scannerLineView.layer.timeOffset = pauseTime;// 将动画的运行速度设置为0, 默认的运行速度是1.0self.scannerLineView.layer.speed = 0;
}
2.7 界面的其他手电筒操作
在我们需要开启和关闭手电筒,比如在比较黑暗的时候开启手电筒,在有光亮的地方关闭手电筒。
- 显示手电筒
/** 显示手电筒 */
- (void)showFlashlight:(BOOL)animated {if (animated) {[UIView animateWithDuration:0.6 animations:^{self.lightTipsLabel.alpha = 1.0;self.lightButton.alpha = 1.0;self.tipsLabel.alpha = 0;} completion:^(BOOL finished) {self.lightButton.enabled = YES;}];} else {self.lightTipsLabel.alpha = 1.0;self.lightButton.alpha = 1.0;self.tipsLabel.alpha = 0;self.lightButton.enabled = YES;}
}
- 藏手电筒
/** 隐藏手电筒 */
- (void)hideFlashlight:(BOOL)animated {self.lightButton.enabled = NO;if (animated) {[UIView animateWithDuration:0.6 animations:^{self.lightTipsLabel.alpha = 0;self.lightButton.alpha = 0;self.tipsLabel.alpha = 1.0;} completion:^(BOOL finished) {}];} else {self.tipsLabel.alpha = 1.0;self.lightTipsLabel.alpha = 0;self.lightButton.alpha = 0;}
}
2.8 绘制扫描区域
通过UIBezierPath绘制出扫描区域,扫描区域之外的则显示透明度为0.7的黑色遮罩效果。
/**draw绘制@param rect rect*/
- (void)drawRect:(CGRect)rect {[super drawRect:rect];// 半透明区域[[UIColor colorWithWhite:0 alpha:0.7] setFill];UIRectFill(rect);// 透明区域CGRect scanner_rect = CGRectMake([self scannerOriginX], [self scannerOriginY], [self scannerWidth], [self scannerWidth]);[[UIColor clearColor] setFill];UIRectFill(scanner_rect);// 边框UIBezierPath *borderPath = [UIBezierPath bezierPathWithRect:CGRectMake([self scannerOriginX], [self scannerOriginY], [self scannerWidth], [self scannerWidth])];borderPath.lineCapStyle = kCGLineCapRound;borderPath.lineWidth = kScannerBorderWidth;[self.config.scannerBorderColor set];[borderPath stroke];for (int index = 0; index < 4; ++index) {UIBezierPath *tempPath = [UIBezierPath bezierPath];tempPath.lineWidth = kScannerCornerWidth;[self.config.scannerCornerColor set];switch (index) {// 左上角棱角case 0:{[tempPath moveToPoint:CGPointMake([self scannerOriginX] + kScannerCornerLength, [self scannerOriginY])];[tempPath addLineToPoint:CGPointMake([self scannerOriginX], [self scannerOriginY])];[tempPath addLineToPoint:CGPointMake([self scannerOriginX], [self scannerOriginY] + kScannerCornerLength)];}break;// 右上角case 1:{[tempPath moveToPoint:CGPointMake([self scannerOriginX] + [self scannerWidth] - kScannerCornerLength, [self scannerOriginY])];[tempPath addLineToPoint:CGPointMake([self scannerOriginX] + [self scannerWidth], [self scannerOriginY])];[tempPath addLineToPoint:CGPointMake([self scannerOriginX] + [self scannerWidth], [self scannerOriginY] + kScannerCornerLength)];}break;// 左下角case 2:{[tempPath moveToPoint:CGPointMake([self scannerOriginX], [self scannerOriginY] + [self scannerWidth] - kScannerCornerLength)];[tempPath addLineToPoint:CGPointMake([self scannerOriginX], [self scannerOriginY] + [self scannerWidth])];[tempPath addLineToPoint:CGPointMake([self scannerOriginX] + kScannerCornerLength, [self scannerOriginY] + [self scannerWidth])];}break;// 右下角case 3:{[tempPath moveToPoint:CGPointMake([self scannerOriginX] + [self scannerWidth] - kScannerCornerLength, [self scannerOriginY] + [self scannerWidth])];[tempPath addLineToPoint:CGPointMake([self scannerOriginX] + [self scannerWidth], [self scannerOriginY] + [self scannerWidth])];[tempPath addLineToPoint:CGPointMake([self scannerOriginX] + [self scannerWidth], [self scannerOriginY] + [self scannerWidth] - kScannerCornerLength)];}break;default:break;}[tempPath stroke];}
}
三、实现打开相册,识别图片二维码
我们打开相册,识别相册中图片的二维码,识别图片中的二维码需要CIDetector。
具体代码如下
#pragma mark -- UIImagePickerControllerDelegate
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info
{UIImage *pickImage = info[UIImagePickerControllerOriginalImage];CIDetector *detector = [CIDetector detectorOfType:CIDetectorTypeQRCode context:nil options:@{CIDetectorAccuracy: CIDetectorAccuracyHigh}];// 获取选择图片中识别结果NSArray *features = [detector featuresInImage:[CIImage imageWithData:UIImagePNGRepresentation(pickImage)]];[picker dismissViewControllerAnimated:YES completion:^{if (features.count > 0) {CIQRCodeFeature *feature = features[0];NSString *stringValue = feature.messageString;[self handleScanValue:stringValue];} else {[self readFromAlbumFailed];}}];
}
四、实现扫一扫及识别图片中二维码的全部代码
实现扫一扫及识别图片中二维码的全部代码,主要是
SDQrScanViewController:UIViewController
SDQrScanView:UIView显示界面
SDQrScanTool:扫一扫权限及设置
SDQrScanConfig:扫一扫边框颜色等
完整代码如下
SDQrScanConfig.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "SDQrScanConfig.h"/**扫描类型*/
typedef NS_ENUM(NSInteger, SDScannerType) {SDScannerTypeQRCode,SDScannerTypeBarCode,SDScannerTypeBoth,
};/**扫描区域*/
typedef NS_ENUM(NSInteger, SDScannerArea) {SDScannerAreaDefault,SDScannerAreaFullScreen,
};/**扫一扫基础配置文件*/
@interface SDQrScanConfig : NSObject/**类型*/
@property (nonatomic, assign) SDScannerType scannerType;/**扫描区域*/
@property (nonatomic, assign) SDScannerArea scannerArea;/**棱角颜色*/
@property (nonatomic, strong) UIColor *scannerCornerColor;/**边框颜色*/
@property (nonatomic, strong) UIColor *scannerBorderColor;/**指示器风格*/
@property (nonatomic, assign) UIActivityIndicatorViewStyle indicatorViewStyle;@end
SDQrScanConfig.m
#import "SDQrScanConfig.h"@implementation SDQrScanConfig- (instancetype)init {self = [super init];if (self) {self.scannerCornerColor = [UIColor colorWithRed:63/255.0 green:187/255.0 blue:54/255.0 alpha:1.0];self.scannerBorderColor = [UIColor whiteColor];self.indicatorViewStyle = UIActivityIndicatorViewStyleWhiteLarge;self.scannerType = SDScannerTypeQRCode;}return self;
}@end
SDQrScanTool.h
#import <Foundation/Foundation.h>
#import <Photos/PHPhotoLibrary.h>
#import <AVFoundation/AVFoundation.h>
#import "SDQrScanConfig.h"@interface SDQrScanTool : NSObject/**校验是否有相机权限@param permissionGranted 获取相机权限回调*/
+ (void)checkCameraAuthorizationStatus:(void(^)(BOOL granted))permissionGranted;/**校验是否有相册权限@param permissionGranted 获取相机权限回调*/
+ (void)checkAlbumAuthorizationStatus:(void(^)(BOOL granted))permissionGranted;/**根据扫描器类型配置支持编码格式@param scannerType 扫描器类型@return 编码格式组成的数组*/
+ (NSArray *)metadataObjectType:(SDScannerType)scannerType;/**根据扫描器类型配置导航栏标题@param scannerType 扫描器类型@return 标题*/
+ (NSString *)navigationItemTitle:(SDScannerType)scannerType;/**手电筒开关@param on YES:打开 NO:关闭*/
+ (void)flashlightOn:(BOOL)on;@end
SDQrScanTool.m
#import "SDQrScanTool.h"@implementation SDQrScanTool/** 校验是否有相机权限 */
+ (void)checkCameraAuthorizationStatus:(void(^)(BOOL granted))permissionGranted
{AVAuthorizationStatus videoAuthStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];switch (videoAuthStatus) {// 已授权case AVAuthorizationStatusAuthorized:{permissionGranted(YES);}break;// 未询问用户是否授权case AVAuthorizationStatusNotDetermined:{// 提示用户授权[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {permissionGranted(granted);}];}break;// 用户拒绝授权或权限受限case AVAuthorizationStatusRestricted:case AVAuthorizationStatusDenied:{UIAlertView *alert = [[UIAlertView alloc]initWithTitle:@"请在”设置-隐私-相机”选项中,允许访问你的相机" message:nil delegate:nil cancelButtonTitle:@"确定" otherButtonTitles:nil];[alert show];permissionGranted(NO);}break;default:break;}
}/** 校验是否有相册权限 */
+ (void)checkAlbumAuthorizationStatus:(void(^)(BOOL granted))permissionGranted {PHAuthorizationStatus photoAuthStatus = [PHPhotoLibrary authorizationStatus];switch (photoAuthStatus) {// 已授权case PHAuthorizationStatusAuthorized:{permissionGranted(YES);}break;// 未询问用户是否授权case PHAuthorizationStatusNotDetermined:{[PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {permissionGranted(status == PHAuthorizationStatusAuthorized);}];}break;// 用户拒绝授权或权限受限case PHAuthorizationStatusRestricted:case PHAuthorizationStatusDenied:{UIAlertView *alert = [[UIAlertView alloc]initWithTitle:@"请在”设置-隐私-相片”选项中,允许访问你的相册" message:nil delegate:nil cancelButtonTitle:@"确定" otherButtonTitles:nil];[alert show];permissionGranted(NO);}break;default:break;}}/** 根据扫描器类型配置支持编码格式 */
+ (NSArray *)metadataObjectType:(SDScannerType)scannerType {switch (scannerType) {case SDScannerTypeQRCode:{return @[AVMetadataObjectTypeQRCode];}break;case SDScannerTypeBarCode:{return @[AVMetadataObjectTypeEAN13Code,AVMetadataObjectTypeEAN8Code,AVMetadataObjectTypeUPCECode,AVMetadataObjectTypeCode39Code,AVMetadataObjectTypeCode39Mod43Code,AVMetadataObjectTypeCode93Code,AVMetadataObjectTypeCode128Code,AVMetadataObjectTypePDF417Code];}break;case SDScannerTypeBoth:{return @[AVMetadataObjectTypeQRCode,AVMetadataObjectTypeEAN13Code,AVMetadataObjectTypeEAN8Code,AVMetadataObjectTypeUPCECode,AVMetadataObjectTypeCode39Code,AVMetadataObjectTypeCode39Mod43Code,AVMetadataObjectTypeCode93Code,AVMetadataObjectTypeCode128Code,AVMetadataObjectTypePDF417Code];}break;default:break;}
}/** 根据扫描器类型配置导航栏标题 */
+ (NSString *)navigationItemTitle:(SDScannerType)scannerType {switch (scannerType) {case SDScannerTypeQRCode:{return @"二维码";}break;case SDScannerTypeBarCode:{return @"条码";}break;case SDScannerTypeBoth:{return @"二维码/条码";}break;default:break;}
}/** 手电筒开关 */
+ (void)flashlightOn:(BOOL)on {AVCaptureDevice *captureDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];if ([captureDevice hasTorch] && [captureDevice hasFlash]) {[captureDevice lockForConfiguration:nil];if (on) {[captureDevice setTorchMode:AVCaptureTorchModeOn];[captureDevice setFlashMode:AVCaptureFlashModeOn];}else{[captureDevice setTorchMode:AVCaptureTorchModeOff];[captureDevice setFlashMode:AVCaptureFlashModeOff];}[captureDevice unlockForConfiguration];}
}@end
SDQrScanView.h
#import <UIKit/UIKit.h>
#import "SDQrScanTool.h"
#import "SDBaseControllerView.h"@interface SDQrScanView : SDBaseControllerView- (instancetype)initWithFrame:(CGRect)frame config:(SDQrScanConfig *)config;/**启动扫描线条动画*/
- (void)startLineAnimation;/**停止扫描线条动画*/
- (void)stopLineAnimation;/**添加指示器*/
- (void)addActivityIndicator;/**移除指示器*/
- (void)removeActivityIndicator;/**扫描器坐标点X@return 坐标点X*/
- (CGFloat)scannerOriginX;/**扫描器坐标点Y@return 坐标点Y*/
- (CGFloat)scannerOriginY;/**扫描器宽度@return 宽度*/
- (CGFloat)scannerWidth;/**显示手电筒@param animated 是否附带动画*/
- (void)showFlashlight:(BOOL)animated;/**隐藏手电筒@param animated 是否附带动画*/
- (void)hideFlashlight:(BOOL)animated;/**设置手电筒开关@param on YES:开 NO:关*/
- (void)setFlashlightOn:(BOOL)on;/**获取手电筒当前开关状态@return YES:开 NO:关*/
- (BOOL)flashlightOn;@end
SDQrScanView.m
#import "SDQrScanView.h"
#import "objc/runtime.h"static const CGFloat kScannerScale = 0.7; //屏幕宽度的比例static const CGFloat kBottomSpace = 50.0; //居中对齐后向上偏移的距离static const CGFloat kScannerLineHeight = 10.0; //扫描器线条高度static const CGFloat kTipsHeight = 50.0; //底部提示高度static const CGFloat kLightSize = 20.0f; //灯光sizestatic const CGFloat kLightTipsHeight = 15.0f; //灯光提示间距static const CGFloat kLightTipsPadding = 10.0f; //灯光提示间距static const CGFloat kScannerBorderWidth = 1.0f; //扫描器边框宽度static const CGFloat kScannerCornerWidth = 3.0f; //扫描器棱角宽度static const CGFloat kScannerCornerLength = 20.0f; //扫描器棱角长度NSString *const scannerLineViewAnmationKey = @"scannerLineViewAnmationKey"; //扫描线条动画Key值@interface SDQrScanView()@property (nonatomic, strong) UIImageView *scannerLineView; /** 扫描线条 */
@property (nonatomic, strong) UIActivityIndicatorView *activityIndicator; /** 加载指示器 */
@property (nonatomic, strong) UIButton *lightButton; /** 手电筒开关 */
@property (nonatomic, strong) UILabel *lightTipsLabel; /** 手电筒提示文字 */
@property (nonatomic, strong) UILabel *tipsLabel; /** 扫描器下方提示文字 */@property (nonatomic, strong) SDQrScanConfig *config;@property (nonatomic, assign) BOOL lightOn; //手电筒开关是否打开@end@implementation SDQrScanView- (instancetype)initWithFrame:(CGRect)frame config:(SDQrScanConfig *)config {self = [super initWithFrame:frame];if (self) {self.config = config;[self setupViews];[self bringSubviewToFront:self.navigationBar];}return self;
}- (void)setupViews {self.backgroundColor = [UIColor clearColor];[self addSubview:self.scannerLineView];[self addSubview:self.tipsLabel];[self addSubview:self.lightButton];[self addSubview:self.lightTipsLabel];[self startLineAnimation];
}- (void)layoutSubviews {[super layoutSubviews];CGFloat width = CGRectGetWidth(self.bounds);CGFloat height = CGRectGetHeight(self.bounds);CGFloat scannerWidth = kScannerScale * width;CGFloat originX = (width - scannerWidth)/2;CGFloat originY = (height - scannerWidth)/2 - kBottomSpace;self.scannerLineView.frame = CGRectMake(originX, originY, scannerWidth, kScannerLineHeight);self.tipsLabel.frame = CGRectMake(0, originY + scannerWidth, width, kTipsHeight);self.lightButton.frame = CGRectMake((width - kLightSize)/2.0, CGRectGetMinY(self.tipsLabel.frame) - kLightTipsPadding - kLightTipsHeight - kLightSize, kLightSize, kLightSize);self.lightTipsLabel.frame = CGRectMake(originX, CGRectGetMinY(self.tipsLabel.frame) - kLightTipsPadding - kLightTipsHeight, scannerWidth, kLightTipsHeight);
}#pragma mark -- 手电筒点击事件
- (void)flashlightClicked:(UIButton *)button {button.selected = !button.selected;[self setFlashlightOn:self.lightButton.selected];
}/** 添加扫描线条动画 */
- (void)startLineAnimation {// 若已添加动画,则先移除动画再添加[self.scannerLineView.layer removeAllAnimations];CABasicAnimation *lineAnimation = [CABasicAnimation animationWithKeyPath:@"transform"];lineAnimation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeTranslation(0, [self scannerWidth] - kScannerLineHeight, 1)];lineAnimation.duration = 4;lineAnimation.repeatCount = MAXFLOAT;lineAnimation.autoreverses = YES; // 动画结束时执行逆动画[self.scannerLineView.layer addAnimation:lineAnimation forKey:scannerLineViewAnmationKey];// 重置动画运行速度为1.0self.scannerLineView.layer.speed = 2.0;
}/** 暂停扫描器动画 */
- (void)stopLineAnimation {// 取出当前时间,转成动画暂停的时间CFTimeInterval pauseTime = [self.scannerLineView.layer convertTime:CACurrentMediaTime() fromLayer:nil];// 设置动画的时间偏移量,指定时间偏移量的目的是让动画定格在该时间点的位置self.scannerLineView.layer.timeOffset = pauseTime;// 将动画的运行速度设置为0, 默认的运行速度是1.0self.scannerLineView.layer.speed = 0;
}/** 显示手电筒 */
- (void)showFlashlight:(BOOL)animated {if (animated) {[UIView animateWithDuration:0.6 animations:^{self.lightTipsLabel.alpha = 1.0;self.lightButton.alpha = 1.0;self.tipsLabel.alpha = 0;} completion:^(BOOL finished) {self.lightButton.enabled = YES;}];} else {self.lightTipsLabel.alpha = 1.0;self.lightButton.alpha = 1.0;self.tipsLabel.alpha = 0;self.lightButton.enabled = YES;}
}/** 隐藏手电筒 */
- (void)hideFlashlight:(BOOL)animated {self.lightButton.enabled = NO;if (animated) {[UIView animateWithDuration:0.6 animations:^{self.lightTipsLabel.alpha = 0;self.lightButton.alpha = 0;self.tipsLabel.alpha = 1.0;} completion:^(BOOL finished) {}];} else {self.tipsLabel.alpha = 1.0;self.lightTipsLabel.alpha = 0;self.lightButton.alpha = 0;}
}/** 添加指示器 */
- (void)addActivityIndicator {if (!self.activityIndicator) {self.activityIndicator = [[UIActivityIndicatorView alloc]initWithActivityIndicatorStyle:self.config.indicatorViewStyle];self.activityIndicator.center = self.center;[self addSubview:self.activityIndicator];}[self.activityIndicator startAnimating];
}/**移除指示器*/
- (void)removeActivityIndicator {if (self.activityIndicator) {[self.activityIndicator removeFromSuperview];self.activityIndicator = nil;}
}/**设置手电筒开关@param on 是否打开,YES打开,NO,关闭*/
- (void)setFlashlightOn:(BOOL)on {[SDQrScanTool flashlightOn:on];self.lightTipsLabel.text = on ? @"轻触关闭":@"轻触照亮";self.lightButton.selected = on;self.lightOn = on;
}/**获取手电筒当前开关状态@return 开关状态, YES 打开状态, NO 关闭状态*/
- (BOOL)flashlightOn {return self.lightOn;
}/**draw绘制@param rect rect*/
- (void)drawRect:(CGRect)rect {[super drawRect:rect];// 半透明区域[[UIColor colorWithWhite:0 alpha:0.7] setFill];UIRectFill(rect);// 透明区域CGRect scanner_rect = CGRectMake([self scannerOriginX], [self scannerOriginY], [self scannerWidth], [self scannerWidth]);[[UIColor clearColor] setFill];UIRectFill(scanner_rect);// 边框UIBezierPath *borderPath = [UIBezierPath bezierPathWithRect:CGRectMake([self scannerOriginX], [self scannerOriginY], [self scannerWidth], [self scannerWidth])];borderPath.lineCapStyle = kCGLineCapRound;borderPath.lineWidth = kScannerBorderWidth;[self.config.scannerBorderColor set];[borderPath stroke];for (int index = 0; index < 4; ++index) {UIBezierPath *tempPath = [UIBezierPath bezierPath];tempPath.lineWidth = kScannerCornerWidth;[self.config.scannerCornerColor set];switch (index) {// 左上角棱角case 0:{[tempPath moveToPoint:CGPointMake([self scannerOriginX] + kScannerCornerLength, [self scannerOriginY])];[tempPath addLineToPoint:CGPointMake([self scannerOriginX], [self scannerOriginY])];[tempPath addLineToPoint:CGPointMake([self scannerOriginX], [self scannerOriginY] + kScannerCornerLength)];}break;// 右上角case 1:{[tempPath moveToPoint:CGPointMake([self scannerOriginX] + [self scannerWidth] - kScannerCornerLength, [self scannerOriginY])];[tempPath addLineToPoint:CGPointMake([self scannerOriginX] + [self scannerWidth], [self scannerOriginY])];[tempPath addLineToPoint:CGPointMake([self scannerOriginX] + [self scannerWidth], [self scannerOriginY] + kScannerCornerLength)];}break;// 左下角case 2:{[tempPath moveToPoint:CGPointMake([self scannerOriginX], [self scannerOriginY] + [self scannerWidth] - kScannerCornerLength)];[tempPath addLineToPoint:CGPointMake([self scannerOriginX], [self scannerOriginY] + [self scannerWidth])];[tempPath addLineToPoint:CGPointMake([self scannerOriginX] + kScannerCornerLength, [self scannerOriginY] + [self scannerWidth])];}break;// 右下角case 3:{[tempPath moveToPoint:CGPointMake([self scannerOriginX] + [self scannerWidth] - kScannerCornerLength, [self scannerOriginY] + [self scannerWidth])];[tempPath addLineToPoint:CGPointMake([self scannerOriginX] + [self scannerWidth], [self scannerOriginY] + [self scannerWidth])];[tempPath addLineToPoint:CGPointMake([self scannerOriginX] + [self scannerWidth], [self scannerOriginY] + [self scannerWidth] - kScannerCornerLength)];}break;default:break;}[tempPath stroke];}
}#pragma mark - 扫描器坐标点位置
/**扫描器坐标点X@return 坐标点X*/
- (CGFloat)scannerOriginX {CGFloat width = CGRectGetWidth(self.bounds);CGFloat scannerWidth = kScannerScale * width;CGFloat originX = (width - scannerWidth)/2;return originX;
}/**扫描器坐标点Y@return 坐标点Y*/
- (CGFloat)scannerOriginY {CGFloat width = CGRectGetWidth(self.bounds);CGFloat height = CGRectGetHeight(self.bounds);CGFloat scannerWidth = kScannerScale * width;CGFloat originY = (height - scannerWidth)/2 - kBottomSpace;return originY;
}/**扫描器宽度@return 宽度*/
- (CGFloat)scannerWidth {CGFloat width = CGRectGetWidth(self.bounds);CGFloat scannerWidth = kScannerScale * width;return scannerWidth;
}#pragma mark - SETTER/GETTER
/**扫描线条@return 扫描线条ImageView*/
- (UIImageView *)scannerLineView {if (!_scannerLineView) {_scannerLineView = [[UIImageView alloc] initWithFrame:CGRectZero];_scannerLineView.image = [UIImage imageNamed:@"ScannerLine"];}return _scannerLineView;
}/**扫描器下方提示文字@return 下方提示文字Label*/
- (UILabel *)tipsLabel {if (!_tipsLabel) {_tipsLabel = [[UILabel alloc]initWithFrame:CGRectZero];_tipsLabel.textAlignment = NSTextAlignmentCenter;_tipsLabel.textColor = [UIColor lightGrayColor];_tipsLabel.text = @"将二维码/条码放入框内,即可自动扫描";_tipsLabel.font = [UIFont systemFontOfSize:12];}return _tipsLabel;
}/**手电筒开关按钮@return 开关按钮Button*/
- (UIButton *)lightButton {if (!_lightButton) {_lightButton = [UIButton buttonWithType:UIButtonTypeCustom];_lightButton.enabled = NO;_lightButton.alpha = 0;[_lightButton addTarget:self action:@selector(flashlightClicked:) forControlEvents:UIControlEventTouchUpInside];[_lightButton setBackgroundImage:[UIImage imageNamed:@"Flashlight_Off"] forState:UIControlStateNormal];[_lightButton setBackgroundImage:[UIImage imageNamed:@"Flashlight_On"] forState:UIControlStateSelected];}return _lightButton;
}/**手电筒提示文字@return 提示文字控件Label*/
- (UILabel *)lightTipsLabel {if (!_lightTipsLabel) {_lightTipsLabel = [[UILabel alloc] initWithFrame:CGRectZero];_lightTipsLabel.font = [UIFont systemFontOfSize:12];_lightTipsLabel.textColor = [UIColor whiteColor];_lightTipsLabel.text = @"轻触照亮";_lightTipsLabel.alpha = 0;_lightTipsLabel.textAlignment = NSTextAlignmentCenter;}return _lightTipsLabel;
}@end
SDQrScanViewController.h
#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>
#import "SDQrScanTool.h"
#import "SDBaseViewController.h"@interface SDQrScanViewController : SDBaseViewController@property (nonatomic, strong) SDQrScanConfig *scanConfig;@end
SDQrScanViewController.m
#import "SDQrScanViewController.h"
#import "SDQrScanView.h"@interface SDQrScanViewController ()<AVCaptureMetadataOutputObjectsDelegate, AVCaptureVideoDataOutputSampleBufferDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate>@property (nonatomic, strong) SDQrScanView *scannerView;
@property (nonatomic, strong) AVCaptureSession *session;@property (nonatomic, strong) UIView *videoPreView; //视频预览显示视图@end@implementation SDQrScanViewController- (void)dealloc {[[NSNotificationCenter defaultCenter] removeObserver:self];
}- (SDQrScanConfig *)scanConfig {if (!_scanConfig) {_scanConfig = [[SDQrScanConfig alloc] init];}return _scanConfig;
}- (SDQrScanView *)scannerView {if (!_scannerView) {_scannerView = [[SDQrScanView alloc] initWithFrame:self.view.bounds config:self.scanConfig];}return _scannerView;
}- (UIView *)videoPreView {if (!_videoPreView) {_videoPreView = [[UIView alloc] initWithFrame:self.view.bounds];}_videoPreView.backgroundColor = [UIColor clearColor];return _videoPreView;
}#pragma mark - Configure NavigationBar
- (void)configureNavigationBar {SDNavButtonItem *leftButtonItem = [[SDNavButtonItem alloc] initWithTitle:nil image:[UIImage imageNamed:@"ic_nav_back_gray"] target:self action:@selector(leftBarClicked)];SDNavButtonItem *rightButtonItem = [[SDNavButtonItem alloc] initWithTitle:nil image:[UIImage imageNamed:@"ic_nav_common_download"] target:self action:@selector(rightBarClicked)];self.scannerView.navigationBar.navTitleView = [[SDNavigationTitleView alloc] initWidthTitle:@"扫一扫" subView:nil];self.scannerView.navigationBar.leftNavItem = leftButtonItem;// self.scannerView.navigationBar.rightNavItem = rightButtonItem;
}- (void)leftBarClicked {[self.navigationController popViewControllerAnimated:YES];
}- (void)rightBarClicked {// 扫一扫
}#pragma mark - loadView
- (void)loadView {[super loadView];
}- (void)viewDidLoad {[super viewDidLoad];self.navigationItem.title = @"扫一扫";[self configureNavigationBar];[self setupScannerLayer];[[NSNotificationCenter defaultCenter] addObserver:selfselector:@selector(appDidBecomeActive:)name:UIApplicationDidBecomeActiveNotificationobject:nil];[[NSNotificationCenter defaultCenter] addObserver:selfselector:@selector(appWillResignActive:)name:UIApplicationWillResignActiveNotificationobject:nil];
}- (void)viewWillAppear:(BOOL)animated {[super viewWillAppear:animated];[self resumeScanning];
}- (void)viewWillDisappear:(BOOL)animated
{[super viewWillDisappear:animated];[self.scannerView setFlashlightOn:NO];[self.scannerView hideFlashlight:YES];
}- (void)setupScannerLayer {self.view.backgroundColor = [UIColor blackColor];UIBarButtonItem *albumItem = [[UIBarButtonItem alloc]initWithTitle:@"相册" style:UIBarButtonItemStylePlain target:self action:@selector(showAlbum)];[albumItem setTintColor:[UIColor blackColor]];self.navigationItem.rightBarButtonItem = albumItem;[self.view addSubview:self.videoPreView];[self.view addSubview:self.scannerView];// 校验相机权限[SDQrScanTool checkCameraAuthorizationStatus:^(BOOL granted) {if (granted) {dispatch_async(dispatch_get_main_queue(), ^{[self loadScanView];});}}];
}/** 创建扫描器 */
- (void)loadScanView {AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice:device error:nil];AVCaptureMetadataOutput *metadataOutput = [[AVCaptureMetadataOutput alloc] init];[metadataOutput setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];if (self.scanConfig.scannerArea == SDScannerAreaDefault) {metadataOutput.rectOfInterest = CGRectMake([self.scannerView scannerOriginX]/self.view.frame.size.height, [self.scannerView scannerOriginY]/self.view.frame.size.width, [self.scannerView scannerWidth]/self.view.frame.size.height, [self.scannerView scannerWidth]/self.view.frame.size.width);}AVCaptureVideoDataOutput *videoDataOutput = [[AVCaptureVideoDataOutput alloc] init];[videoDataOutput setSampleBufferDelegate:self queue:dispatch_get_main_queue()];self.session = [[AVCaptureSession alloc]init];[self.session setSessionPreset:AVCaptureSessionPresetHigh];if ([self.session canAddInput:deviceInput]) {[self.session addInput:deviceInput];}if ([self.session canAddOutput:metadataOutput]) {[self.session addOutput:metadataOutput];}if ([self.session canAddOutput:videoDataOutput]) {[self.session addOutput:videoDataOutput];}metadataOutput.metadataObjectTypes = [SDQrScanTool metadataObjectType:self.scanConfig.scannerType];AVCaptureVideoPreviewLayer *videoPreviewLayer = [AVCaptureVideoPreviewLayer layerWithSession:_session];videoPreviewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;videoPreviewLayer.frame = self.view.layer.bounds;[self.videoPreView.layer insertSublayer:videoPreviewLayer atIndex:0];[self.session startRunning];
}#pragma mark -- 跳转相册
- (void)imagePicker {UIImagePickerController *imagePicker = [[UIImagePickerController alloc]init];imagePicker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;imagePicker.delegate = self;[self presentViewController:imagePicker animated:YES completion:nil];
}#pragma mark -- AVCaptureMetadataOutputObjectsDelegate
- (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection {// 获取扫一扫结果if (metadataObjects && metadataObjects.count > 0) {[self pauseScanning];AVMetadataMachineReadableCodeObject *metadataObject = metadataObjects[0];NSString *stringValue = metadataObject.stringValue;//AVMetadataMachineReadableCodeObject *obj = (AVMetadataMachineReadableCodeObject *)[self.lay transformedMetadataObjectForMetadataObject:metadataObjects.lastObject];//[self changeVideoScale:metadataObject];[self handleScanValue:stringValue];}
}#pragma mark -- AVCaptureVideoDataOutputSampleBufferDelegate
/** 此方法会实时监听亮度值 */
- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {CFDictionaryRef metadataDict = CMCopyDictionaryOfAttachments(NULL,sampleBuffer, kCMAttachmentMode_ShouldPropagate);NSDictionary *metadata = [[NSMutableDictionary alloc] initWithDictionary:(__bridge NSDictionary*)metadataDict];CFRelease(metadataDict);NSDictionary *exifMetadata = [[metadata objectForKey:(NSString *)kCGImagePropertyExifDictionary] mutableCopy];// 亮度值float brightnessValue = [[exifMetadata objectForKey:(NSString *)kCGImagePropertyExifBrightnessValue] floatValue];if (![self.scannerView flashlightOn]) {if (brightnessValue < -1.0) {[self.scannerView showFlashlight:YES];} else {[self.scannerView hideFlashlight:YES];}}
}- (void)showAlbum {// 校验相册权限[SDQrScanTool checkAlbumAuthorizationStatus:^(BOOL granted) {if (granted) {[self imagePicker];}}];
}#pragma mark -- UIImagePickerControllerDelegate
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info
{UIImage *pickImage = info[UIImagePickerControllerOriginalImage];CIDetector *detector = [CIDetector detectorOfType:CIDetectorTypeQRCode context:nil options:@{CIDetectorAccuracy: CIDetectorAccuracyHigh}];// 获取选择图片中识别结果NSArray *features = [detector featuresInImage:[CIImage imageWithData:UIImagePNGRepresentation(pickImage)]];[picker dismissViewControllerAnimated:YES completion:^{if (features.count > 0) {CIQRCodeFeature *feature = features[0];NSString *stringValue = feature.messageString;[self handleScanValue:stringValue];} else {[self readFromAlbumFailed];}}];
}- (void)changeVideoScale:(AVMetadataMachineReadableCodeObject *)objc {NSArray *array = objc.corners;CGPoint point = CGPointZero;int index = 0;CFDictionaryRef dict = (__bridge CFDictionaryRef)(array[index++]);// 把点转换为不可变字典// 把字典转换为点,存在point里,成功返回true 其他falseCGPointMakeWithDictionaryRepresentation(dict, &point);NSLog(@"X:%f -- Y:%f",point.x,point.y);CGPoint point2 = CGPointZero;CGPointMakeWithDictionaryRepresentation((__bridge CFDictionaryRef)array[2], &point2);NSLog(@"X:%f -- Y:%f",point2.x,point2.y);NSLog(@"bounds:%@",NSStringFromCGRect(objc.bounds));}#pragma mark -- App 从后台进入前台
- (void)appDidBecomeActive:(NSNotification *)notify {[self resumeScanning];
}#pragma mark -- App 从前台进入后台
- (void)appWillResignActive:(NSNotification *)notify {[self pauseScanning];
}/**恢复扫一扫功能*/
- (void)resumeScanning {if (self.session) {[self.session startRunning];[self.scannerView startLineAnimation];}
}/**暂停扫一扫*/
- (void)pauseScanning {if (self.session) {[self.session stopRunning];[self.scannerView stopLineAnimation];}
}#pragma mark -- 扫一扫API
/**处理扫一扫结果@param value 扫描结果*/
- (void)handleScanValue:(NSString *)value {NSLog(@"handleScanValue === %@", value);
}/**相册选取图片无法读取数据*/
- (void)readFromAlbumFailed {NSLog(@"readFromAlbumFailed");
}- (void)didReceiveMemoryWarning {[super didReceiveMemoryWarning];// Dispose of any resources that can be recreated.
}@end
至此实现了二维码扫一扫Scan及识别图片中二维码功能。
五、小结
iOS开发-实现二维码扫一扫Scan及识别图片中二维码功能。扫一扫是使用摄像头扫码二维码或者条形码,获取对应二维码或条形码内容字符串。识别图中二维码通过CIDetector来识别出内容字符串。最后实现响应的业务逻辑。
学习记录,每天不停进步。