美文网首页
系统截取头像偏移问题

系统截取头像偏移问题

作者: 乾坤醉心尘 | 来源:发表于2018-07-19 18:12 被阅读132次

    背景

    不知道你们是否曾经遇到过,在做头像上传的时候,使用系统的默认裁剪图片的方法,会出现图片跟裁剪框发生一定的偏移。经过我的搜索和调查,发现网上很多的方法都不适用。这个问题就纯粹是系统相册的一个 bug 。也不知道苹果什么时候能够修复好。于是就有想法,自己重写一个裁剪图片的控制器。

    问题展示

    关于图片跟裁剪框偏移的问题,在 iPhoneX 上因为存在安全距离,所以导致这个偏移更为明显。先给大家看一下不同手机(模拟器)上的差异情况。 PS : iPhoneX 上已经做了适配。


    iPhone7上裁剪图片偏移展示图.png iPhoneX上裁剪图片偏移展示图.png

    重写思想

    一开始,以为自己要重写的东西包括:图片选择,图片裁剪,图片预览,拍照这一整套东西。其实,仔细观察系统 UIImagePickerController ,会发现,其实真正要修改的也仅仅是图片裁剪这个控制器。

    源码及构建思维

    1、修改 UIImagePickerController 的 allowsEditing 属性为 NO ,令它不回自动跳入它本身的裁剪控制器。

     [self.imagePickerViewController setAllowsEditing:NO];
    

    2、修改选择图片之后的代理方法 -imagePickerController:didFinishPickingMediaWithInfo:,在这里,我们控制其跳入自己的控制器。

    - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
    {
        UIImage *image = [info objectForKey:UIImagePickerControllerOriginalImage];
        if (image && picker) {
            ZKRAccountAvatarCropController *cropVC = [[ZKRAccountAvatarCropController alloc] initWithImage:image];
            cropVC.delegate = self;
            [picker pushViewController:cropVC animated:YES];
        } else {
            [ZKRUtilities showStatusBarMsg:@"获取图片失败,请重新选择" success:NO];
            _editIcon = NO;
        }
    }
    

    3、搭建自己的裁剪控制器。

    现在外部的条件基本准备好了,所以就是单纯的搭建自己的图片裁剪控制器了。

    (1).h 文件:

    主要使用代理来进行回调操作。
    并创建公有属性,裁剪区域,图片最大缩放比例,是否隐藏导航栏。
    初始化方法。

    #import <UIKit/UIKit.h>
    
    @class ZKRAccountAvatarCropController;
    
    @protocol ZKRAccountAvatarCropControllerDelegate <NSObject>
    
    - (void)avatarCropController:(ZKRAccountAvatarCropController *)cropController didFinishCropWithImage:(UIImage *)image;
    
    - (void)avatarCropControllerDidCancel:(ZKRAccountAvatarCropController *)cropController;
    
    @end
    
    @interface ZKRAccountAvatarCropController : UIViewController
    
    @property (nonatomic, weak) id<ZKRAccountAvatarCropControllerDelegate>delegate;
    
    /**
     *  裁剪区域  默认 屏幕宽度显示屏幕中心位置
     */
    @property (nonatomic, assign) CGRect cropRect;
    
    /**
     *  最大缩放比例  默认2
     */
    @property (nonatomic, assign) CGFloat maxScale;
    
    /**
     *  是否隐藏导航栏  默认隐藏
     */
    @property (nonatomic, assign) BOOL navigationBarHidden;
    
    /**
     *  初始化方法
     *
     *  @param image 待裁剪图片
     *
     *  @return ZKRAccountAvatarCropController
     */
    - (instancetype)initWithImage:(UIImage *)image NS_DESIGNATED_INITIALIZER;
    
    - (instancetype)init NS_UNAVAILABLE;
    
    - (instancetype)initWithCoder:(NSCoder *)aDecoder NS_UNAVAILABLE;
    
    @end
    

    (2) .m 文件

    主要思想:
    1、对 image 的处理 -fixOrientation: 。因为对于 2M 以上的图片进行截取处理,会造成旋转 90 度的结果。而这个原因是因为用手机拍摄出来的照片含有 EXIF 信息,这就是 UIImage 的 imageOrientation 属性。而我们对 image 进行截取或者 - drawRect 等操作的时候,会下意识的忽略 imageOrientation 这个属性对我们造成的影响。所以我们对 image 处理之前,需要根据 imageOrientation 属性,进行 transform 的确定。并对图片进行重绘。

        // 判断当前旋转方向,取最后的修正transform
        switch (aImage.imageOrientation) {
            case UIImageOrientationDown:
            case UIImageOrientationDownMirrored:
                transform = CGAffineTransformTranslate(transform, aImage.size.width, aImage.size.height);
                transform = CGAffineTransformRotate(transform, M_PI);
                break;
    
            case UIImageOrientationLeft:
            case UIImageOrientationLeftMirrored:
                transform = CGAffineTransformTranslate(transform, aImage.size.width, 0);
                transform = CGAffineTransformRotate(transform, M_PI_2);
                break;
    
            case UIImageOrientationRight:
            case UIImageOrientationRightMirrored:
                transform = CGAffineTransformTranslate(transform, 0, aImage.size.height);
                transform = CGAffineTransformRotate(transform, -M_PI_2);
                break;
            default:
                break;
        }
    
    

    2、对于不同的 image 来说,它们的大小也是不一样的。我们需要在一开始进入我们裁剪界面的时候对 image 的 frame 进行判断操作,来获取 imageView 的大小。 imageView 的宽度默认是固定 self.view.frame.size.widht

    3、裁剪图片需要放到多线程中去。

    看代码:

    //
    //  ZKRAccountAvatarCropController.m
    //
    //  Created by zhengqiankun on 2018/5/30.
    //  Copyright © 2018年 ZAKER. All rights reserved.
    //
    
    #import "ZKRAccountAvatarCropController.h"
    #import "ZKRAccountAvatarMaskView.h"
    
    #define PADDING_BUTTON_LEFT   15
    #define PADDING_BUTTON_RIGHT  15
    #define PADDING_BUTTON_BOTTOM 15
    #define WIDTH_BUTTON          60
    #define HEIGHT_BUTTON         40
    
    #define HEIGHT_BUTTONVIEW     70
    
    @interface ZKRAccountAvatarCropController ()<UIScrollViewDelegate>
    
    @property (nonatomic, strong) UIScrollView *scrollView;
    @property (nonatomic, strong) UIImageView *imageView;
    @property (nonatomic, strong) ZKRAccountAvatarMaskView *maskView;
    @property (nonatomic, strong) UIView *buttonView;
    @property (nonatomic, strong) UIButton *cancelButton;
    @property (nonatomic, strong) UIButton *cropButton;
    @property (nonatomic, strong) UIImage *image; // 待裁剪的图片
    
    @property (nonatomic, assign) BOOL originalNaviBarHidden;
    
    @end
    
    @implementation ZKRAccountAvatarCropController
    
    - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
    {
        return [self initWithImage:nil];
    }
    
    - (instancetype)initWithImage:(UIImage *)image
    {
        self = [super initWithNibName:nil bundle:nil];
        if (self) {
            _image = image;
        }
        return self;
    }
    
    - (void)viewDidLoad
    {
        [super viewDidLoad];
    
        self.view.backgroundColor = [UIColor blackColor];
    
        CGRect bounds = self.view.bounds;
        CGFloat currentWidth = bounds.size.width;
        CGFloat currentHeight = bounds.size.height;
    
        _navigationBarHidden = YES;
    
        _maxScale = 1.5f;
        _cropRect = CGRectMake(0, (currentHeight - currentWidth) / 2, currentWidth, currentWidth);
    
        if (_image) {
            _image = [self fixOrientation:_image];
        }
    
        [self initSubviews];
    }
    
    - (CGRect)imageViewRectWithImage:(UIImage *)image
    {
        CGRect bounds = self.view.bounds;
        CGFloat currentWidth = bounds.size.width;
    
        CGFloat width = 0;
        CGFloat height = 0;
    
        width = currentWidth;
        height = image.size.height / image.size.width * width;
        if (height < currentWidth) {
            height = currentWidth;
            width = image.size.width / image.size.height * height;
        }
    
        return CGRectMake(0, 0, width, height);
    }
    
    - (void)viewDidLayoutSubviews
    {
        [super viewDidLayoutSubviews];
    
        [self layoutSubViews];
    }
    
    - (void)viewWillAppear:(BOOL)animated
    {
        [super viewWillAppear:animated];
        _originalNaviBarHidden = self.navigationController.navigationBar.isHidden;
        self.navigationController.navigationBar.hidden = _navigationBarHidden;
    
        [_maskView setMaskRect:self.cropRect];
        [self refreshScrollView];
    }
    
    - (void)viewWillDisappear:(BOOL)animated
    {
        [super viewWillDisappear:animated];
        self.navigationController.navigationBar.hidden = _originalNaviBarHidden;
    }
    
    - (void)setNavigationBarHidden:(BOOL)navigationBarHidden
    {
        _navigationBarHidden = navigationBarHidden;
        if (self.navigationController) {
            self.navigationController.navigationBar.hidden = navigationBarHidden;
        }
    }
    
    - (void)initSubviews
    {
        _scrollView = [[UIScrollView alloc] init];
        _scrollView.delegate = self;
        _scrollView.alwaysBounceVertical = YES;
        _scrollView.alwaysBounceHorizontal = YES;
        _scrollView.showsVerticalScrollIndicator = NO;
        _scrollView.showsHorizontalScrollIndicator = NO;
        [self.view addSubview:_scrollView];
    
        _maskView = [[ZKRAccountAvatarMaskView alloc] init];
        _maskView.userInteractionEnabled = NO;
        [self.view addSubview:_maskView];
    
        _buttonView = [[UIView alloc] init];
        _buttonView.backgroundColor = [UIColor colorWithRed:20 / 255.0 green:20 / 255.0 blue:20 / 255.0 alpha:0.8];
        [self.view addSubview:_buttonView];
    
        _cropButton = [[UIButton alloc] init];
        [_cropButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
        [_cropButton.titleLabel setFont:[UIFont systemFontOfSize:17]];
        [_cropButton setTitle:@"选取" forState:UIControlStateNormal];
        [_cropButton addTarget:self action:@selector(cropImageAction) forControlEvents:UIControlEventTouchUpInside];
        [self.view addSubview:_cropButton];
    
        _cancelButton = [[UIButton alloc] init];
        [_cancelButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
        [_cancelButton.titleLabel setFont:[UIFont systemFontOfSize:17]];
        [_cancelButton setTitle:@"取消" forState:UIControlStateNormal];
        [_cancelButton addTarget:self action:@selector(cancelAction) forControlEvents:UIControlEventTouchUpInside];
        [self.view addSubview:_cancelButton];
    
        [self layoutSubViews];
    }
    
    - (void)layoutSubViews
    {
        CGRect bounds = self.view.bounds;
        CGFloat currentWidth = bounds.size.width;
        CGFloat currentHeight = bounds.size.height;
    
        _cropRect = CGRectMake(0, (currentHeight - currentWidth) / 2, currentWidth, currentWidth);
    
        _scrollView.frame = bounds;
    
        if (!_imageView) {
            _imageView = [[UIImageView alloc] initWithImage:_image];
            _imageView.frame = [self imageViewRectWithImage:_image];
            [_scrollView addSubview:_imageView];
        } else {
            _imageView.frame = [self imageViewRectWithImage:_image];
            _imageView.image = _image;
        }
    
        _scrollView.contentSize = _imageView.frame.size;
        CGRect scrollViewFrame = _scrollView.frame;
        _maskView.frame = scrollViewFrame;
        CGFloat buttonViewY = bounds.size.height - HEIGHT_BUTTONVIEW;
        if ([UIScreen whl_isIPhoneX]) {
            buttonViewY = bounds.size.height - HEIGHT_BUTTONVIEW - WHL_IPHONEX_BOTTOM_INSET;
        }
        _buttonView.frame = CGRectMake(0, buttonViewY, bounds.size.width, HEIGHT_BUTTONVIEW);
    
        CGFloat buttonY = [UIScreen whl_isIPhoneX] ? currentHeight - HEIGHT_BUTTON - PADDING_BUTTON_BOTTOM - WHL_IPHONEX_BOTTOM_INSET : currentHeight - HEIGHT_BUTTON - PADDING_BUTTON_BOTTOM;
        _cropButton.frame = CGRectMake(currentWidth - WIDTH_BUTTON - PADDING_BUTTON_RIGHT, buttonY, WIDTH_BUTTON, HEIGHT_BUTTON);
        _cancelButton.frame = CGRectMake(PADDING_BUTTON_LEFT, buttonY, WIDTH_BUTTON, HEIGHT_BUTTON);
    }
    
    - (void)cropImageAction
    {
        [self cropImage];
    }
    
    - (void)cancelAction
    {
        if ([self.delegate respondsToSelector:@selector(avatarCropControllerDidCancel:)]) {
            [self.delegate avatarCropControllerDidCancel:self];
        }
    }
    
    #pragma mark - 裁剪图片
    - (void)cropImage
    {
        // 计算缩放比例
        CGFloat scale = _imageView.image.size.height / _imageView.frame.size.height;
        CGFloat imageScale = _imageView.image.scale;
    
        CGFloat width = self.cropRect.size.width * scale * imageScale;
        CGFloat height = self.cropRect.size.height * scale * imageScale;
        CGFloat x = (self.cropRect.origin.x + _scrollView.contentOffset.x) * scale * imageScale;
        CGFloat y = (self.cropRect.origin.y + _scrollView.contentOffset.y) * scale * imageScale;
    
        // 设置裁剪图片的区域
        CGRect rect = CGRectMake(x, y, width, height);
    
        CGImageRef imageRef = CGImageCreateWithImageInRect(self.imageView.image.CGImage, rect);
        
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            // 截取区域图片
            UIImage *image = [UIImage imageWithCGImage:imageRef];
            CGImageRelease(imageRef);
            
            dispatch_async(dispatch_get_main_queue(), ^{
                if ([self.delegate respondsToSelector:@selector(avatarCropController:didFinishCropWithImage:)]) {
                    [self.delegate avatarCropController:self didFinishCropWithImage:image];
                }
            });
        });
    }
    
    #pragma mark - UIScrollViewDelegate 返回缩放的view
    - (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
    {
        return _imageView;
    }
    
    #pragma mark - 处理scrollView的最小缩放比例 和 滚动范围
    - (void)refreshScrollView
    {
        CGFloat top = self.cropRect.origin.y - 20;
    
        CGFloat minScale = 0.f;
        if (_imageView.image.size.height > _imageView.image.size.width) {
            minScale = self.cropRect.size.width / _imageView.bounds.size.width;
        } else {
            minScale = self.cropRect.size.height / _imageView.bounds.size.height;
        }
        CGFloat bottom = self.cropRect.origin.y;
        if ([UIScreen whl_isIPhoneX]) {
            top = self.cropRect.origin.y - WHL_IPHONEX_TOP_INSET;
            bottom = bottom - WHL_IPHONEX_BOTTOM_INSET;
        }
    
        _scrollView.maximumZoomScale = self.maxScale;
        _scrollView.minimumZoomScale = minScale;
        _scrollView.contentInset = UIEdgeInsetsMake(top, 0, bottom, 0);
    
        [self scrollToCenter];
    }
    
    #pragma mark - 滚动图片到中间位置
    - (void)scrollToCenter
    {
        CGRect bounds = self.view.bounds;
        CGFloat currentWidth = bounds.size.width;
        CGFloat currentHeight = bounds.size.height;
    
        CGFloat x = (_imageView.frame.size.width - currentWidth) / 2;
        CGFloat y = (_imageView.frame.size.height - currentHeight) / 2 + 20;
        if ([UIScreen whl_isIPhoneX]) {
            y = (_imageView.frame.size.height - currentHeight) / 2  + WHL_IPHONEX_TOP_INSET;
        }
        _scrollView.contentOffset = CGPointMake(x, y);
    }
    
    - (UIStatusBarStyle)preferredStatusBarStyle
    {
        return UIStatusBarStyleLightContent;
    }
    
    #pragma mark -- 图片旋转
    - (UIImage *)fixOrientation:(UIImage *)aImage
    {
        // 图片为正向
        if (aImage.imageOrientation == UIImageOrientationUp) {
            return aImage;
        }
    
        CGAffineTransform transform = CGAffineTransformIdentity;
    
        // 判断当前旋转方向,取最后的修正transform
        switch (aImage.imageOrientation) {
            case UIImageOrientationDown:
            case UIImageOrientationDownMirrored:
                transform = CGAffineTransformTranslate(transform, aImage.size.width, aImage.size.height);
                transform = CGAffineTransformRotate(transform, M_PI);
                break;
    
            case UIImageOrientationLeft:
            case UIImageOrientationLeftMirrored:
                transform = CGAffineTransformTranslate(transform, aImage.size.width, 0);
                transform = CGAffineTransformRotate(transform, M_PI_2);
                break;
    
            case UIImageOrientationRight:
            case UIImageOrientationRightMirrored:
                transform = CGAffineTransformTranslate(transform, 0, aImage.size.height);
                transform = CGAffineTransformRotate(transform, -M_PI_2);
                break;
            default:
                break;
        }
    
        switch (aImage.imageOrientation) {
            case UIImageOrientationUpMirrored:
            case UIImageOrientationDownMirrored:
                transform = CGAffineTransformTranslate(transform, aImage.size.width, 0);
                transform = CGAffineTransformScale(transform, -1, 1);
                break;
    
            case UIImageOrientationLeftMirrored:
            case UIImageOrientationRightMirrored:
                transform = CGAffineTransformTranslate(transform, aImage.size.height, 0);
                transform = CGAffineTransformScale(transform, -1, 1);
                break;
            default:
                break;
        }
    
        CGContextRef ctx = CGBitmapContextCreate(NULL, aImage.size.width, aImage.size.height,
                                                 CGImageGetBitsPerComponent(aImage.CGImage), 0,
                                                 CGImageGetColorSpace(aImage.CGImage),
                                                 CGImageGetBitmapInfo(aImage.CGImage));
        CGContextConcatCTM(ctx, transform);
        switch (aImage.imageOrientation) {
            case UIImageOrientationLeft:
            case UIImageOrientationLeftMirrored:
            case UIImageOrientationRight:
            case UIImageOrientationRightMirrored:
                CGContextDrawImage(ctx, CGRectMake(0, 0, aImage.size.height, aImage.size.width), aImage.CGImage);
                break;
            default:
                CGContextDrawImage(ctx, CGRectMake(0, 0, aImage.size.width, aImage.size.height), aImage.CGImage);
                break;
        }
    
        CGImageRef cgimg = CGBitmapContextCreateImage(ctx);
        UIImage *img = [UIImage imageWithCGImage:cgimg];
        CGContextRelease(ctx);
        CGImageRelease(cgimg);
        return img;
    }
    
    @end
    
    

    4、配套 maskView ,裁剪框

    .h 代码:

    //
    //  ZKRAccountAvatarMaskView.h
    //
    //  Created by zhengqiankun on 2018/5/30.
    //  Copyright © 2018年 ZAKER. All rights reserved.
    //
    
    #import <UIKit/UIKit.h>
    
    @interface ZKRAccountAvatarMaskView : UIView
    
    @property (nonatomic, assign) CGRect maskRect;
    
    - (void)setMaskRect:(CGRect)rect;
    
    @end
    
    

    .m 代码

    //
    //  ZKRAccountAvatarMaskView.m
    //
    //  Created by zhengqiankun on 2018/5/30.
    //  Copyright © 2018年 ZAKER. All rights reserved.
    //
    
    #import "ZKRAccountAvatarMaskView.h"
    
    @interface ZKRAccountAvatarMaskView ()
    
    @property (nonatomic, strong) UIView *rectView;
    
    @end
    
    @implementation ZKRAccountAvatarMaskView
    
    - (instancetype)initWithFrame:(CGRect)frame
    {
        if (self = [super initWithFrame:frame]) {
            self.backgroundColor = [UIColor clearColor];
            _rectView = [[UIView alloc] init];
            _rectView.clipsToBounds = YES;
            _rectView.layer.borderColor = [UIColor whiteColor].CGColor;
            _rectView.layer.borderWidth = 2;
            [self addSubview:_rectView];
        }
        return self;
    }
    
    - (void)drawRect:(CGRect)rect
    {
        [super drawRect:rect];
    
        CGContextRef context = UIGraphicsGetCurrentContext();
        CGContextAddRect(context, self.maskRect);
        CGContextAddRect(context, rect);
        [[UIColor colorWithRed:0 green:0 blue:0 alpha:0.4] setFill];
        CGContextDrawPath(context, kCGPathEOFill);
    }
    
    - (void)setMaskRect:(CGRect)rect
    {
        if (!CGRectEqualToRect(_maskRect, rect)) {
            _maskRect = rect;
            _rectView.frame = rect;
            [self setNeedsDisplay];
        }
    }
    
    @end
    
    

    如果有错误的地方,希望大家多多指正。

    相关文章

      网友评论

          本文标题:系统截取头像偏移问题

          本文链接:https://www.haomeiwen.com/subject/qvtxmftx.html