使用过UIImagePickerController的童鞋们都知道,iOS在从相册或者摄像头后选择照片后可以编辑(选择照片的一部分剪切下来),只需要将UIImagePickerController的allowsEditing属性设为YES即可。哇,so easy!
真的如此简单吗?你如果这样想就可以不要继续看下去了。
用过的童鞋应该都会这么认为(至少我是这么认为)系统的那个渣渣真是他妈的弱爆了,主要体现在如下两点:
1、只能裁剪正方形;
2、照片边缘部分可能无法移动到裁剪区域。
哦,怎么办,产品狗就要给他弄一个长方形,甚至裁剪一个不规则形状。
哥前两天就遇到了这么狗血的事情,怎么办?哥百度、谷歌、github都了个遍,好像没找到有什么卵用的东西,于是乎只能自己动手写一个了。
好了,进入正题。如何实现一个可以剪切任意形状的图片编辑器?
需要考虑如下两个问题:
1、用户如何操作,以及界面的实现
2、如何裁剪。
首先解决第一个问题:
系统的图片编辑器是选中框不能移动,可以通过缩放和移动图片,从而确定需要选中的图片区域,此方法的优点是图片可以缩放,小图片可以放大来看清楚。但说真的,我不习惯这种操作。
我采取的方式是,将图片放在确定的位置,通过缩放和移动选中框而确定选中的图片区域。
实现如下:为了实现不规则形状选择时,未选中部分也有半透明阴影,通过在遮挡视图上绘制而成:
遮挡视图.h
#import <UIKit/UIKit.h>
typedef NS_ENUM(NSInteger, MGCEditSelectImageViewShapeStyle) {
MGCEditSelectImageViewShapeStyle_rect,
MGCEditSelectImageViewShapeStyle_circle,
};
// 遮挡视图(目前只支持矩形和圆形,其它形状可以类似实现)
@interface MGCEditSelectImageView : UIView
@property (nonatomic, readonly) CGFloat width;
@property (nonatomic, readonly) CGFloat height;
@property (nonatomic, readonly) MGCEditSelectImageViewShapeStyle style;
/**
* 画形状
*
* @param width 宽度、长半径
* @param height 高度、短半径
* @param style 形状类型,当画矩形时,若width或者height中的一个为0,那么画正方形,当画椭圆时,若width或者height为0时,画圆
*/
- (void)drawShapeWithWidth:(CGFloat)width height:(CGFloat)height shapeStyle:(MGCEditSelectImageViewShapeStyle)style;
@end
遮挡视图.m
#import "MGCEditSelectImageView.h"
@interface MGCEditSelectImageView ()
@property (nonatomic, assign) CGFloat width;
@property (nonatomic, assign) CGFloat height;
@property (nonatomic, assign) MGCEditSelectImageViewShapeStyle style;
@end
@implementation MGCEditSelectImageView
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
if (self.superview) {
// 绘制边框,
CGContextAddRect(context, CGRectMake(-self.frame.origin.x - 1, -self.frame.origin.y - 1, self.superview.frame.size.width + 2, self.superview.frame.size.height + 2));
}else{
CGContextAddRect(context, self.bounds);
}
[self drawShapeContext:context];
// 设置触笔颜色
[[UIColor whiteColor] setStroke];
// 将未选中部分蛇者为阴影半透明颜色
[[[UIColor blackColor] colorWithAlphaComponent:0.6] setFill];
CGContextDrawPath(context, kCGPathEOFillStroke);
}
#pragma mark - pravate
// 绘制形状
- (void)drawShapeContext:(CGContextRef)context
{
if (self.width || self.height) {
if (self.width <= 0 && self.height > 0) {
self.width = self.height;
}else if (self.height <= 0 && self.width > 0){
self.height = self.width;
}
CGFloat x = (self.frame.size.width - self.width) / 2;
CGFloat y = (self.frame.size.height - self.height) / 2;
CGRect rect = CGRectMake(x, y, self.width, self.height);
if (self.style == MGCEditSelectImageViewShapeStyle_rect) {
// 绘制矩形
CGContextAddRect(context, rect);
}else if (self.style == MGCEditSelectImageViewShapeStyle_circle){
// 绘制圆形
CGContextAddEllipseInRect(context, rect);
}
}
}
#pragma mark - public
- (void)drawShapeWithWidth:(CGFloat)width height:(CGFloat)height shapeStyle:(MGCEditSelectImageViewShapeStyle)style
{
self.width = width;
self.height = height;
self.style = style;
[self setNeedsDisplay];
}
@end
遮挡视图已经解决,嘿,为什么把选中部分的形状绘制在中心?因为这样的话,在移动选择区域时只需要移动整个遮挡视图,而无需每次都计算绘制形状的位置,为了保证无论如何移动,遮挡视图都会覆盖整个父视图,因此,该遮挡视图的大小必选是父视图大小的9倍。
接下来看控制器代码:
.h
#import <UIKit/UIKit.h>
#import "MGCEditSelectImageView.h"
@class MGCEditImageViewController;
@protocol MGCEditImageViewControllerDelegate <NSObject>
// 编辑完成
- (void)editDidFinsh:(MGCEditImageViewController *)controller originalImage:(UIImage *)originalImage editImage:(UIImage *)editImage;
// 编辑取消
- (void)editCancel:(MGCEditImageViewController *)controller origiinalImage:(UIImage *)originalImage;
@end
@interface MGCEditImageViewController : UIViewController
@property (nonatomic, strong) UIImage *image;
@property (nonatomic, assign) MGCEditSelectImageViewShapeStyle editStyle; //
@property (nonatomic, assign) CGFloat ratioW_Y; // 宽高比 // 默认为1
@property (nonatomic, assign) CGFloat suitableWidth; // 最适合的宽度,或者直径
@property (nonatomic, weak) id<MGCEditImageViewControllerDelegate>delegate;
@end
.m
#import "MGCEditImageViewController.h"
#import "UIImage+addition.h"
@interface MGCEditImageViewController ()
@property (nonatomic, strong) UIImageView *imageView; // 图片视图
@property (nonatomic, strong) MGCEditSelectImageView *selecterView; // 选择视图
@property (nonatomic, strong) UIView *bottomBar;
@property (nonatomic, strong) UIButton *cancelButton;
@property (nonatomic, strong) UIButton *selectButton;
@property (nonatomic, assign) CGFloat selectViewScale; // 默认为1
@property (nonatomic, assign) CGPoint panStarPoint;
@property (nonatomic, assign) CGFloat pinchLastScale;
@end
@implementation MGCEditImageViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor blackColor];
[self.view addSubview:self.imageView];
[self.view addSubview:self.bottomBar];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|-0-[_bottomBar]-0-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_bottomBar)]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[_bottomBar(44)]-0-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_bottomBar)]];
UITapGestureRecognizer *tapGes = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(doubleClickSelectView:)];
tapGes.numberOfTapsRequired = 2;
[self.view addGestureRecognizer:tapGes];
UIPinchGestureRecognizer *pinchGes = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(zoomSelectView:)];
[self.view addGestureRecognizer:pinchGes];
UIPanGestureRecognizer *panGes = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(moveSelectView:)];
[self.view addGestureRecognizer:panGes];
}
- (instancetype)init
{
if (self = [super init]) {
self.selectViewScale = 1;
self.ratioW_Y = 1;
self.editStyle = MGCEditSelectImageViewShapeStyle_rect;
self.panStarPoint = CGPointZero;
}
return self;
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self.navigationController setNavigationBarHidden:YES animated:YES];
}
- (void)viewWillDisappear:(BOOL)animated
{
[self.navigationController setNavigationBarHidden:NO animated:YES];
[[UIApplication sharedApplication] setStatusBarHidden:NO];
[super viewWillDisappear:animated];
}
- (void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
// 设置图片视图的位置和大小
[self updateImageViewFram];
// 更新选择视图的位置
[self updateSelectViewFramWithCenter:CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height / 2)];
// 指定选中区域的大小和图片视图大小的比例
self.selectViewScale = [self suitableScale];
// 根据selectViewScale从新绘制选中区域
[self updateSelectView];
}
#pragma mark - get and set
// 图片视图
- (UIImageView *)imageView
{
if (!_imageView) {
_imageView = [[UIImageView alloc] init];
_imageView.backgroundColor = [UIColor clearColor];
}
return _imageView;
}
// 选择视图(遮挡视图)
- (MGCEditSelectImageView *)selecterView
{
if (!_selecterView) {
_selecterView = [[MGCEditSelectImageView alloc] init];
_selecterView.backgroundColor = [UIColor clearColor];
_selecterView.userInteractionEnabled = NO;
_selecterView.translatesAutoresizingMaskIntoConstraints = NO;
}
return _selecterView;
}
- (void)setImage:(UIImage *)image
{
_image = image;
self.imageView.image = image;
// 更新图片视图的位置
[self updateImageViewFram];
}
// 取消按钮
- (UIButton *)cancelButton
{
if (!_cancelButton) {
_cancelButton = [self barButtonWithTitle:@"取消"];
[_cancelButton addTarget:self action:@selector(cancel:) forControlEvents:UIControlEventTouchUpInside];
}
return _cancelButton;
}
// 确定按钮
- (UIButton *)selectButton
{
if (!_selectButton) {
_selectButton = [self barButtonWithTitle:@"使用照片"];
[_selectButton addTarget:self action:@selector(confirm:) forControlEvents:UIControlEventTouchUpInside];
}
return _selectButton;
}
// 底部工具栏
- (UIView *)bottomBar
{
if (!_bottomBar) {
_bottomBar = [[UIView alloc] init];
_bottomBar.backgroundColor = [UIColor clearColor];
_bottomBar.translatesAutoresizingMaskIntoConstraints = NO;
[_bottomBar addSubview:self.cancelButton];
[_bottomBar addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|-20-[_cancelButton]" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_cancelButton)]];
[_bottomBar addConstraint:[NSLayoutConstraint constraintWithItem:self.cancelButton attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:_bottomBar attribute:NSLayoutAttributeCenterY multiplier:1 constant:0]];
[_bottomBar addSubview:self.selectButton];
[_bottomBar addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"[_selectButton]-20-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_selectButton)]];
[_bottomBar addConstraint:[NSLayoutConstraint constraintWithItem:self.selectButton attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:_bottomBar attribute:NSLayoutAttributeCenterY multiplier:1 constant:0]];
}
return _bottomBar;
}
#pragma mark - private
- (UIButton *)barButtonWithTitle:(NSString *)title
{
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
button.backgroundColor = [UIColor clearColor];
[button setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
[button setTitle:title forState:UIControlStateNormal];
button.translatesAutoresizingMaskIntoConstraints = NO;
return button;
}
// 图片和self.view的比例
- (CGFloat)ratioImageToView
{
CGFloat scaleX = self.image.size.width / self.view.frame.size.width;
CGFloat scaleY = self.image.size.height / self.view.frame.size.height;
return MAX(scaleX, scaleY);
}
// 更新图片视图视图
- (void)updateImageViewFram
{
// 获取图片和self.view的正确显示比例
CGFloat maxScale = [self ratioImageToView];
CGFloat width = self.image.size.width / maxScale;
CGFloat height = self.image.size.height / maxScale;
self.imageView.frame = CGRectMake(0, 0, width, height);
self.imageView.center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height / 2);
}
// 更新选择视图位置和大小
- (void)updateSelectViewFramWithCenter:(CGPoint)center
{
// 大小设置为父视图的9倍,保证始终覆盖父视图
self.selecterView.frame = CGRectMake(0, 0, self.view.frame.size.width * 3, self.view.frame.size.height * 3);
// 不需要考虑左右或者上下都超出的情况,因为形状的大小有selectViewScale控制
if (center.x - self.selecterView.width / 2 < self.imageView.frame.origin.x) {
center.x = self.imageView.frame.origin.x + self.selecterView.width / 2;
}else if (center.x + self.selecterView.width / 2 > CGRectGetMaxX(self.imageView.frame)){
center.x = CGRectGetMaxX(self.imageView.frame) - self.selecterView.width / 2;
}
if (center.y - self.selecterView.height / 2 < self.imageView.frame.origin.y) {
center.y = self.imageView.frame.origin.y + self.selecterView.height / 2;
}else if (center.y + self.selecterView.height / 2 > CGRectGetMaxY(self.imageView.frame)){
center.y = CGRectGetMaxY(self.imageView.frame) - self.selecterView.height / 2;
}
self.selecterView.center = center;
}
- (void)updateSelectView
{
if (self.selectViewScale > 1) {
self.selectViewScale = 1;
}else if (self.selectViewScale < 0.01){
self.selectViewScale = 0.01;
}
CGFloat imageViewRadioW_Y = self.imageView.frame.size.width / self.imageView.frame.size.height;
CGFloat width = 0;
CGFloat height = 0;
if (imageViewRadioW_Y > self.ratioW_Y) {
height = self.imageView.frame.size.height * self.selectViewScale;
width = height * self.ratioW_Y;
}else{
width = self.imageView.frame.size.width * self.selectViewScale;
height = width / self.ratioW_Y;
}
[self.selecterView drawShapeWithWidth:width height:height shapeStyle:self.editStyle];
}
// 根据给出的最合适的宽度和宽高比,计算最合适的选择形状的和图片视图的比例
- (CGFloat)suitableScale
{
if (self.suitableWidth > 0) {
CGFloat suitableHeight = self.suitableWidth / self.ratioW_Y;
return MIN(MAX(suitableHeight / self.imageView.frame.size.height, self.suitableWidth / self.imageView.frame.size.width), 1) ;
}else{
return 1;
}
}
#pragma mark - action
// 双击试,回复最合适的大小,并置于中心
- (void)doubleClickSelectView:(UITapGestureRecognizer *)sender
{
self.selectViewScale = [self suitableScale];
[self updateSelectView];
self.selecterView.center = self.view.center;
}
// 缩放选择区域
- (void)zoomSelectView:(UIPinchGestureRecognizer *)sender
{
if (self.pinchLastScale == 0) {
self.pinchLastScale = sender.scale;
return;
}
sender.scale = sender.scale - self.pinchLastScale + 1;
CGFloat scale = self.selectViewScale * sender.scale;
self.selectViewScale = scale > 1 ? 1 : scale;
[self updateSelectView];
[self updateSelectViewFramWithCenter:self.selecterView.center];
self.pinchLastScale = sender.scale;
}
// 移动选择区域,实则移动整个选择视图
- (void)moveSelectView:(UIPanGestureRecognizer *)sender
{
CGPoint startCenter = self.selecterView.center;
CGRect shapeFramToSeleView = [self.selecterView convertRect:CGRectMake(self.selecterView.frame.size.width / 2 - self.selecterView.width / 2, self.selecterView.frame.size.height / 2 - self.selecterView.height / 2, self.selecterView.width, self.selecterView.height) toView:self.view];
BOOL startPointInShapeRect = self.panStarPoint.x >= shapeFramToSeleView.origin.x && self.panStarPoint.y >= shapeFramToSeleView.origin.y && self.panStarPoint.x <= CGRectGetMaxX(shapeFramToSeleView) && self.panStarPoint.y <= CGRectGetMaxY(shapeFramToSeleView);
CGPoint gesCenter = [sender locationInView:sender.view];
if (sender.state == UIGestureRecognizerStateChanged) {
if (startPointInShapeRect) {
CGPoint center = gesCenter;
[self updateSelectViewFramWithCenter:center];
self.panStarPoint = gesCenter;
}
}else if (sender.state == UIGestureRecognizerStateEnded){
self.panStarPoint = CGPointZero;
}else if (sender.state == UIGestureRecognizerStateBegan){
self.panStarPoint = gesCenter;
}
if (self.selecterView.center.x != startCenter.x || self.selecterView.center.y != startCenter.y) {
[self updateSelectView];
}
}
- (void)confirm:(UIButton *)sender
{
if (self.delegate && [self.delegate respondsToSelector:@selector(editDidFinsh:originalImage:editImage:)]) {
[self.delegate editDidFinsh:self originalImage:self.image editImage:[self editImage]];
}
}
- (void)cancel:(UIButton *)sender
{
if (self.delegate && [self.delegate respondsToSelector:@selector(editCancel:origiinalImage:)]) {
[self.delegate editCancel:self origiinalImage:self.image];
}
}
上面完成了第一个问题,下面来解决裁剪的问题,在MGCEditImageViewController的实现中添加如下代码:
- (UIImage *)editImage
{
CGFloat ratioImageToView = [self ratioImageToView];
CGRect selectToImageView = [self.selecterView convertRect:CGRectMake(self.selecterView.frame.size.width / 2 - self.selecterView.width / 2, self.selecterView.frame.size.height / 2 - self.selecterView.height / 2, self.selecterView.width, self.selecterView.height) toView:self.imageView];
CGFloat ratio = ratioImageToView;
CGRect resultRect = CGRectMake(selectToImageView.origin.x * ratio, selectToImageView.origin.y * ratio, selectToImageView.size.width * ratio, selectToImageView.size.height * ratio);
return [self.image cutFromRect:resultRect];
}
添加一个UIImage的类别,实现如下裁剪代码:
// 裁剪图片
- (UIImage *)cutFromRect:(CGRect)rect
{
UIGraphicsBeginImageContext(CGSizeMake(self.size.width, self.size.height));
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextAddRect(context, rect);
CGContextClip(context);
[self drawInRect:CGRectMake(0, 0, self.size.width, self.size.height)];
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
UIGraphicsBeginImageContext(rect.size);
context = UIGraphicsGetCurrentContext();
[newImage drawInRect:CGRectMake(-rect.origin.x, -rect.origin.y, self.size.width, self.size.height)];
newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return newImage;
}
好了,到此,已经实现了一个可以裁剪任何比例图片的编辑器了,在裁剪圆形图片了,代码还未上传到托管服务器,需要源码的伙伴可以联系我:770322699@qq.com。
demo中,为了扩展强,裁剪圆形图片仍然裁剪的矩形,在显示时通过设置mask显示圆形,若需要裁剪圆形,在裁剪时添加圆形path即可。
下面是效果图: