特点:图片能移动,放大缩小。裁剪框能移动,自由拉伸。移动过程中裁剪框不会超出图片的范围。不在裁剪框里面会有黑色透明效果。
头文件
#import
#define SCREEN_WIDTH [UIScreen mainScreen].bounds.size.width
#define SCREEN_HEIGHT [UIScreen mainScreen].bounds.size.height
#define TouchRectSide 40.0
#define MinBoxSide 100.0
@interface FQQCropImageView : UIView
@property (nonatomic) UIImageView *imageView;
@property (nonatomic) CGRect imageViewOriginFrame;
@property (nonatomic) UIView *cropView;
@property (nonatomic) NSMutableArray *coverViews;
@property (nonatomic) NSMutableArray *decoraterViews;
@property (nonatomic) CGRect cropRect;
@property (nonatomic) CGRect concer1;
@property (nonatomic) CGRect concer2;
@property (nonatomic) CGRect concer3;
@property (nonatomic) CGRect concer4;
@property (nonatomic) CGRect topBorder;
@property (nonatomic) CGRect leftBorder;
@property (nonatomic) CGRect rightBorder;
@property (nonatomic) CGRect buttomBorder;
- (instancetype)initWithImage:(UIImage *)image;
- (UIImage *)cropImage;
- (void)setTouchRects;
- (void)layoutDecoraterViews;
- (void)layoutCoverViews;
@end
#import "FQQCropImageView.h"
typedef NS_ENUM(NSInteger, PanType){
PanNone,
PanImage,
PanCropView,
PanCropViewConcer1,
PanCropViewConcer2,
PanCropViewConcer3,
PanCropViewConcer4,
PanCropViewTopBorder,
PanCropViewButtomBorder,
PanCropViewLeftBorder,
PanCropViewRightBorder
};
@interface FQQCropImageView()
@property (nonatomic) PanType panType;
@property (nonatomic) CGRect cropViewPreFrame;
@end
- (instancetype)initWithImage:(UIImage *)image{
self = [super init];
if(self){
self.frame = CGRectMake(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
self.backgroundColor = [UIColor purpleColor];
[self setImageViewWithImage:image];
[self initCropView];
[self initCoverViews];
[self setTouchRects];
[self bindGestureRecognizer];
_imageViewOriginFrame = _imageView.frame;
}
return self;
}
关于imageView的大小,首先是以宽为基准,两边各留个50的间隙吧,再把宽高比弄成与图片等比例,如果图片是很高那种就以高为基准。
- (void)setImageViewWithImage:(UIImage *)image{
CGFloat ratio = image.size.width / image.size.height;
CGFloat width = SCREEN_WIDTH - 100;
CGFloat height = width / ratio;
if(height > SCREEN_HEIGHT - 164){
height = SCREEN_HEIGHT - 164;
width = height * ratio;
}
_imageView = [[UIImageView alloc]initWithFrame:CGRectMake(0, 0, width, height)];
_imageView.image = image;
_imageView.center = CGPointMake(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 + 32);
[self addSubview:_imageView];
}
然后是cropView,原点大小跟imageView一样。如果你喜欢可以加上一些装饰用的views
- (void)initCropView{
_cropView = [[UIView alloc]initWithFrame:_imageView.frame];
_cropView.layer.borderWidth = 0.5;
_cropView.layer.borderColor = [UIColor greenColor].CGColor;
_decoraterViews = [NSMutableArray arrayWithCapacity:8];
for(int i = 0; i < 8; i++){
UIView *decoraterView = [self getDecoraterView];
[_decoraterViews addObject:decoraterView];
[_cropView addSubview:decoraterView];
}
[self layoutDecoraterViews];
[self addSubview:_cropView];
}
没选中的黑色透明效果其实就是用4个黑色半透明的view
- (void)initCoverViews{
_coverViews = [NSMutableArray arrayWithCapacity:4];
for(int i = 0; i < 4; i++){
UIView *coverView = [self getCoverView];
[_coverViews addObject:coverView];
[self addSubview:coverView];
}
[self layoutCoverViews];
[self bringSubviewToFront:_cropView];
}
- (void)layoutCoverViews{
CGRect frame = _cropView.frame;
_coverViews[0].frame = CGRectMake(0, 0, CGRectGetMaxX(frame), CGRectGetMinY(frame));
_coverViews[1].frame = CGRectMake(0, CGRectGetMinY(frame), CGRectGetMinX(frame), SCREEN_HEIGHT - CGRectGetMinY(frame));
_coverViews[2].frame = CGRectMake(CGRectGetMaxX(frame), 0, SCREEN_WIDTH - CGRectGetMaxX(frame), CGRectGetMaxY(frame));
_coverViews[3].frame = CGRectMake(CGRectGetMinX(frame), CGRectGetMaxY(frame), SCREEN_WIDTH - CGRectGetMinX(frame), SCREEN_HEIGHT - CGRectGetMaxY(frame));
}
上面定义的CGRect类型表示9个区域,4个角,4条边和裁剪框本身。当一个拖动事件开始的touch的点在不同的区域内会有不同的拖动效果,角和边会改变裁剪框的大小,裁剪框本身会拖动裁剪框,剩下的所有区域就是拖动图片。PanType就是用来记录这10种情况。
- (void)setTouchRects{
CGPoint point = _cropView.frame.origin;
_concer1 = CGRectMake(point.x - TouchRectSide / 2, point.y - TouchRectSide / 2, TouchRectSide, TouchRectSide);
point.x += _cropView.frame.size.width;
_concer2 = CGRectMake(point.x - TouchRectSide / 2, point.y - TouchRectSide / 2, TouchRectSide, TouchRectSide);
point.y += _cropView.frame.size.height;
_concer4 = CGRectMake(point.x - TouchRectSide / 2, point.y - TouchRectSide / 2, TouchRectSide, TouchRectSide);
point.x -= _cropView.frame.size.width;
_concer3 = CGRectMake(point.x - TouchRectSide / 2, point.y - TouchRectSide / 2, TouchRectSide, TouchRectSide);
_topBorder = CGRectMake(_concer1.origin.x + _concer1.size.width, _concer1.origin.y, _cropView.frame.size.width - TouchRectSide, TouchRectSide);
CGRect temp = _topBorder;
temp.origin.y += _cropView.frame.size.height;
_buttomBorder = temp;
_leftBorder = CGRectMake(_concer1.origin.x, _concer1.origin.y + _concer1.size.height, TouchRectSide, _cropView.frame.size.height - TouchRectSide);
temp = _leftBorder;
temp.origin.x += _cropView.frame.size.width;
_rightBorder = temp;
_cropRect = CGRectMake(_concer1.origin.x + TouchRectSide, _concer1.origin.y + TouchRectSide, _cropView.frame.size.width - TouchRectSide, _cropView.frame.size.height - TouchRectSide);
}
准备工作完成,最后为整个view添加手势,是整个view,而不是cropView或者imageView
- (void)bindGestureRecognizer{
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(handlePan:)];
UIPinchGestureRecognizer *pinch = [[UIPinchGestureRecognizer alloc]initWithTarget:self action:@selector(handlePinch:)];
[self addGestureRecognizer:pan];
[self addGestureRecognizer:pinch];
}
整个项目的重点难点就是如何保证cropView始终在imageView内部,而且在图片的放大缩小过程中的实现难度还是有点高得。先看拖动手势
- (void)handlePan:(UIPanGestureRecognizer *)recognizer{
if(recognizer.state == UIGestureRecognizerStateBegan){
CGPoint touchPoint = [recognizer locationInView:self];
_cropViewPreFrame = _cropView.frame;
if(CGRectContainsPoint(_cropRect, touchPoint)) _panType = PanCropView;
else if(CGRectContainsPoint(_concer1, touchPoint)) _panType = PanCropViewConcer1;
else if(CGRectContainsPoint(_concer2, touchPoint)) _panType = PanCropViewConcer2;
else if(CGRectContainsPoint(_concer3, touchPoint)) _panType = PanCropViewConcer3;
else if(CGRectContainsPoint(_concer4, touchPoint)) _panType = PanCropViewConcer4;
else if(CGRectContainsPoint(_topBorder, touchPoint)) _panType = PanCropViewTopBorder;
else if(CGRectContainsPoint(_buttomBorder, touchPoint)) _panType = PanCropViewButtomBorder;
else if(CGRectContainsPoint(_leftBorder, touchPoint)) _panType = PanCropViewLeftBorder;
else if(CGRectContainsPoint(_rightBorder, touchPoint)) _panType = PanCropViewRightBorder;
else _panType = PanImage;
}
再开始时记录拖动类型,PanImage是最简单那种,只需保证不超过cropView
else if(recognizer.state == UIGestureRecognizerStateChanged){
CGRect nextFrame;
CGPoint transPoint;
if(_panType == PanImage){
transPoint = [recognizer translationInView:self];
nextFrame = _imageView.frame;
nextFrame.origin.x += transPoint.x;
nextFrame.origin.y += transPoint.y;
if(CGRectGetMinY(nextFrame) > CGRectGetMinY(_cropViewPreFrame)) nextFrame.origin.y = CGRectGetMinY(_cropViewPreFrame);
if(CGRectGetMinX(nextFrame) > CGRectGetMinX(_cropViewPreFrame)) nextFrame.origin.x = CGRectGetMinX(_cropViewPreFrame);
if(CGRectGetMaxX(nextFrame) < CGRectGetMaxX(_cropViewPreFrame)) nextFrame.origin.x = CGRectGetMaxX(_cropViewPreFrame) - CGRectGetWidth(nextFrame);
if(CGRectGetMaxY(nextFrame) < CGRectGetMaxY(_cropViewPreFrame)) nextFrame.origin.y = CGRectGetMaxY(_cropViewPreFrame) - CGRectGetHeight(nextFrame);
_imageView.frame = nextFrame;
[recognizer setTranslation:CGPointZero inView:self];
}
关于cropView的限制有:不能超过imageView,屏幕的左右和顶部还有顶部的标题栏,所以先记录4个最值
else{
transPoint = [recognizer translationInView:self];
nextFrame = _cropView.frame;
CGFloat maxTop = MAX(64, CGRectGetMinY(_imageView.frame));
CGFloat maxLeft = MAX(0, CGRectGetMinX(_imageView.frame));
CGFloat minRight = MIN(SCREEN_WIDTH, CGRectGetMaxX(_imageView.frame));
CGFloat minButtom = MIN(SCREEN_HEIGHT, CGRectGetMaxY(_imageView.frame));
if(_panType == PanCropView){
nextFrame.origin.x += transPoint.x;
nextFrame.origin.y += transPoint.y;
if(CGRectGetMinY(nextFrame) < maxTop) nextFrame.origin.y = maxTop;
if(CGRectGetMinX(nextFrame) < maxLeft) nextFrame.origin.x = maxLeft;
if(CGRectGetMaxX(nextFrame) > minRight){
nextFrame.origin.x = minRight - CGRectGetWidth(_cropViewPreFrame);
}
if(CGRectGetMaxY(nextFrame) > minButtom){
nextFrame.origin.y = minButtom - CGRectGetHeight(_cropViewPreFrame);
}
}
拿其中一个角和一个边做示范:
else if(_panType == PanCropViewConcer1){
nextFrame.origin.x += transPoint.x;
nextFrame.origin.y += transPoint.y;
nextFrame.size.width -= transPoint.x;
nextFrame.size.height -= transPoint.y;
if(CGRectGetWidth(nextFrame) < MinBoxSide){
nextFrame.size.width = MinBoxSide;
nextFrame.origin.x = CGRectGetMaxX(_cropViewPreFrame) - MinBoxSide;
}
if(CGRectGetHeight(nextFrame) < MinBoxSide){
nextFrame.size.height = MinBoxSide;
nextFrame.origin.y = CGRectGetMaxY(_cropViewPreFrame) - MinBoxSide;
}
if(CGRectGetMinX(nextFrame) < maxLeft){
nextFrame.origin.x = maxLeft;
nextFrame.size.width = CGRectGetMaxX(_cropViewPreFrame) - maxLeft;
}
if(CGRectGetMinY(nextFrame) < maxTop){
nextFrame.origin.y = maxTop;
nextFrame.size.height = CGRectGetMaxY(_cropViewPreFrame) - maxTop;
}
else if(_panType == PanCropViewTopBorder){
nextFrame.origin.y += transPoint.y;
nextFrame.size.height -= transPoint.y;
if(CGRectGetHeight(nextFrame) < MinBoxSide){
nextFrame.size.height = MinBoxSide;
nextFrame.origin.y = CGRectGetMaxY(_cropViewPreFrame) - MinBoxSide;
}
if(CGRectGetMinY(nextFrame) < maxTop){
nextFrame.origin.y = maxTop;
nextFrame.size.height = CGRectGetMaxY(_cropViewPreFrame) - maxTop;
}
}
最后不要忘记更新黑色半透明遮盖视图和装饰视图
_cropView.frame = nextFrame;
[self layoutCoverViews];
[self layoutDecoraterViews];
[recognizer setTranslation:CGPointZero inView:self];
拖动事件结束时,还有更新touch区域,因为cropView已经改变了
else if(recognizer.state == UIGestureRecognizerStateEnded){
[self setTouchRects];
_panType = PanNone;
}
放大缩小手势。一开始想用transfrom来实现的,因为比较方便,但是这样我不知道怎么控制图片的最大与最小,而且transfrom是默认以中心为锚点,修改锚点也很麻烦。最后还是直接对frame动手,这样更灵活,当然难度也大。同理,你可以改变图片的最大放大倍数,也能缩小。不过我发现安卓和ios的系统裁剪功能都是没有缩小的,所以我也没弄了。
- (void)handlePinch:(UIPinchGestureRecognizer *)recognizer{
CGRect currentImageFrame = _imageView.frame;
CGRect nextImageFrame = currentImageFrame;
CGFloat scale = recognizer.scale;
CGFloat nextWidth = scale * CGRectGetWidth(currentImageFrame);
CGFloat nextHeight = scale * CGRectGetHeight(currentImageFrame);
if(nextWidth > CGRectGetWidth(_imageViewOriginFrame) * 3){
nextWidth = CGRectGetWidth(_imageViewOriginFrame) * 3;
}
if(nextHeight > CGRectGetHeight(_imageViewOriginFrame) * 3){
nextHeight = CGRectGetHeight(_imageViewOriginFrame) * 3;
}
if(nextWidth < CGRectGetWidth(_imageViewOriginFrame) * 1){
nextWidth = CGRectGetWidth(_imageViewOriginFrame) * 1;
}
if(nextHeight < CGRectGetHeight(_imageViewOriginFrame) * 1){
nextHeight = CGRectGetHeight(_imageViewOriginFrame) * 1;
}
nextImageFrame.size.width = nextWidth;
nextImageFrame.size.height = nextHeight;
//确定大小后才能确定原点,下面的效果是以中心为锚点,但并不是最终结果,因为有些情况需要不同的锚点
nextImageFrame.origin.x -= (CGRectGetWidth(nextImageFrame) - CGRectGetWidth(currentImageFrame)) / 2;
nextImageFrame.origin.y -= (CGRectGetHeight(nextImageFrame) - CGRectGetHeight(currentImageFrame)) / 2;
CGRect cropViewFrame = _cropView.frame;
//当你在缩小过程中已经靠着其中一条或者相邻的两条边时要改变锚点,又或者裁剪框的宽或高要超过图片的大小时,连裁剪框的大小也要跟着变小。其他的情况就直接使用上面的nextImageFrame就行
if(scale < 1 && !CGRectContainsRect(nextI mageFrame, cropViewFrame)){
if(CGRectGetWidth(nextImageFrame) <= CGRectGetWidth(cropViewFrame) || CGRectGetHeight(nextImageFrame) <= CGRectGetHeight(cropViewFrame)){
//裁剪框过长,要跟着缩小
if(CGRectGetWidth(nextImageFrame) <= CGRectGetWidth(cropViewFrame)){
cropViewFrame.size.width = CGRectGetWidth(nextImageFrame);
cropViewFrame.origin.x = CGRectGetMinX(nextImageFrame);
if(CGRectGetMinY(nextImageFrame) > CGRectGetMinY(cropViewFrame)){
nextImageFrame.origin.y = CGRectGetMinY(currentImageFrame);
}
if(CGRectGetMaxY(nextImageFrame) < CGRectGetMaxY(cropViewFrame)){
nextImageFrame.origin.y = CGRectGetMaxY(cropViewFrame) - CGRectGetHeight(nextImageFrame);
}
}
//裁剪框过高,要跟着缩小
if(CGRectGetHeight(nextImageFrame) <= CGRectGetHeight(cropViewFrame)){
cropViewFrame.size.height = CGRectGetHeight(nextImageFrame);
cropViewFrame.origin.y = CGRectGetMinY(nextImageFrame);
if(CGRectGetMinX(nextImageFrame) > CGRectGetMinX(cropViewFrame)){
nextImageFrame.origin.x = CGRectGetMinX(currentImageFrame);
}
if(CGRectGetMaxX(nextImageFrame) < CGRectGetMaxX(cropViewFrame)){
nextImageFrame.origin.x = CGRectGetMaxX(cropViewFrame) - CGRectGetWidth(nextImageFrame);
}
}
//更新裁剪框
_cropView.frame = cropViewFrame;
[self layoutCoverViews];
[self layoutDecoraterViews];
}else{
//这种只需改变锚点
if(CGRectGetMinX(nextImageFrame) > CGRectGetMinX(cropViewFrame)){
nextImageFrame.origin.x = CGRectGetMinX(currentImageFrame);
}
if(CGRectGetMinY(nextImageFrame) > CGRectGetMinY(cropViewFrame)){
nextImageFrame.origin.y = CGRectGetMinY(currentImageFrame);
}
if(CGRectGetMaxX(nextImageFrame) < CGRectGetMaxX(cropViewFrame)){
nextImageFrame.origin.x = CGRectGetMaxX(cropViewFrame) - CGRectGetWidth(nextImageFrame);
}
if(CGRectGetMaxY(nextImageFrame) < CGRectGetMaxY(cropViewFrame)){
nextImageFrame.origin.y = CGRectGetMaxY(cropViewFrame) - CGRectGetHeight(nextImageFrame);
}
}
}
//最后更新图片
_imageView.frame = nextImageFrame;
recognizer.scale = 1.0;
}
最后就是裁剪功能了。要注意的是图片的大小跟imageView的大小是不一样的,因为我们事前已经把imageView改成跟image等宽高比的,所以图片拉伸后没有变形。我们裁剪图片是在原图上进行,也就是说cropView的frame还不是正确的,必须先处理先
- (UIImage *)cropImage{
CGRect imageFrame = _imageView.frame;
CGRect cropFrame = _cropView.frame;
CGRect targetFrame = CGRectMake(CGRectGetMinX(cropFrame) - CGRectGetMinX(imageFrame), CGRectGetMinY(cropFrame) - CGRectGetMinY(imageFrame), CGRectGetWidth(cropFrame), CGRectGetHeight(cropFrame));
float scale = _imageView.image.size.width / imageFrame.size.width;
targetFrame.origin.x *= scale;
targetFrame.origin.y *= scale;
targetFrame.size.width *= scale;
targetFrame.size.height *= scale;
return [UIImage imageWithCGImage:CGImageCreateWithImageInRect(_imageView.image.CGImage, targetFrame)];
}
完成。因为项目需要,在网上找了找,发现ios的第三方真的很少而且功能很单一啊,安卓随便就是一堆,无奈只能看着手上安卓手机的裁剪功能自己写个。其实这些功能特点我都是按照安卓的来写的,感觉安卓裁剪功能的比ios的好啊。
项目源码的Demo在Github上有,有兴趣的可以去看看https://github.com/FQQA/iOS