IOS自定義相機(jī),使用AVFoundation(附實(shí)現(xiàn)部分騰訊水印相機(jī)功能demo)

在項(xiàng)目中當(dāng)我們遇到拍照的功能模塊的時(shí)候,如果僅僅是用來(lái)拍照,系統(tǒng)提供的UIImagePickerController足夠用來(lái)完成我們的任務(wù)。但是當(dāng)我們的應(yīng)用場(chǎng)景稍稍復(fù)雜點(diǎn)的時(shí)候,如要實(shí)現(xiàn)類(lèi)似水印相機(jī)、美顏相機(jī)的時(shí)候,UIImagePickerController就有點(diǎn)力不從心了,需要自己去diy一個(gè)自定義相機(jī)。

UIImagePickerController使用起來(lái)比較簡(jiǎn)單易用,拍照,錄制視頻、控制閃光燈,前后攝像頭的切換一應(yīng)俱全。但是相機(jī)支持ui界面的自定義并不好(雖然可以支持自定義),在不同的系統(tǒng)下相機(jī)的功能界面還有所差別。

以水印相機(jī)為例,在水印相機(jī)中我們需要能夠讓水印模式實(shí)時(shí)的顯示在相機(jī)的取景框中,而且水印模式還要可以左右滑動(dòng)切換,在橫屏的時(shí)候水印也要跟著橫屏,還要有放大縮小鏡頭的以及點(diǎn)擊屏幕能夠聚焦等功能。

騰訊水印相機(jī).gif

當(dāng)然有人可能會(huì)想,將水印模式分成一個(gè)視圖層然后放到相機(jī)的最上層不就行了嗎?當(dāng)然是不行的,首先UIImagePickerController在不同系統(tǒng)中的封裝是略微不一樣的,ui界面有所差別,界面不能夠統(tǒng)一,即便是現(xiàn)在花了很過(guò)代碼一個(gè)系統(tǒng)一個(gè)系統(tǒng)的適配,也很難保證以后不出問(wèn)題,其次是手勢(shì)的識(shí)別也有問(wèn)題,即便是對(duì)手勢(shì)進(jìn)行了攔截處理,也不能解決,說(shuō)白了也就是不是自己封裝的東西,難以得到完美的掌控。

下面就開(kāi)始自定義一個(gè)相機(jī),并實(shí)現(xiàn)拍照、取消、閃光燈控制、前后攝像頭控制、聚焦、放大縮小、拍照后預(yù)覽、重拍、使用照片等功能。

@property (nonatomic, strong) ZTImagePickerOverLayView  *overlayView;//預(yù)覽圖層

@property (nonatomic) dispatch_queue_t sessionQueue;

@property (nonatomic, strong) AVCaptureSession* session;//用于捕捉視頻和音頻,協(xié)調(diào)視頻和音頻的輸入和輸出流

@property (nonatomic, strong) AVCaptureDeviceInput* videoInput;

@property (nonatomic, strong) AVCaptureStillImageOutput* stillImageOutput;//輸出靜態(tài)影像

@property (nonatomic, strong) AVCaptureDevice             *device;//主要用來(lái)獲取iphone一些關(guān)于相機(jī)設(shè)備的屬性
@property (nonatomic, strong) AVCaptureVideoPreviewLayer* previewLayer;//預(yù)覽圖層layer

這里將相機(jī)的控件以及相機(jī)的實(shí)時(shí)顯示的圖層放在一個(gè)視圖類(lèi)ZTImagePickerOverLayView中,拍完照后的圖層放在另外一個(gè)類(lèi)ZTImagePickerPreImageView中,各個(gè)視圖間的協(xié)調(diào)及部分邏輯放在控制器中ZTImagePickerController。封裝完整個(gè)相機(jī)不過(guò)用了幾百行代碼。

1.初始化

 self.session = [[AVCaptureSession alloc] init];
    [self.session setSessionPreset:AVCaptureSessionPresetPhoto];
    
    NSError *error;
    
    self.device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
    
    
    self.videoInput = [[AVCaptureDeviceInput alloc] initWithDevice:self.device error:&error];
   
    self.stillImageOutput = [[AVCaptureStillImageOutput alloc] init];
    //輸出設(shè)置。AVVideoCodecJPEG   輸出jpeg格式圖片
    NSDictionary * outputSettings = [[NSDictionary alloc] initWithObjectsAndKeys:AVVideoCodecJPEG,AVVideoCodecKey, nil];
    [self.stillImageOutput setOutputSettings:outputSettings];
    
    if ([self.session canAddInput:self.videoInput]) {
        [self.session addInput:self.videoInput];
    }
    if ([self.session canAddOutput:self.stillImageOutput]) {
        [self.session addOutput:self.stillImageOutput];
    }
    
    //初始化預(yù)覽圖層
    self.previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:self.session];
    [self.previewLayer setVideoGravity:AVLayerVideoGravityResizeAspect];
    NSLog(@"%f",ScreenWidth);
    self.previewLayer.frame = CGRectMake(0, 0,ScreenWidth, ScreenHeight);
    self.preview = [[ZTImagePickerOverLayView alloc] init];
    self.preview.frame = CGRectMake(0, 0, ScreenWidth, ScreenHeight);
    [self.preview layoutSubviews];
    [self.preview.layer addSublayer:self.previewLayer];
    [self.view addSubview:self.preview];
    
    //添加頂部以及底部的自定義工具條
    [self.view addSubview:self.preview.topbar];
    [self.view addSubview:self.preview.buttomBar];
    self.preview.topbar.frame = CGRectMake(0, 0, self.view.width, 64 * ScreenWidth/320.0);
    self.preview.buttomBar.frame = CGRectMake(0, self.view.height - 70 * ScreenWidth/320.0 , self.view.width, 70* ScreenWidth/320.0);
    [self.preview layoutSubviews];
    //設(shè)置閃關(guān)燈模式
    if(self.device.isFlashAvailable)
        [self.preview setFlashModel:self.device.flashMode];
    else{
        self.preview.flashButton.hidden = YES;
        self.preview.cameraSwitchButton.hidden = YES;
    }
    
    //設(shè)置拍照后預(yù)覽圖層
    self.preImageView = [[ZTImagePickerPreImageView alloc] initWithFrame:CGRectMake(0, 0, ScreenWidth, ScreenHeight)];
    [self.preImageView layoutSubviews];
    self.preImageView.hidden = YES;
    [self.view addSubview:self.preImageView];

2.添加手勢(shì)
給預(yù)覽層添加捏合手勢(shì)控制放大縮小,添加點(diǎn)擊手勢(shì)來(lái)聚焦

 UIPinchGestureRecognizer *pinch = [[UIPinchGestureRecognizer alloc] initWithTarget:self
                                                                                action:@selector(handlePinchGesture:)];
    pinch.delegate = self;
    [self.preview addGestureRecognizer:pinch];
    
    
    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self
                                                                          action:@selector(focusAction:)];
    [self.preview addGestureRecognizer:tap];

3.給控件添加響應(yīng)事件
閃光燈打開(kāi)、關(guān)閉、自動(dòng),攝像頭切換、取消、拍照、重新拍照、使用照片等按鈕添加響應(yīng)事件

[self.preview.cameraSwitchButton addTarget:self action:@selector(switchCameraSegmentedControlClick:) forControlEvents:UIControlEventTouchUpInside];
    [self.preview.flashAutoButton addTarget:self action:@selector(flashButtonClick:) forControlEvents:UIControlEventTouchUpInside];
    [self.preview.flashOpeanButton addTarget:self action:@selector(flashButtonClick:) forControlEvents:UIControlEventTouchUpInside];
    [self.preview.flashCloseButton addTarget:self action:@selector(flashButtonClick:) forControlEvents:UIControlEventTouchUpInside];
    
    [self.preview.takePictureButton addTarget:self action:@selector(takePhotoButtonClick:) forControlEvents:UIControlEventTouchUpInside];
    
    [self.preview.cancelButton addTarget:self action:@selector(cancelButtonClick:) forControlEvents:UIControlEventTouchUpInside];
    
    [self.preImageView.reTakeButton addTarget:self action:@selector(retakeButtonClick:) forControlEvents:UIControlEventTouchUpInside];
    [self.preImageView.useImageButton addTarget:self action:@selector(useImageButtonClick:) forControlEvents:UIControlEventTouchUpInside];

4.閃光燈控制

- (void)flashButtonClick:(UIButton *)sender {
    //[self.preview reSetTopbar];
    [self.preview chosedFlashButton:sender];
    
    NSLog(@"flashButtonClick");
    
    AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
    
    //修改前必須先鎖定
    [device lockForConfiguration:nil];
    //必須判定是否有閃光燈,否則如果沒(méi)有閃光燈會(huì)崩潰
    if ([device hasFlash]) {
        if([sender.titleLabel.text isEqualToString:@"打開(kāi)"]){
            if([device isFlashModeSupported:AVCaptureFlashModeOn])
                [device setFlashMode:AVCaptureFlashModeOn];
        }else if ([sender.titleLabel.text isEqualToString:@"自動(dòng)"]){
            if([device isFlashModeSupported:AVCaptureFlashModeAuto])
                [device setFlashMode:AVCaptureFlashModeAuto];
            
        }else if ([sender.titleLabel.text isEqualToString:@"關(guān)閉"]){
            if([device isFlashModeSupported:AVCaptureFlashModeOff])
                [device setFlashMode:AVCaptureFlashModeOff];
        }
    } else {
        
        NSLog(@"設(shè)備不支持閃光燈");
    }
    [device unlockForConfiguration];
}

5.前后攝像頭切換

- (void)switchCameraSegmentedControlClick:(id)sender {
    
    //NSLog(@"%ld",(long)sender.selectedSegmentIndex);
    
    AVCaptureDevicePosition desiredPosition;
    AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
    if (isUsingFrontFacingCamera){
        
        if(device.isFlashAvailable) self.preview.flashButton.hidden = NO;
        desiredPosition = AVCaptureDevicePositionBack;
        
    }else{
        desiredPosition = AVCaptureDevicePositionFront;
        [self.preview reSetTopbar];
        self.preview.flashButton.hidden = YES;
        
        
    }
    
    
    for (AVCaptureDevice *d in [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]) {
        if ([d position] == desiredPosition) {
            [self.previewLayer.session beginConfiguration];
            AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:d error:nil];
            for (AVCaptureInput *oldInput in self.previewLayer.session.inputs) {
                [[self.previewLayer session] removeInput:oldInput];
            }
            [self.previewLayer.session addInput:input];
            [self.previewLayer.session commitConfiguration];
            break;
        }
    }
    
    isUsingFrontFacingCamera = !isUsingFrontFacingCamera;
}

6.拍照

- (void)takePhotoButtonClick:(id )sender{
    
    AVCaptureConnection *stillImageConnection = [self.stillImageOutput connectionWithMediaType:AVMediaTypeVideo];
    UIDeviceOrientation curDeviceOrientation = [[UIDevice currentDevice] orientation];
    AVCaptureVideoOrientation avcaptureOrientation = [self avOrientationForDeviceOrientation:curDeviceOrientation];
    [stillImageConnection setVideoOrientation:avcaptureOrientation];
    [stillImageConnection setVideoScaleAndCropFactor:self.effectiveScale];
    
    [self.stillImageOutput captureStillImageAsynchronouslyFromConnection:stillImageConnection completionHandler:^(CMSampleBufferRef imageDataSampleBuffer, NSError *error) {
        
        NSData *jpegData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageDataSampleBuffer];
        self.imageData = jpegData;
        CFDictionaryRef attachments = CMCopyDictionaryOfAttachments(kCFAllocatorDefault,
                                                                    imageDataSampleBuffer,
                                                                    kCMAttachmentMode_ShouldPropagate);
        UIImage *image = [UIImage imageWithData:jpegData];
        [self waterMarkFixed];
        self.preImageView.imageView.image = image;
        [self.preview hiddenSelfAndBars:YES];
        self.preImageView.hidden = NO;
        
        ALAuthorizationStatus author = [ALAssetsLibrary authorizationStatus];
            if (author == ALAuthorizationStatusRestricted || author ==ALAuthorizationStatusDenied){
                    //無(wú)權(quán)限
                    return ;
                }
        //保存到相冊(cè)
//            ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
//            [library writeImageDataToSavedPhotosAlbum:jpegData metadata:(__bridge id)attachments completionBlock:^(NSURL *assetURL, NSError *error) {
//        
//            }];
        
    }];
    
    if([self.delegate respondsToSelector:@selector(imagePickerControllerTakePhoto:)])
        [self.delegate imagePickerControllerTakePhoto:self];
    
}

7.放大縮小

- (void)handlePinchGesture:(UIPinchGestureRecognizer *)recognizer{
    
    BOOL allTouchesAreOnThePreviewLayer = YES;
    NSUInteger numTouches = [recognizer numberOfTouches], i;
    for ( i = 0; i < numTouches; ++i ) {
        CGPoint location = [recognizer locationOfTouch:i inView:self.preview];
        CGPoint convertedLocation = [self.previewLayer convertPoint:location fromLayer:self.previewLayer.superlayer];
        if ( ! [self.previewLayer containsPoint:convertedLocation] ) {
            allTouchesAreOnThePreviewLayer = NO;
            break;
        }
    }
    
    if ( allTouchesAreOnThePreviewLayer ) {
        
        
        self.effectiveScale = self.beginGestureScale * recognizer.scale;
        if (self.effectiveScale < 1.0){
            self.effectiveScale = 1.0;
        }
        
        CGFloat maxScaleAndCropFactor = [[self.stillImageOutput connectionWithMediaType:AVMediaTypeVideo] videoMaxScaleAndCropFactor];
        
        
        if (self.effectiveScale > maxScaleAndCropFactor)
            self.effectiveScale = maxScaleAndCropFactor;
        
        [CATransaction begin];
        [CATransaction setAnimationDuration:.025];
        [self.previewLayer setAffineTransform:CGAffineTransformMakeScale(self.effectiveScale, self.effectiveScale)];
        [CATransaction commit];
        
    }
}

8.照片方向修正
拍完照的時(shí)候,拍出的照片你會(huì)發(fā)現(xiàn)呈現(xiàn)的方向不對(duì),需要對(duì)照片的方向進(jìn)行修正。

 AVCaptureVideoOrientation avcaptureOrientation = [self avOrientationForDeviceOrientation:curDeviceOrientation];
    [stillImageConnection setVideoOrientation:avcaptureOrientation];

- (AVCaptureVideoOrientation)avOrientationForDeviceOrientation:(UIDeviceOrientation)deviceOrientation
{
    AVCaptureVideoOrientation result = (AVCaptureVideoOrientation)deviceOrientation;
    if ( deviceOrientation == UIDeviceOrientationLandscapeLeft )
        result = AVCaptureVideoOrientationLandscapeRight;
    else if ( deviceOrientation == UIDeviceOrientationLandscapeRight )
        result = AVCaptureVideoOrientationLandscapeLeft;
    return result;
}

這里需要注意的是如果想要拍完照的效果和UIimagePickerController的效果一樣,即在橫屏下拍完照,照片要旋轉(zhuǎn)顯示并且顯示的小一些,那么就要對(duì)拍完照預(yù)覽圖層進(jìn)行修改。

圖片

在預(yù)覽圖層中修改imageview的大小。(這部分代碼我并沒(méi)有加到demo中,如果想實(shí)現(xiàn)拍完照后照片的方向與系統(tǒng)相機(jī)的一樣,可以在ZTImagePickerPreImageView中加上)

- (void)changeImageViewFrameIfNeeded:(UIDeviceOrientation)orientation{
    if(orientation == UIDeviceOrientationLandscapeLeft || orientation == UIDeviceOrientationLandscapeRight){
    
        self.imageView.frame = CGRectMake(0, 0, self.width, 240 * [UIScreen mainScreen].bounds.size.width / 320.0);
        self.imageView.centerY = self.height / 2.0;
    }else{
        _imageView.frame = CGRectMake(0, 0, self.width, 418 * [UIScreen mainScreen].bounds.size.width / 320.0);
        _imageView.centerY = self.height / 2.0;
    }
}

僅僅是這樣還不夠,還要對(duì)設(shè)備方向的獲取進(jìn)行改進(jìn)。
通常我們獲取設(shè)備方向是通過(guò)[[UIDevice currentDevice] orientation] 或者通過(guò)[UIApplication sharedApplication].statusBarOrientation的方式來(lái)獲取。但這兩種方式有一個(gè)缺點(diǎn),在豎排方向開(kāi)關(guān)關(guān)閉的時(shí)候,獲取到的方向是正確的,在開(kāi)關(guān)打開(kāi)的時(shí)候獲取到的方向是豎直方向,在橫屏等情況下獲取的方向不正確。這時(shí)候就要通過(guò)CMMotionManager來(lái)獲取方向了。(下面這段代碼也沒(méi)有加到demo中,如果有這樣的功能需求,可以在ZTImagePickerController中加上這段代碼)

- (void)p_startMotionManager{
    self.deviceOrientation = UIDeviceOrientationPortrait;
    if (_motionManager == nil) {
        _motionManager = [[CMMotionManager alloc] init];
    }
    _motionManager.deviceMotionUpdateInterval = 1/15.0;
    if (_motionManager.deviceMotionAvailable) {
        NSLog(@"Device Motion Available");
        [_motionManager startDeviceMotionUpdatesToQueue:[NSOperationQueue currentQueue]
                                            withHandler: ^(CMDeviceMotion *motion, NSError *error){
                                                [self performSelectorOnMainThread:@selector(handleDeviceMotion:) withObject:motion waitUntilDone:YES];
                                                
                                            }];
    } else {
        NSLog(@"No device motion on device.");
    }
}

- (void)p_stopMonotionManager{
    [_motionManager stopDeviceMotionUpdates];
    _motionManager = nil;
}

二、添加水印
如果要實(shí)現(xiàn)類(lèi)似騰訊的水印相機(jī)形式的水印,需要對(duì)水印專(zhuān)門(mén)做一個(gè)圖層來(lái)進(jìn)行管理。將水印圖層放在相機(jī)的最上層就可以實(shí)時(shí)看到水印了,并且可以左右切換水印。這里使用scrollview來(lái)容納每種水印樣式,如果水印樣式比較多當(dāng)然可以使用collectionView來(lái)容納。

為了讓水印圖層的手勢(shì)(scrollView的左右滑動(dòng),每種水印樣式視圖中的控件響應(yīng)手勢(shì))響應(yīng)不與相機(jī)圖層的手勢(shì)響應(yīng)不沖突,在水印圖層可以將手勢(shì)進(jìn)行攔截,根據(jù)實(shí)際情況來(lái)返回響應(yīng)手勢(shì)的視圖控件。

這里的代碼根據(jù)實(shí)際情況修改
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    
    UIView *view = [super hitTest:point withEvent:event];
    
    CGPoint tempPoint = [self.reportView convertPoint:point fromView:self];
    if(CGRectContainsPoint(self.reportView.reportTypeLb.frame, tempPoint)){
        view = self.reportView.reportTypeLb;
        return view;
    }
    
    NSInteger left = 0,top = 0, height = 0,width = self.contentSize.width;
     height = MAX(self.reportView.xmNameLb.top, self.handleProblemView.userLb.top);
    if([UIDevice currentDevice].orientation == UIDeviceOrientationLandscapeLeft){
        left = self.height - height;
        width = self.width;
    }

    if(CGRectContainsPoint(CGRectMake(left, top, width, height), point)){
            view = [self.superview.subviews objectAtIndex:0];
            view = [view hitTest:point withEvent:event];
        }else{

    }
    return view;
}

拍完照選擇好水印樣式后,將水印樣式用的空間繪制到照片上,就形成了水印照片

- (UIImage *)markedImageWithType:(XBWaterMark )waterMarkType date:(NSDate *)date user:(NSString *)user placLocation:(ZTLocationModel *)locationModel withPhone:(NSString *)phone xmType:(NSString *)xmType{
    
    if (self.size.width == 0.0 || self.size.height == 0.0) return nil;
    
    UIImage *defaultImage = nil;
    
    CGFloat scale = [UIScreen mainScreen].scale;
    if(scale >= 3) scale = 2;
    
    UIImage *image = [self thumbnailForMaxWidth:1024/scale maxHeight:1024/scale];
    
    CGSize newSize = CGSizeMake(image.size.width*image.scale/scale, image.size.height*image.scale/scale);
    
    UIView *waterMarkView = [self p_markWaterMarkView:waterMarkType date:date user:user
                                            placLocation:locationModel  withPhone:phone newSize:newSize xmType:xmType];
    //將水印樣式中的控件繪制到圖片
    UIGraphicsBeginImageContextWithOptions(newSize, YES, 0.0);
    
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSaveGState(context);
    
    [image drawInRect:CGRectMake(0.0, 0.0, newSize.width, newSize.height)];
    
    for (UIView *view in waterMarkView.subviews) {
        if([view isKindOfClass:[UIImageView class]]){
            UIImageView *iv = (UIImageView *)view;
            [iv.image drawInRect:CGRectMake(iv.left,iv.top, iv.width, iv.height)];
        }else if ([view isKindOfClass:[UILabel class]]){
            UILabel *lb = (UILabel *)view;
            UIImage *lbImage = [lb imageByRenderingView];
            [lbImage drawInRect:CGRectMake(lb.left, lb.top, lb.width, lb.height)];
        }
    }
    
    CGContextRestoreGState(context);
    
    defaultImage = UIGraphicsGetImageFromCurrentImageContext();
    
    UIGraphicsEndImageContext();
    
    return defaultImage;
}

效果圖

自定義水印相機(jī).gif

自定義相機(jī)demo

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫(kù)、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,658評(píng)論 4 61
  • iOS 蘋(píng)果官方Demo合集 字?jǐn)?shù)10517閱讀21059評(píng)論18喜歡144 其實(shí), 開(kāi)發(fā)了這么久, 不得不說(shuō), ...
    bingo居然被占了閱讀 10,615評(píng)論 2 31
  • 2013-09-27 自我管理能力就是領(lǐng)導(dǎo)力 ——小城散漫表達(dá)系列之“領(lǐng)導(dǎo)力” 火山 社會(huì)在物質(zhì)文明的推動(dòng)和深入發(fā)...
    朱明云閱讀 548評(píng)論 0 3
  • 真的好久沒(méi)有碼字了。離開(kāi)上一個(gè)公司也差不多一個(gè)月了。 在這段不長(zhǎng)的工作(實(shí)習(xí))時(shí)間里,也認(rèn)識(shí)了許多優(yōu)秀的人...
    冰美式烏龍閱讀 319評(píng)論 0 1
  • 在喧鬧的大街上漫無(wú)目的地走著,在人潮人海的都市里糜爛的生活著,在夜深人靜時(shí)倍感寂寥的徘徊著,在去往頓悟的漫漫長(zhǎng)路中...
    曖昧__閱讀 362評(píng)論 0 0

友情鏈接更多精彩內(nèi)容