系统截取头像偏移问题

2018-07-19  本文已影响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

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

上一篇下一篇

猜你喜欢

热点阅读