iOS中实现动态区域裁剪图片功能实例

前言

相信大家应该都有所体会,裁剪图片功能在很多上传图片的场景里都需要用到,一方面应用服务器可能对图片的尺寸大小有限制,因而希望上传的图片都是符合规定的,另一方面,用户可能希望只上传图片中的部分内容,突出图片中关键的信息。而为了满足用户多种多样的裁剪需求,就需要裁剪图片时能支持由用户动态地改变裁剪范围、裁剪尺寸等。

动态裁剪图片的基本过程大致可以分为以下几步

  • 显示图片与裁剪区域
  • 支持移动和缩放图片
  • 支持手势改变裁剪区域
  • 进行图片裁剪并获得裁剪后的图片

显示图片与裁剪区域

显示图片

在裁剪图片之前,首先我们要在页面上显示待裁剪的图片,如下图所示

这一步比较简单,配置一个 UIImageView 来放置图片即可。但是要注意一点,UIImageView 有多种 contentMode,最常见有三种

  • UIViewContentModeScaleToFill
  • UIViewContentModeScaleAspectFit
  • UIViewContentModeScaleAspectFill

三者区别可以看下面的比较

UIViewContentModeScaleToFill

UIViewContentModeScaleAspectFit

UIViewContentModeScaleAspectFill

可以看出,ScaleToFill 会改变图片的长宽比例来铺满整个 UIImageView,ScaleAspectFill 则会保持图片比例来铺满,从而会有部分图片内容超出 UIImageView 区域的情况,而 ScaleAspectFit 则会保证图片比例不变,同时图片内容都显示在 UIImageView 中,即使无法铺满 UIImageView。

因此不同显示模式会影响到我们最终显示到屏幕上的图片的样子,而在裁剪过程中最理想的放置图片的模式则是,图片的短边刚好铺满裁剪区域的短边,而长边至少不会小于裁剪区域的长边,这就要求我们要考虑裁剪区域的长宽来放置我们的图片。

裁剪区域

接下来我们要放置我们的裁剪区域,它的样子如下所示

裁剪区域本身就是在 UIImageView 上放上一层 UIView,再在 UIView 上绘制出一个白边框的方格 Layer。

首先自定义一个 CAShapeLayer

#import <QuartzCore/QuartzCore.h>

@interface YasicClipAreaLayer : CAShapeLayer

@property(assign, nonatomic) NSInteger cropAreaLeft;
@property(assign, nonatomic) NSInteger cropAreaTop;
@property(assign, nonatomic) NSInteger cropAreaRight;
@property(assign, nonatomic) NSInteger cropAreaBottom;

- (void)setCropAreaLeft:(NSInteger)cropAreaLeft CropAreaTop:(NSInteger)cropAreaTop CropAreaRight:(NSInteger)cropAreaRight CropAreaBottom:(NSInteger)cropAreaBottom;

@end

@implementation YasicClipAreaLayer

- (instancetype)init
{
 self = [super init];
 if (self) {
 _cropAreaLeft = 50;
 _cropAreaTop = 50;
 _cropAreaRight = SCREEN_WIDTH - self.cropAreaLeft;
 _cropAreaBottom = 400;
 }
 return self;
}

- (void)drawInContext:(CGContextRef)ctx
{
 UIGraphicsPushContext(ctx);

 CGContextSetStrokeColorWithColor(ctx, [UIColor whiteColor].CGColor);
 CGContextSetLineWidth(ctx, lineWidth);
 CGContextMoveToPoint(ctx, self.cropAreaLeft, self.cropAreaTop);
 CGContextAddLineToPoint(ctx, self.cropAreaLeft, self.cropAreaBottom);
 CGContextSetShadow(ctx, CGSizeMake(2, 0), 2.0);
 CGContextStrokePath(ctx);

 CGContextSetStrokeColorWithColor(ctx, [UIColor whiteColor].CGColor);
 CGContextSetLineWidth(ctx, lineWidth);
 CGContextMoveToPoint(ctx, self.cropAreaLeft, self.cropAreaTop);
 CGContextAddLineToPoint(ctx, self.cropAreaRight, self.cropAreaTop);
 CGContextSetShadow(ctx, CGSizeMake(0, 2), 2.0);
 CGContextStrokePath(ctx);

 CGContextSetStrokeColorWithColor(ctx, [UIColor whiteColor].CGColor);
 CGContextSetLineWidth(ctx, lineWidth);
 CGContextMoveToPoint(ctx, self.cropAreaRight, self.cropAreaTop);
 CGContextAddLineToPoint(ctx, self.cropAreaRight, self.cropAreaBottom);
 CGContextSetShadow(ctx, CGSizeMake(-2, 0), 2.0);
 CGContextStrokePath(ctx);

 CGContextSetStrokeColorWithColor(ctx, [UIColor whiteColor].CGColor);
 CGContextSetLineWidth(ctx, lineWidth);
 CGContextMoveToPoint(ctx, self.cropAreaLeft, self.cropAreaBottom);
 CGContextAddLineToPoint(ctx, self.cropAreaRight, self.cropAreaBottom);
 CGContextSetShadow(ctx, CGSizeMake(0, -2), 2.0);
 CGContextStrokePath(ctx);

 UIGraphicsPopContext();
}

- (void)setCropAreaLeft:(NSInteger)cropAreaLeft
{
 _cropAreaLeft = cropAreaLeft;
 [self setNeedsDisplay];
}

- (void)setCropAreaTop:(NSInteger)cropAreaTop
{
 _cropAreaTop = cropAreaTop;
 [self setNeedsDisplay];
}

- (void)setCropAreaRight:(NSInteger)cropAreaRight
{
 _cropAreaRight = cropAreaRight;
 [self setNeedsDisplay];
}

- (void)setCropAreaBottom:(NSInteger)cropAreaBottom
{
 _cropAreaBottom = cropAreaBottom;
 [self setNeedsDisplay];
}

- (void)setCropAreaLeft:(NSInteger)cropAreaLeft CropAreaTop:(NSInteger)cropAreaTop CropAreaRight:(NSInteger)cropAreaRight CropAreaBottom:(NSInteger)cropAreaBottom
{
 _cropAreaLeft = cropAreaLeft;
 _cropAreaRight = cropAreaRight;
 _cropAreaTop = cropAreaTop;
 _cropAreaBottom = cropAreaBottom;

 [self setNeedsDisplay];
}

@end

这里 layer 有几个属性 cropAreaLeft、cropAreaRight、cropAreaTop、cropAreaBottom,从命名上可以看出这几个属性定义了这个 layer 上绘制的白边框裁剪区域的坐标信息。还暴露了一个方法用于配置这四个属性。

然后在 CAShapeLayer 内部,重点在于复写 drawInContext 方法,这个方法负责直接在图层上绘图,复写的方法主要做的事情是根据上面四个属性 cropAreaLeft、cropAreaRight、cropAreaTop、cropAreaBottom 绘制出封闭的四条线,这样就能表示裁剪区域的边界了。

要注意的是 drawInContext 方法不能手动显示调用,必须通过调用 setNeedsDisplay 或者 setNeedsDisplayInRect 让系统自动调该方法。

在裁剪页面里,我们放置了一个 cropView,然后将自定义的 CAShaplayer 加入到这个 view 上

 self.cropView.layer.sublayers = nil;
 YasicClipAreaLayer * layer = [[YasicClipAreaLayer alloc] init];

 CGRect cropframe = CGRectMake(self.cropAreaX, self.cropAreaY, self.cropAreaWidth, self.cropAreaHeight);
 UIBezierPath * path = [UIBezierPath bezierPathWithRoundedRect:self.cropView.frame cornerRadius:0];
 UIBezierPath * cropPath = [UIBezierPath bezierPathWithRect:cropframe];
 [path appendPath:cropPath];
 layer.path = path.CGPath;

 layer.fillRule = kCAFillRuleEvenOdd;
 layer.fillColor = [[UIColor blackColor] CGColor];
 layer.opacity = 0.5;

 layer.frame = self.cropView.bounds;
 [layer setCropAreaLeft:self.cropAreaX CropAreaTop:self.cropAreaY CropAreaRight:self.cropAreaX + self.cropAreaWidth CropAreaBottom:self.cropAreaY + self.cropAreaHeight];
 [self.cropView.layer addSublayer:layer];
 [self.view bringSubviewToFront:self.cropView];

这里主要是为了用自定义的 CAShapelayer 产生出空心遮罩的效果,从而出现中心的裁剪区域高亮而四周非裁剪区域有蒙层的效果,示意图如下

所以首先确定了 cashapelayer 的大小为 cropview 的大小,生成一个对应的 UIBezierPath,然后根据裁剪区域的大小(由 self.cropAreaX, self.cropAreaY, self.cropAreaWidth, self.cropAreaHeight 确定)生成空心遮罩的内圈 UIBezierPath,

CGRect cropframe = CGRectMake(self.cropAreaX, self.cropAreaY, self.cropAreaWidth, self.cropAreaHeight);
 UIBezierPath * path = [UIBezierPath bezierPathWithRoundedRect:self.cropView.frame cornerRadius:0];
 UIBezierPath * cropPath = [UIBezierPath bezierPathWithRect:cropframe];
 [path appendPath:cropPath];
 layer.path = path.CGPath;

然后将这个 path 配置给 CAShapeLayer,并将 CAShapeLayer 的 fillRule 配置为 kCAFillRuleEvenOdd

 layer.fillRule = kCAFillRuleEvenOdd;
 layer.fillColor = [[UIColor blackColor] CGColor];
 layer.opacity = 0.5;
 layer.frame = self.cropView.bounds;

其中 fillRule 属性表示使用哪一种算法去判断画布上的某区域是否属于该图形“内部”,内部区域将被填充颜色,主要有两种方式

kCAFillRuleNonZero,这种算法判断规则是,如果从某一点射出任意方向射线,与对应 Layer 交点为 0 则不在 Layer 内,大于 0 则在 画布内

kCAFillRuleEvenOdd 如果从某一点射出任意射线,与对应 Layer 交点为偶数则在画布内,否则不在画布内

再给 CAShapeLayer 设置蒙层颜色为透明度 0.5 的黑色,就可以实现空心蒙层效果了。

最后就是设置 layer 的四个属性并绘制内边框的白色边线。

 [layer setCropAreaLeft:self.cropAreaX CropAreaTop:self.cropAreaY CropAreaRight:self.cropAreaX + self.cropAreaWidth CropAreaBottom:self.cropAreaY + self.cropAreaHeight];
 [self.cropView.layer addSublayer:layer];
 [self.view bringSubviewToFront:self.cropView];

合理放置图片

到这一步我们正确显示了图片,也正确显示出了裁剪区域,但是我们没有将二者的约束关系建立起来,因此可能会出现下面这样的情况

可以看到这里由于这张图片的 width 远大于 height,因此会在裁剪区域内出现黑色区域,这对用户来说是一种不好的体验,同时也会影响到我们后面的裁剪步骤,究其原因是因为我们没有针对裁剪区域的宽高来放置 UIImageView,我们希望最理想的效果是,能在裁剪区域内实现类似 UIViewContentModeScaleAspectFill 的效果,也就是图片保持比例地铺满裁剪区域,并允许部分内容超出裁剪区域,这就要求

  • 当图片宽与裁剪区域宽之比大于图片高与裁剪区域高之比时,将图片高铺满裁剪区域高,图片宽成比例放大
  • 当图片高与裁剪区域高之比大于图片宽与裁剪区域宽之比时,将图片宽铺满裁剪区域宽,图片高成比例方法

这里我们用到 Masonry 来做这些布局操作

 CGFloat tempWidth = 0.0;
 CGFloat tempHeight = 0.0;

 if (self.targetImage.size.width/self.cropAreaWidth <= self.targetImage.size.height/self.cropAreaHeight) {
 tempWidth = self.cropAreaWidth;
 tempHeight = (tempWidth/self.targetImage.size.width) * self.targetImage.size.height;
 } else if (self.targetImage.size.width/self.cropAreaWidth > self.targetImage.size.height/self.cropAreaHeight) {
 tempHeight = self.cropAreaHeight;
 tempWidth = (tempHeight/self.targetImage.size.height) * self.targetImage.size.width;
 }

 [self.bigImageView mas_updateConstraints:^(MASConstraintMaker *make) {
 make.left.mas_equalTo(self.cropAreaX - (tempWidth - self.cropAreaWidth)/2);
 make.top.mas_equalTo(self.cropAreaY - (tempHeight - self.cropAreaHeight)/2);
 make.width.mas_equalTo(tempWidth);
 make.height.mas_equalTo(tempHeight);
 }];

可以看到,我们进行了两步判断,从而获得合适的宽高值,然后将图片进行布局,在自动布局时将图片中心与裁剪区域中心重合,最后我们会得到下面的效果图

支持移动和缩放图片

正如上面所讲,由于图片在裁剪区域内是以类似 UIViewContentModeScaleAspectFill 的方式放置的,很可能出现部分内容溢出裁剪区域,因此我们要让图片能支持动态移动和缩放,从而使用户能灵活地裁剪图片的内容。

具体实现上,我们其实是在 cropview 上加上手势,间接操作图片的尺寸和位置,这样有助于后面我们实现动态改变裁剪区域的实现。

缩放功能

这里实现缩放的原理实际是对放置图片的 UIImageView 的 frame 进行修改,首先我们要记录下最初的 UIImageView 的 frame

self.originalFrame = CGRectMake(self.cropAreaX - (tempWidth - self.cropAreaWidth)/2, self.cropAreaY - (tempHeight - self.cropAreaHeight)/2, tempWidth, tempHeight);

然后为 cropView 添加手势

 // 捏合手势
 UIPinchGestureRecognizer *pinGesture = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handleCenterPinGesture:)];
 [self.view addGestureRecognizer:pinGesture];

然后是手势处理函数

-(void)handleCenterPinGesture:(UIPinchGestureRecognizer *)pinGesture
{
 CGFloat scaleRation = 3;
 UIView * view = self.bigImageView;

 // 缩放开始与缩放中
 if (pinGesture.state == UIGestureRecognizerStateBegan || pinGesture.state == UIGestureRecognizerStateChanged) {
 // 移动缩放中心到手指中心
 CGPoint pinchCenter = [pinGesture locationInView:view.superview];
 CGFloat distanceX = view.frame.origin.x - pinchCenter.x;
 CGFloat distanceY = view.frame.origin.y - pinchCenter.y;
 CGFloat scaledDistanceX = distanceX * pinGesture.scale;
 CGFloat scaledDistanceY = distanceY * pinGesture.scale;
 CGRect newFrame = CGRectMake(view.frame.origin.x + scaledDistanceX - distanceX, view.frame.origin.y + scaledDistanceY - distanceY, view.frame.size.width * pinGesture.scale, view.frame.size.height * pinGesture.scale);
 view.frame = newFrame;
 pinGesture.scale = 1;
 }

 // 缩放结束
 if (pinGesture.state == UIGestureRecognizerStateEnded) {
 CGFloat ration = view.frame.size.width / self.originalFrame.size.width;

 // 缩放过大
 if (ration > 5) {
 CGRect newFrame = CGRectMake(0, 0, self.originalFrame.size.width * scaleRation, self.originalFrame.size.height * scaleRation);
 view.frame = newFrame;
 }

 // 缩放过小
 if (ration < 0.25) {
 view.frame = self.originalFrame;
 }
 // 对图片进行位置修正
 CGRect resetPosition = CGRectMake(view.frame.origin.x, view.frame.origin.y, view.frame.size.width, view.frame.size.height);

 if (resetPosition.origin.x >= self.cropAreaX) {
 resetPosition.origin.x = self.cropAreaX;
 }
 if (resetPosition.origin.y >= self.cropAreaY) {
 resetPosition.origin.y = self.cropAreaY;
 }
 if (resetPosition.size.width + resetPosition.origin.x < self.cropAreaX + self.cropAreaWidth) {
 CGFloat movedLeftX = fabs(resetPosition.size.width + resetPosition.origin.x - (self.cropAreaX + self.cropAreaWidth));
 resetPosition.origin.x += movedLeftX;
 }
 if (resetPosition.size.height + resetPosition.origin.y < self.cropAreaY + self.cropAreaHeight) {
 CGFloat moveUpY = fabs(resetPosition.size.height + resetPosition.origin.y - (self.cropAreaY + self.cropAreaHeight));
 resetPosition.origin.y += moveUpY;
 }
 view.frame = resetPosition;

 // 对图片缩放进行比例修正,防止过小
 if (self.cropAreaX < self.bigImageView.frame.origin.x
 || ((self.cropAreaX + self.cropAreaWidth) > self.bigImageView.frame.origin.x + self.bigImageView.frame.size.width)
 || self.cropAreaY < self.bigImageView.frame.origin.y
 || ((self.cropAreaY + self.cropAreaHeight) > self.bigImageView.frame.origin.y + self.bigImageView.frame.size.height)) {
 view.frame = self.originalFrame;
 }
 }
}

在手势处理时,要注意,为了能跟随用户捏合手势的中心进行缩放,我们要在手势过程中移动缩放中心到手指中心,这里我们判断了 pinGesture 的 state 来确定手势开始、进行中和结束阶段。

 if (pinGesture.state == UIGestureRecognizerStateBegan || pinGesture.state == UIGestureRecognizerStateChanged) {
 // 移动缩放中心到手指中心
 CGPoint pinchCenter = [pinGesture locationInView:view.superview];
 CGFloat distanceX = view.frame.origin.x - pinchCenter.x;
 CGFloat distanceY = view.frame.origin.y - pinchCenter.y;
 CGFloat scaledDistanceX = distanceX * pinGesture.scale;
 CGFloat scaledDistanceY = distanceY * pinGesture.scale;
 CGRect newFrame = CGRectMake(view.frame.origin.x + scaledDistanceX - distanceX, view.frame.origin.y + scaledDistanceY - distanceY, view.frame.size.width * pinGesture.scale, view.frame.size.height * pinGesture.scale);
 view.frame = newFrame;
 pinGesture.scale = 1;
 }

pinchCenter 就是捏合手势的中心,我们获取到当前图片 view 的 frame,然后计算当前 view 与手势中心的 x、y 坐标差,再根据手势缩放值 scale,创建出新的 frame

 CGRect newFrame = CGRectMake(view.frame.origin.x + scaledDistanceX - distanceX, view.frame.origin.y + scaledDistanceY - distanceY, view.frame.size.width * pinGesture.scale, view.frame.size.height * pinGesture.scale);

这个 frame 的中心坐标就在缩放手势的中心,将新的 frame 赋值给图片 view,从而实现依据手势中心进行缩放的效果。

而在手势结束阶段,我们要对图片缩放进行边界保护,既不能放大过大,也不能缩小过小。

CGFloat ration = view.frame.size.width / self.originalFrame.size.width;

 // 缩放过大
 if (ration > 5) {
 CGRect newFrame = CGRectMake(0, 0, self.originalFrame.size.width * scaleRation, self.originalFrame.size.height * scaleRation);
 view.frame = newFrame;
 }

 // 缩放过小
 if (ration < 0.25) {
 view.frame = self.originalFrame;
 }

同时缩放后如果图片与裁剪区域出现了空白区域,还要对图片的位置进行修正以保证图片始终是覆盖全裁剪区域的。

// 对图片进行位置修正
 CGRect resetPosition = CGRectMake(view.frame.origin.x, view.frame.origin.y, view.frame.size.width, view.frame.size.height);

 if (resetPosition.origin.x >= self.cropAreaX) {
  resetPosition.origin.x = self.cropAreaX;
 }
 if (resetPosition.origin.y >= self.cropAreaY) {
  resetPosition.origin.y = self.cropAreaY;
 }
 if (resetPosition.size.width + resetPosition.origin.x < self.cropAreaX + self.cropAreaWidth) {
  CGFloat movedLeftX = fabs(resetPosition.size.width + resetPosition.origin.x - (self.cropAreaX + self.cropAreaWidth));
  resetPosition.origin.x += movedLeftX;
 }
 if (resetPosition.size.height + resetPosition.origin.y < self.cropAreaY + self.cropAreaHeight) {
  CGFloat moveUpY = fabs(resetPosition.size.height + resetPosition.origin.y - (self.cropAreaY + self.cropAreaHeight));
  resetPosition.origin.y += moveUpY;
 }
 view.frame = resetPosition;

这里我们通过生成当前图片的 CGRect,与裁剪区域的边界进行如下比较

  • 图片左边线大于裁剪区域左边线时图片移动到裁剪区域 x 值
  • 图片上边线大于裁剪区域上边线时图片移动到裁剪区域 y 值
  • 图片右边线小于裁剪区域右边线时图片右贴裁剪区域右边线
  • 图片下边线小于裁剪区域右边线时图片下贴裁剪区域下边线

进行这番操作后,可能会出现由于图片过小无法铺满裁剪区域的情况,如下图所示

因此还需要再次对图片尺寸进行修正

 // 对图片缩放进行比例修正,防止过小
 if (self.cropAreaX < self.bigImageView.frame.origin.x
  || ((self.cropAreaX + self.cropAreaWidth) > self.bigImageView.frame.origin.x + self.bigImageView.frame.size.width)
  || self.cropAreaY < self.bigImageView.frame.origin.y
  || ((self.cropAreaY + self.cropAreaHeight) > self.bigImageView.frame.origin.y + self.bigImageView.frame.size.height)) {
  view.frame = self.originalFrame;
 }

这样就实现了缩放功能。

移动功能

相比于缩放,移动功能实现就简单了,只需要在 cropview 上添加 UIPanGestureRecognizer,然后在回调方法里拿到需要移动的距离,修改 UIImageView 的 center 就可以了。

 CGPoint translation = [panGesture translationInView:view.superview];
 [view setCenter:CGPointMake(view.center.x + translation.x, view.center.y + translation.y)];
  [panGesture setTranslation:CGPointZero inView:view.superview];

但是同样为了保证移动后的图片不会与裁剪区域出现空白甚至是超出裁剪区域,这里更新了图片位置后,在手势结束时还要对图片进行位置修正

  CGRect currentFrame = view.frame;

  if (currentFrame.origin.x >= self.cropAreaX) {
   currentFrame.origin.x = self.cropAreaX;

  }
  if (currentFrame.origin.y >= self.cropAreaY) {
   currentFrame.origin.y = self.cropAreaY;
  }
  if (currentFrame.size.width + currentFrame.origin.x < self.cropAreaX + self.cropAreaWidth) {
   CGFloat movedLeftX = fabs(currentFrame.size.width + currentFrame.origin.x - (self.cropAreaX + self.cropAreaWidth));
   currentFrame.origin.x += movedLeftX;
  }
  if (currentFrame.size.height + currentFrame.origin.y < self.cropAreaY + self.cropAreaHeight) {
   CGFloat moveUpY = fabs(currentFrame.size.height + currentFrame.origin.y - (self.cropAreaY + self.cropAreaHeight));
   currentFrame.origin.y += moveUpY;
  }
  [UIView animateWithDuration:0.3 animations:^{

   [view setFrame:currentFrame];
  }];

可以看到,这里做的位置检查与缩放时做的检查是一样的,只是由于不会改变图片尺寸所以这里不需要进行尺寸修正。

支持手势改变裁剪区域

接下来就是动态裁剪图片的核心内容了,其实原理也很简单,只要在上面的移动手势处理函数中,进行一些判断,决定是移动图片位置还是改变裁剪区域,也就是自定义的 CAShapeLayer 的绘制方框的尺寸就可以了。

首先我们定义一个枚举,用来表示当前应当操作的是图片还是裁剪区域的边线

typedef NS_ENUM(NSInteger, ACTIVEGESTUREVIEW) {
 CROPVIEWLEFT,
 CROPVIEWRIGHT,
 CROPVIEWTOP,
 CROPVIEWBOTTOM,
 BIGIMAGEVIEW
};

它们分别表示触发对象为裁剪区域左边线、右边线、上边线、下边线以及 UIImageView

然后我们定义一个枚举属性

@property(assign, nonatomic) ACTIVEGESTUREVIEW activeGestureView;

判断操作对象的标准是当前的手势所触发的位置是在边线上还是在非边线上,因此我们需要知道手势触发时的坐标,要知道这一点就需要我们继承一个 UIPanGestureRecognizer 并覆写一些方法

@interface YasicPanGestureRecognizer : UIPanGestureRecognizer

@property(assign, nonatomic) CGPoint beginPoint;
@property(assign, nonatomic) CGPoint movePoint;

-(instancetype)initWithTarget:(id)target action:(SEL)action inview:(UIView*)view;

@end

@interface YasicPanGestureRecognizer()

@property(strong, nonatomic) UIView *targetView;

@end

@implementation YasicPanGestureRecognizer

-(instancetype)initWithTarget:(id)target action:(SEL)action inview:(UIView*)view{

 self = [super initWithTarget:target action:action];
 if(self) {
 self.targetView = view;
 }
 return self;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent*)event{

 [super touchesBegan:touches withEvent:event];
 UITouch *touch = [touches anyObject];
 self.beginPoint = [touch locationInView:self.targetView];
}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
 [super touchesMoved:touches withEvent:event];
 UITouch *touch = [touches anyObject];
 self.movePoint = [touch locationInView:self.targetView];
}

@end

可以看到,我们首先传入了一个 view,用于将手势触发的位置转换为 view 中的坐标值。在 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent*)event{ 方法中我们得到了手势开始时的触发点 beginPoint,在 - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event 方法中我们获得了手势进行时的触发点 movePoint。

自定义完 UIPanGestureRecognizer 后我们将其加到 cropview 上并把 cropview 作为参数传给 UIPanGestureRecognizer

 // 拖动手势
 YasicPanGestureRecognizer *panGesture = [[YasicPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleDynamicPanGesture:) inview:self.cropView];
 [self.cropView addGestureRecognizer:panGesture];

接下来就是处理手势的函数,这里我们可以将整个过程分为三个步骤,开始时 -> 进行时 -> 结束时。

手势开始时

在这里我们要根据手势的 beginPoint 判断触发对象是边线还是 UIImageView

// 开始滑动时判断滑动对象是 ImageView 还是 Layer 上的 Line
 if (panGesture.state == UIGestureRecognizerStateBegan) {
 if (beginPoint.x >= self.cropAreaX - judgeWidth && beginPoint.x <= self.cropAreaX + judgeWidth && beginPoint.y >= self.cropAreaY && beginPoint.y <= self.cropAreaY + self.cropAreaHeight && self.cropAreaWidth >= 50) {
  self.activeGestureView = CROPVIEWLEFT;
 } else if (beginPoint.x >= self.cropAreaX + self.cropAreaWidth - judgeWidth && beginPoint.x <= self.cropAreaX + self.cropAreaWidth + judgeWidth && beginPoint.y >= self.cropAreaY && beginPoint.y <= self.cropAreaY + self.cropAreaHeight && self.cropAreaWidth >= 50) {
  self.activeGestureView = CROPVIEWRIGHT;
 } else if (beginPoint.y >= self.cropAreaY - judgeWidth && beginPoint.y <= self.cropAreaY + judgeWidth && beginPoint.x >= self.cropAreaX && beginPoint.x <= self.cropAreaX + self.cropAreaWidth && self.cropAreaHeight >= 50) {
  self.activeGestureView = CROPVIEWTOP;
 } else if (beginPoint.y >= self.cropAreaY + self.cropAreaHeight - judgeWidth && beginPoint.y <= self.cropAreaY + self.cropAreaHeight + judgeWidth && beginPoint.x >= self.cropAreaX && beginPoint.x <= self.cropAreaX + self.cropAreaWidth && self.cropAreaHeight >= 50) {
  self.activeGestureView = CROPVIEWBOTTOM;
 } else {
  self.activeGestureView = BIGIMAGEVIEW;
  [view setCenter:CGPointMake(view.center.x + translation.x, view.center.y + translation.y)];
  [panGesture setTranslation:CGPointZero inView:view.superview];
 }
 }

手势进行时

在这里,如果触发对象是边线,则计算边线需要移动的距离和方向,以及对于边界条件的限制以防止边线之间交叉错位的情况,具体来说就是获得坐标差值,更新 cropAreaX、cropAreaWidth 等值,然后更新 CAShapeLayer 上的空心蒙层。

如果触发对象是 UIImageView 则只需要将其位置进行改变即可。

// 滑动过程中进行位置改变
 if (panGesture.state == UIGestureRecognizerStateChanged) {
 CGFloat diff = 0;
 switch (self.activeGestureView) {
  case CROPVIEWLEFT: {
  diff = movePoint.x - self.cropAreaX;
  if (diff >= 0 && self.cropAreaWidth > 50) {
   self.cropAreaWidth -= diff;
   self.cropAreaX += diff;
  } else if (diff < 0 && self.cropAreaX > self.bigImageView.frame.origin.x && self.cropAreaX >= 15) {
   self.cropAreaWidth -= diff;
   self.cropAreaX += diff;
  }
  [self setUpCropLayer];
  break;
  }
  case CROPVIEWRIGHT: {
  diff = movePoint.x - self.cropAreaX - self.cropAreaWidth;
  if (diff >= 0 && (self.cropAreaX + self.cropAreaWidth) < MIN(self.bigImageView.frame.origin.x + self.bigImageView.frame.size.width, self.cropView.frame.origin.x + self.cropView.frame.size.width - 15)){
   self.cropAreaWidth += diff;
  } else if (diff < 0 && self.cropAreaWidth >= 50) {
   self.cropAreaWidth += diff;
  }
  [self setUpCropLayer];
  break;
  }
  case CROPVIEWTOP: {
  diff = movePoint.y - self.cropAreaY;
  if (diff >= 0 && self.cropAreaHeight > 50) {
   self.cropAreaHeight -= diff;
   self.cropAreaY += diff;
  } else if (diff < 0 && self.cropAreaY > self.bigImageView.frame.origin.y && self.cropAreaY >= 15) {
   self.cropAreaHeight -= diff;
   self.cropAreaY += diff;
  }
  [self setUpCropLayer];
  break;
  }
  case CROPVIEWBOTTOM: {
  diff = movePoint.y - self.cropAreaY - self.cropAreaHeight;
  if (diff >= 0 && (self.cropAreaY + self.cropAreaHeight) < MIN(self.bigImageView.frame.origin.y + self.bigImageView.frame.size.height, self.cropView.frame.origin.y + self.cropView.frame.size.height - 15)){
   self.cropAreaHeight += diff;
  } else if (diff < 0 && self.cropAreaHeight >= 50) {
   self.cropAreaHeight += diff;
  }
  [self setUpCropLayer];
  break;
  }
  case BIGIMAGEVIEW: {
  [view setCenter:CGPointMake(view.center.x + translation.x, view.center.y + translation.y)];
  [panGesture setTranslation:CGPointZero inView:view.superview];
  break;
  }
  default:
  break;
 }
 }

手势结束时

手势结束时,我们需要对位置进行修正。如果是裁剪区域边线,则要判断左右、上下边线之间的距离是否过短,边线是否超出 UIImageView 的范围等。如果左右边线距离过短则设置最小裁剪宽度,如果上线边线距离过短则设置最小裁剪高度,如果左边线超出了 UIImageView 左边线则需要紧贴 UIImageView 的左边线,并更新裁剪区域宽度,以此类推。然后更新 CAShapeLayer 上的空心蒙层即可。

如果是 UIImageView 则跟上一节一样要保证图片不会与裁剪区域出现空白。

 // 滑动结束后进行位置修正
 if (panGesture.state == UIGestureRecognizerStateEnded) {
 switch (self.activeGestureView) {
  case CROPVIEWLEFT: {
  if (self.cropAreaWidth < 50) {
   self.cropAreaX -= 50 - self.cropAreaWidth;
   self.cropAreaWidth = 50;
  }
  if (self.cropAreaX < MAX(self.bigImageView.frame.origin.x, 15)) {
   CGFloat temp = self.cropAreaX + self.cropAreaWidth;
   self.cropAreaX = MAX(self.bigImageView.frame.origin.x, 15);
   self.cropAreaWidth = temp - self.cropAreaX;
  }
  [self setUpCropLayer];
  break;
  }
  case CROPVIEWRIGHT: {
  if (self.cropAreaWidth < 50) {
   self.cropAreaWidth = 50;
  }
  if (self.cropAreaX + self.cropAreaWidth > MIN(self.bigImageView.frame.origin.x + self.bigImageView.frame.size.width, self.cropView.frame.origin.x + self.cropView.frame.size.width - 15)) {
   self.cropAreaWidth = MIN(self.bigImageView.frame.origin.x + self.bigImageView.frame.size.width, self.cropView.frame.origin.x + self.cropView.frame.size.width - 15) - self.cropAreaX;
  }
  [self setUpCropLayer];
  break;
  }
  case CROPVIEWTOP: {
  if (self.cropAreaHeight < 50) {
   self.cropAreaY -= 50 - self.cropAreaHeight;
   self.cropAreaHeight = 50;
  }
  if (self.cropAreaY < MAX(self.bigImageView.frame.origin.y, 15)) {
   CGFloat temp = self.cropAreaY + self.cropAreaHeight;
   self.cropAreaY = MAX(self.bigImageView.frame.origin.y, 15);
   self.cropAreaHeight = temp - self.cropAreaY;
  }
  [self setUpCropLayer];
  break;
  }
  case CROPVIEWBOTTOM: {
  if (self.cropAreaHeight < 50) {
   self.cropAreaHeight = 50;
  }
  if (self.cropAreaY + self.cropAreaHeight > MIN(self.bigImageView.frame.origin.y + self.bigImageView.frame.size.height, self.cropView.frame.origin.y + self.cropView.frame.size.height - 15)) {
   self.cropAreaHeight = MIN(self.bigImageView.frame.origin.y + self.bigImageView.frame.size.height, self.cropView.frame.origin.y + self.cropView.frame.size.height - 15) - self.cropAreaY;
  }
  [self setUpCropLayer];
  break;
  }
  case BIGIMAGEVIEW: {
  CGRect currentFrame = view.frame;

  if (currentFrame.origin.x >= self.cropAreaX) {
   currentFrame.origin.x = self.cropAreaX;

  }
  if (currentFrame.origin.y >= self.cropAreaY) {
   currentFrame.origin.y = self.cropAreaY;
  }
  if (currentFrame.size.width + currentFrame.origin.x < self.cropAreaX + self.cropAreaWidth) {
   CGFloat movedLeftX = fabs(currentFrame.size.width + currentFrame.origin.x - (self.cropAreaX + self.cropAreaWidth));
   currentFrame.origin.x += movedLeftX;
  }
  if (currentFrame.size.height + currentFrame.origin.y < self.cropAreaY + self.cropAreaHeight) {
   CGFloat moveUpY = fabs(currentFrame.size.height + currentFrame.origin.y - (self.cropAreaY + self.cropAreaHeight));
   currentFrame.origin.y += moveUpY;
  }
  [UIView animateWithDuration:0.3 animations:^{

   [view setFrame:currentFrame];
  }];
  break;
  }
  default:
  break;
 }
 }

进行图片裁剪并获得裁剪后的图片

最后一步就是对图片进行裁剪了。首先确定对图片的缩放尺寸 imageScale

 CGFloat imageScale = MIN(self.bigImageView.frame.size.width/self.targetImage.size.width, self.bigImageView.frame.size.height/self.targetImage.size.height);

然后将 cropView 的裁剪区域对应到 UIImageView 上,再除以缩放值,即可得到对应 UIImage 上需要裁剪的区域

 CGFloat cropX = (self.cropAreaX - self.bigImageView.frame.origin.x)/imageScale;
 CGFloat cropY = (self.cropAreaY - self.bigImageView.frame.origin.y)/imageScale;
 CGFloat cropWidth = self.cropAreaWidth/imageScale;
 CGFloat cropHeight = self.cropAreaHeight/imageScale;
 CGRect cropRect = CGRectMake(cropX, cropY, cropWidth, cropHeight);

最后调用 CoreGraphic 的方法,将图片对应区域的数据取出来生成新的图片,就是我们需要的裁剪后的图片了。

 CGImageRef sourceImageRef = [self.targetImage CGImage];
 CGImageRef newImageRef = CGImageCreateWithImageInRect(sourceImageRef, cropRect);
 UIImage *newImage = [UIImage imageWithCGImage:newImageRef];

源码下载:

github下载地址:点击这里

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

时间: 2017-11-28

详解iOS 裁剪圆形图像并显示(类似于微信头像)

本文主要讲解如何从照片库选择一张照片后将其裁剪成圆形头像并显示,类似于微信头像那种模式. 本文的方法也适用于当时拍照获取的图像,方法类似,所以不再赘述. 本文主要是在iOS 10环境下使用,此时如果要使用使用系统照片库.照相机等功能需要授权,授权方法如下: 右键点击工程目录中的"Info.plist文件-->Open As -->Source Code",打开复制以下你在应用中使用的隐私权限设置(描述自己修改): <key>NSVideoSubscriberAc

iOS如何裁剪圆形头像

本文实例为大家介绍了iOS裁剪圆形头像的详细代码,供大家参考,具体内容如下 - (void)viewDidLoad { [super viewDidLoad]; //加载图片 UIImage *image = [UIImage imageNamed:@"菲哥"]; //获取图片尺寸 CGSize size = image.size; //开启位图上下文 UIGraphicsBeginImageContextWithOptions(size, NO, 0); //创建圆形路径 UIBez

iOS实现裁剪框和图片剪裁功能

图片处理中经常用的图片剪裁,就是通过剪裁框确定图片剪裁的区域,然后剪去该区域的图片,今天实现了一下,其实图片剪裁本身不难,主要剪裁框封装发了点时间,主要功能可以拖动四个角缩放,但不能超出父视图,拖动四个边单方向缩放,不能超出父视图,拖动中间部分单单移动,不改变大小,不能超出父视图.下面列举一些主要代码. 四个角的处理代码: -(void)btnPanGesture:(UIPanGestureRecognizer*)panGesture { UIView *vw = panGesture.view

iOS 图片裁剪的实现方法

iOS 图片裁剪方法,主要有两种,一起来看下. 通过 CGImage 或 CIImage 裁剪 UIImage有cgImage和ciImage属性,分别可以获得CGImage和CIImage对象.CGImage和CIImage对象都有cropping(to:)方法,传入CGRect的参数表示要裁剪的区域(采用UIImage的坐标). static func cropImage(_ image: UIImage, withRect rect: CGRect) -> UIImage? { if le

iOS 图片裁剪 + 旋转

之前分别介绍了图片裁剪和图片旋转方法 <iOS 图片裁剪方法> 地址:http://www.jb51.net/article/107308.htm <iOS 图片旋转方法> 地址:http://www.jb51.net/article/107361.htm 裁剪和旋转是可以连在一起执行的.先定位到需要裁剪的区域,然后以此区域的中心为轴,旋转一定角度,最后获取旋转后此区域内的图片.可以用位图(Bitmap)绘制实现 static func cropImage(_ image: UII

iOS拍照后图片自动旋转90度的完美解决方法

今天开发一个拍照获取照片的功能的时候, 发现上传之后图片会自动旋转90. 测试发现, 只要是图片大于2M, 系统就会自动翻转照片 相机拍照后直接取出来的UIimage(用UIImagePickerControllerOriginalImage取出),它本身的imageOrientation属性是3,即UIImageOrientationRight.如果这个图片直接使用则没事,但是如果对它进行裁剪.缩放等操作后,它的这个imageOrientation属性会变成0.此时这张图片用在别的地方就会发生

iOS 图片旋转方法实例代码

通过 CGImage 或 CIImage 旋转特定角度 UIImage可通过CGImage或CIImage初始化,初始化方法分别为init(cgImage: CGImage, scale: CGFloat, orientation: UIImageOrientation)和init(ciImage: CIImage, scale: CGFloat, orientation: UIImageOrientation).通过UIImageOrientation的不同取值,可以使图片旋转90.180.2

基于RxPaparazzo实现图片裁剪、图片旋转、比例放大缩小功能

前言:基于RxPaparazzo的图片裁剪,图片旋转.比例放大|缩小. 效果: 开发环境:AndroidStudio2.2.1+gradle-2.14.1 涉及知识: 1.Material Design (CardView+CoordinatorLayout+AppBarLayout+NestedScrollView+CollapsingToolbarLayout+Toolbar+FloatingActionButton)使用 2.butterknife注解式开发 3.基于RxJava+RxAn

iOS如何将图片裁剪成圆形

本文实例为大家分享了iOS将图片裁剪成圆形的具体代码,供大家参考,具体内容如下 原图: 圆形图片裁剪效果: 裁剪成带边框的圆形图片: 核心代码: #import <UIKit/UIKit.h> @interface UIImage (image) /** * 生成一张圆形图片 * * @param image 要裁剪的图片 * * @return 生成的圆形图片 */ + (UIImage *)imageWithClipImage:(UIImage *)image; /** * 生成一张带有边

Python图片裁剪实例代码(如头像裁剪)

今天就来说个常用的功能,图片裁剪,可用于头像裁剪啊之类的.用的还是我们之前用的哪个模块pillow 1. 安装pillow 用pip安装 pip install pillow 2. 图片裁剪 2.1 准备一张图片 2.2 我们使用的是Image中的crop(box)功能,它需要一个参数box,元组 类型,元组包括4个元素,如: (距离图片左边界距离x, 距离图片上边界距离y,距离图片左边界距离+裁剪框宽度x+w,距离图片上边界距离+裁剪框高度y+h) 如图:(x, y, x+w, y+h), x

基于jQuery+HttpHandler实现图片裁剪效果代码(适用于论坛, SNS)

正文:为了使层次分明及便于阅读,  整个解决方案如下: 其中BitmapCutter.Core是图片的服务器端处理程序, 类图为: 简单说明下, 更多说明可查看源码注释 : Cutter为裁剪对象, 用于存储客户端通过AJAX提交的数据. Helper为图片处理类, 包括图片翻转(RotateImage()), 图片裁剪(GenerateBitmap()). Callback为服务器端图片处理类, 通过使用Cutter封装客户端AJAX提交的数据, 然后调用Helper中的方法来完成图片处理.

vue-cli结合Element-ui基于cropper.js封装vue实现图片裁剪组件功能

前端工作中,经常需要图片裁剪的场景,cropper.js是一款优秀的前端插件,api十分丰富. 本文是在vue-cli项目下封装图片裁剪插件,效果图如下: 话不多说,看步骤吧. 第一步:准备开发环境 cropper.js是基于jquery的,所以要先安装jquery 执行命令: npm  install --save-dev jquery cropper 为webpack配置添加jquery的映射 修改webpack.base.conf.js配置,添加标红的一行 第二步:新建图片裁剪组件 ind

IOS图片无限轮播器的实现原理

首先实现思路:整个collectionView中只有2个cell.中间始终显示第二个cell. 滚动:向前滚动当前cell的脚标为0,向后滚动当前的cell脚标为2.利用当前cell的脚标减去1,得到+1,或者-1,来让图片的索引加1或者减1,实现图片的切换. 声明一个全局变量index来保存图片的索引,用来切换显示在当前cell的图片. 在滚动前先让显示的cell的脚标变为1.代码viewDidLoad中设置 完成一次滚动结束后,代码再设置当前的cell为第二个cell(本质上就是让当前显示的