背景
不知道你们是否曾经遇到过,在做头像上传的时候,使用系统的默认裁剪图片的方法,会出现图片跟裁剪框发生一定的偏移。经过我的搜索和调查,发现网上很多的方法都不适用。这个问题就纯粹是系统相册的一个 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
如果有错误的地方,希望大家多多指正。
网友评论