背景
不知道你們是否曾經(jīng)遇到過(guò)苞氮,在做頭像上傳的時(shí)候,使用系統(tǒng)的默認(rèn)裁剪圖片的方法瓤逼,會(huì)出現(xiàn)圖片跟裁剪框發(fā)生一定的偏移笼吟。經(jīng)過(guò)我的搜索和調(diào)查,發(fā)現(xiàn)網(wǎng)上很多的方法都不適用霸旗。這個(gè)問(wèn)題就純粹是系統(tǒng)相冊(cè)的一個(gè) bug 贷帮。也不知道蘋(píng)果什么時(shí)候能夠修復(fù)好。于是就有想法诱告,自己重寫(xiě)一個(gè)裁剪圖片的控制器撵枢。
問(wèn)題展示
關(guān)于圖片跟裁剪框偏移的問(wèn)題,在 iPhoneX 上因?yàn)榇嬖诎踩嚯x精居,所以導(dǎo)致這個(gè)偏移更為明顯锄禽。先給大家看一下不同手機(jī)(模擬器)上的差異情況。 PS : iPhoneX 上已經(jīng)做了適配靴姿。
重寫(xiě)思想
一開(kāi)始沃但,以為自己要重寫(xiě)的東西包括:圖片選擇,圖片裁剪佛吓,圖片預(yù)覽宵晚,拍照這一整套東西。其實(shí)维雇,仔細(xì)觀察系統(tǒng) UIImagePickerController
淤刃,會(huì)發(fā)現(xiàn),其實(shí)真正要修改的也僅僅是圖片裁剪這個(gè)控制器谆沃。
源碼及構(gòu)建思維
1钝凶、修改 UIImagePickerController
的 allowsEditing 屬性為 NO ,令它不回自動(dòng)跳入它本身的裁剪控制器。
[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:@"獲取圖片失敗饺蔑,請(qǐng)重新選擇" success:NO];
_editIcon = NO;
}
}
3、搭建自己的裁剪控制器嗜诀。
現(xiàn)在外部的條件基本準(zhǔn)備好了猾警,所以就是單純的搭建自己的圖片裁剪控制器了。
(1).h 文件:
主要使用代理來(lái)進(jìn)行回調(diào)操作隆敢。
并創(chuàng)建公有屬性发皿,裁剪區(qū)域,圖片最大縮放比例拂蝎,是否隱藏導(dǎo)航欄穴墅。
初始化方法。
#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;
/**
* 裁剪區(qū)域 默認(rèn) 屏幕寬度顯示屏幕中心位置
*/
@property (nonatomic, assign) CGRect cropRect;
/**
* 最大縮放比例 默認(rèn)2
*/
@property (nonatomic, assign) CGFloat maxScale;
/**
* 是否隱藏導(dǎo)航欄 默認(rèn)隱藏
*/
@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温自、對(duì) image 的處理 -fixOrientation:
玄货。因?yàn)閷?duì)于 2M 以上的圖片進(jìn)行截取處理,會(huì)造成旋轉(zhuǎn) 90 度的結(jié)果悼泌。而這個(gè)原因是因?yàn)橛檬謾C(jī)拍攝出來(lái)的照片含有 EXIF 信息松捉,這就是 UIImage 的 imageOrientation 屬性。而我們對(duì) image 進(jìn)行截取或者 - drawRect
等操作的時(shí)候馆里,會(huì)下意識(shí)的忽略 imageOrientation 這個(gè)屬性對(duì)我們?cè)斐傻挠绊懓馈K晕覀儗?duì) image 處理之前,需要根據(jù) imageOrientation 屬性鸠踪,進(jìn)行 transform 的確定以舒。并對(duì)圖片進(jìn)行重繪。
// 判斷當(dāng)前旋轉(zhuǎn)方向慢哈,取最后的修正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蔓钟、對(duì)于不同的 image 來(lái)說(shuō),它們的大小也是不一樣的卵贱。我們需要在一開(kāi)始進(jìn)入我們裁剪界面的時(shí)候?qū)?image 的 frame 進(jìn)行判斷操作滥沫,來(lái)獲取 imageView 的大小。 imageView 的寬度默認(rèn)是固定 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
{
// 計(jì)算縮放比例
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;
// 設(shè)置裁剪圖片的區(qū)域
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), ^{
// 截取區(qū)域圖片
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的最小縮放比例 和 滾動(dòng)范圍
- (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 - 滾動(dòng)圖片到中間位置
- (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 -- 圖片旋轉(zhuǎn)
- (UIImage *)fixOrientation:(UIImage *)aImage
{
// 圖片為正向
if (aImage.imageOrientation == UIImageOrientationUp) {
return aImage;
}
CGAffineTransform transform = CGAffineTransformIdentity;
// 判斷當(dāng)前旋轉(zhuǎn)方向编振,取最后的修正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
如果有錯(cuò)誤的地方,希望大家多多指正臀玄。