下载

demo和工具下载链接SPClipTool

使用说明

[[SPClipTool shareClipTool] sp_clipOriginImage:pickerImage complete:^(UIImage * _Nonnull image) {
// 获取到裁剪后的image 后续操作
}];

需求

图片裁剪,效果如下图,支持图片拖拽,缩放,裁剪框自由变换大小。

思路

两个UIImageView,一个做背景,并加上蒙版效果,另外一个通过蒙版控制显示区域,并且保证两个UIImageView平移和缩放的时候完全重叠。最后使用一个UIView来做交互,绘制三分网格线(专业术语我不知道叫啥,截图时一个参照,2/3 ≈0.667 接近黄金比0.618)。

注意

  • 坐标系转换问题。

  • mask灵活使用问题。

  • 手势的处理和三分网格线绘制的时候,计算线条宽度和长度需要特别注意。

  • 为了增强用户体验,在裁剪框边缘交互设计的时候,注意额外增加用户的可操控范围。

实现

  • 初始化两个UIImageView,一个做背景图(backgroudImageView),一个用来显示裁剪区域(clipImageView),拖拽手势加到了clipImageView。
- (void)setupImageView {
// backgroudImageView
UIImageView *backgroudImageView = [[UIImageView alloc] initWithFrame:self.view.bounds];
backgroudImageView.contentMode = UIViewContentModeScaleAspectFit;
backgroudImageView.image = self.originImage;
[self.view addSubview:backgroudImageView];
self.backgroudImageView = backgroudImageView;
backgroudImageView.layer.mask = [[CALayer alloc] init];
backgroudImageView.layer.mask.frame = backgroudImageView.bounds;
backgroudImageView.layer.mask.backgroundColor = [UIColor colorWithWhite:1 alpha:0.5].CGColor; // clipImageView
UIImageView *clipImageView = [[UIImageView alloc] initWithFrame:backgroudImageView.frame];
clipImageView.userInteractionEnabled = YES;
clipImageView.image = backgroudImageView.image;
clipImageView.contentMode = backgroudImageView.contentMode;
[self.view addSubview:clipImageView];
self.clipImageView = clipImageView;
clipImageView.layer.mask = [[CALayer alloc] init];
clipImageView.layer.mask.backgroundColor = [UIColor whiteColor].CGColor;
clipImageView.layer.mask.borderColor = [UIColor whiteColor].CGColor;
clipImageView.layer.mask.borderWidth = 1;
[clipImageView.layer.mask removeAllAnimations]; UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(imagePan:)];
[clipImageView addGestureRecognizer:panGesture];
}
  • 初始化用于裁剪交互的SPClipView
- (void)setupClipView {
SPClipView *clipView = [[SPClipView alloc] init];
clipView.backgroundColor = [UIColor clearColor];
// 打开下面两行注释,可以查看真实clipView的大小。
// clipView.layer.borderColor = [UIColor whiteColor].CGColor;
// clipView.layer.borderWidth = 1;
[self.view addSubview:clipView];
self.clipView = clipView;
// 获取真实frame
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
clipView.frame = CGRectMake(0, 0, self.view.width / 1.5, self.view.height / 1.5);
clipView.center = self.view.center;
self.backgroudImageView.frame = self.view.bounds;
self.clipImageView.frame = self.backgroudImageView.frame;
[self dealMask];
}); UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(clipPan:)];
[clipView addGestureRecognizer:panGesture]; UIPinchGestureRecognizer *pinchGesture = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinchGestureAction:)];
[self.view addGestureRecognizer:pinchGesture];
}
  • 手势处理
#pragma mark- UIPanGestureRecognizer
- (void)clipPan:(UIPanGestureRecognizer *)panGesture {
CGPoint point = [panGesture translationInView:self.clipView];
self.clipView.origin = [self.clipView convertPoint:point toView:self.view];
[self expandClipView:panGesture];
[self dealGuideLine:panGesture];
[self dealMask];
[panGesture setTranslation:CGPointMake(0, 0) inView:self.view];
} - (void)imagePan:(UIPanGestureRecognizer *)panGesture {
CGPoint point = [panGesture translationInView:self.clipImageView];
self.clipImageView.origin = [self.clipImageView convertPoint:point toView:self.view];
self.backgroudImageView.center = self.clipImageView.center;
[self dealGuideLine:panGesture];
[self dealMask];
[panGesture setTranslation:CGPointMake(0, 0) inView:self.view];
} #pragma mark- UIPinchGestureRecognizer
- (void)pinchGestureAction:(UIPinchGestureRecognizer *)pinchGesture {
switch (pinchGesture.state) {
case UIGestureRecognizerStateBegan: {
if (lastScale <= minScale) {
lastScale = minScale;
}else if (lastScale >= maxScale) {
lastScale = maxScale;
}
self.clipImageViewCenter = self.clipImageView.center;
self.clipView.showGuideLine = YES;
}
case UIGestureRecognizerStateChanged: {
CGFloat currentScale = lastScale + pinchGesture.scale - 1;
if (currentScale > minScale && currentScale < maxScale) {
[self dealViewScale:currentScale];
}
}
break;
case UIGestureRecognizerStateEnded:
lastScale += (pinchGesture.scale - 1);
self.clipView.showGuideLine = NO;
[self.clipView setNeedsDisplay];
default:
break;
}
} #pragma mark- Action
- (void)dealViewScale:(CGFloat)currentScale {
self.clipImageView.width = currentScale * self.view.width;
self.clipImageView.height = currentScale * self.view.height;
self.clipImageView.center = self.clipImageViewCenter;
self.backgroudImageView.frame = self.clipImageView.frame;
self.backgroudImageView.layer.mask.frame = self.backgroudImageView.bounds;
[self.backgroudImageView.layer.mask removeAllAnimations];
[self dealMask];
} - (void)expandClipView:(UIPanGestureRecognizer *)panGesture {
CGPoint point = [panGesture translationInView:self.clipImageView];
CGFloat margin = 60;
CGFloat minValue = margin;
if (panGesture.numberOfTouches) {
CGPoint location = [panGesture locationOfTouch:0 inView:panGesture.view];
if (location.x < margin) {
self.clipView.width = MAX(self.clipView.width -= point.x, minValue);
}
if ((self.clipView.width - location.x) < margin) {
self.clipView.frame = CGRectMake(self.clipView.x - point.x, self.clipView.y, self.clipView.width + point.x, self.clipView.height);
}
if (location.y < margin) {
self.clipView.height = MAX(self.clipView.height -= point.y, minValue);
}
if ((self.clipView.height - location.y) < margin) {
self.clipView.frame = CGRectMake(self.clipView.x , self.clipView.y - point.y, self.clipView.width, self.clipView.height + point.y);
}
}
} - (void)dealGuideLine:(UIPanGestureRecognizer *)panGesture {
switch (panGesture.state) {
case UIGestureRecognizerStateBegan:
self.clipView.showGuideLine = YES;
break;
case UIGestureRecognizerStateEnded:
self.clipView.showGuideLine = NO;
break;
default:
break;
}
} - (void)dealMask {
// 额外增加拖拉区域 增强边缘手势体验
CGFloat margin = 30;
CGRect rect = [self.view convertRect:self.clipView.frame toView:self.clipImageView];
self.clipImageView.layer.mask.frame = CGRectMake(rect.origin.x + margin, rect.origin.y + margin, rect.size.width - 2 * margin, rect.size.height - 2 * margin);
[self.clipView setNeedsDisplay];
[self.clipImageView.layer.mask removeAllAnimations];
}
  • 图片裁剪
- (void)clipImage {

    CGSize size = self.view.bounds.size;
UIGraphicsBeginImageContextWithOptions(size, NO, [UIScreen mainScreen].scale);
[self.view drawViewHierarchyInRect:self.view.bounds afterScreenUpdates:NO];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
CGImageRef cgImage = [image CGImage];
CGRect rect = [self.clipImageView convertRect:self.clipImageView.layer.mask.frame toView:self.view]; // 边框线条宽度值
CGFloat borderW = 1;
CGImageRef cgClipImage = CGImageCreateWithImageInRect(cgImage, CGRectMake((rect.origin.x + borderW / 2) * image.scale, (rect.origin.y + borderW / 2) * image.scale, (rect.size.width - borderW) * image.scale, (rect.size.height - borderW) * image.scale));
UIGraphicsEndImageContext();
if (self.complete) {
self.complete([UIImage imageWithCGImage:cgClipImage]); }
[self dismissViewControllerAnimated:YES completion:nil];
}
  • 裁剪区域绘制

    在这里,裁剪区域的矩形框我并没有直接采用clipView的fram大小,而是在其内部绘制了一个矩形框,为了让用户在调节边缘的时候更灵活,不然只有当手指在边框内部边缘才能触发调节边框大小的事件。如下图,可以看到clipView真实的大小(外框)。

@implementation SPClipView

- (void)drawRect:(CGRect)rect {
// Drawing code
CGContextRef currentContext = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(currentContext, [UIColor whiteColor].CGColor);
CGContextSetLineWidth(currentContext, 1);
// 额外增加拖拉区域 增强边缘手势体验,该值应该和上文- (void)dealMask;方法中的margin一致
CGFloat margin = 30; // 绘制矩形框
CGContextAddRect(currentContext, CGRectMake(margin, margin, self.width - 2 * margin, self.height - 2 * margin));
CGContextStrokePath(currentContext); // 绘制三分线
CGFloat maskW = self.width - 2 * margin;
CGFloat maskH = self.height - 2 * margin;
CGContextSetLineWidth(currentContext, 0.5);
if (self.showGuideLine) {
CGContextSetStrokeColorWithColor(currentContext, [UIColor whiteColor].CGColor);
}else {
CGContextSetStrokeColorWithColor(currentContext, [UIColor clearColor].CGColor);
}
CGContextMoveToPoint(currentContext, margin, maskH / 3 + margin);
CGContextAddLineToPoint(currentContext, self.width - margin, maskH / 3 + margin);
CGContextMoveToPoint(currentContext, margin, 2 / 3.0 * maskH + margin);
CGContextAddLineToPoint(currentContext, self.width - margin, 2 / 3.0 * maskH + margin); CGContextMoveToPoint(currentContext, maskW / 3 + margin, margin);
CGContextAddLineToPoint(currentContext, maskW / 3+ margin, self.height - margin);
CGContextMoveToPoint(currentContext, 2 / 3.0 * maskW + margin, margin);
CGContextAddLineToPoint(currentContext, 2 / 3.0 * maskW + margin, self.height - margin); CGContextStrokePath(currentContext); // 绘制四角
CGFloat cornerL = 15;
CGFloat cornerLW = 2;
// 实际的长度
CGFloat cornerRL = cornerL + cornerLW;
CGPoint originH = CGPointMake(margin - cornerLW, margin - cornerLW / 2);
CGPoint originV = CGPointMake(margin - cornerLW / 2, margin - cornerLW);
CGContextSetStrokeColorWithColor(currentContext, [UIColor whiteColor].CGColor);
CGContextSetLineWidth(currentContext, cornerLW); // 左上
CGContextMoveToPoint(currentContext, originH.x, originH.y);
CGContextAddLineToPoint(currentContext, originH.x + cornerRL, originH.y);
CGContextMoveToPoint(currentContext, originV.x, originV.y);
CGContextAddLineToPoint(currentContext, originV.x, originV.y + cornerRL); // 左下
CGContextMoveToPoint(currentContext, originH.x, originH.y + maskH + cornerLW);
CGContextAddLineToPoint(currentContext, originH.x + cornerRL, originH.y + maskH + cornerLW);
CGContextMoveToPoint(currentContext, originV.x, originV.y + maskH + 2 * cornerLW);
CGContextAddLineToPoint(currentContext, originV.x, originV.y + maskH + 2 * cornerLW - cornerRL); // 右上
CGContextMoveToPoint(currentContext, originH.x + maskW + 2 * cornerLW, originH.y);
CGContextAddLineToPoint(currentContext, originH.x + maskW + 2 * cornerLW - cornerRL, originH.y);
CGContextMoveToPoint(currentContext, originV.x + maskW + cornerLW, originV.y);
CGContextAddLineToPoint(currentContext, originV.x + maskW + cornerLW, originV.y + cornerRL); // 右下
CGContextMoveToPoint(currentContext, originH.x + maskW + 2 * cornerLW, originH.y + maskH + cornerLW);
CGContextAddLineToPoint(currentContext, originH.x + maskW + 2 * cornerLW - cornerRL, originH.y + maskH + cornerLW);
CGContextMoveToPoint(currentContext, originV.x + maskW + cornerLW, originV.y + maskH + 2 * cornerLW);
CGContextAddLineToPoint(currentContext, originV.x + maskW + cornerLW, originV.y + maskH + 2 * cornerLW - cornerRL); CGContextStrokePath(currentContext);
}

这里一定要注意线条的宽度,线条是有宽度的,绘制路径位于线条的中心位置。

iOS 裁剪工具的更多相关文章

  1. 苹果 iOS 8 新固件新功能特性总结汇总 (苹果 iPhone/iPad 最新移动操作系统)

    苹果在 WWDC 2014 大会上正式发布了其最新的 OS X Yosemite 桌面系统以及 iOS 8 移动操作系统,虽然 iOS 8 依然延续了 iOS7 的扁平化设计风格,但在功能上却还是给我 ...

  2. iOS开源项目周报0330

    由OpenDigg 出品的iOS开源项目周报第十四期来啦.我们的iOS开源周报集合了OpenDigg一周来新收录的优质的iOS开源项目,方便iOS开发人员便捷的找到自己需要的项目工具等. FengNi ...

  3. iOS可视化动态绘制连通图

    上篇博客<iOS可视化动态绘制八种排序过程>可视化了一下一些排序的过程,本篇博客就来聊聊图的东西.在之前的博客中详细的讲过图的相关内容,比如<图的物理存储结构与深搜.广搜>.当 ...

  4. 【疯狂造轮子-iOS】JSON转Model系列之二

    [疯狂造轮子-iOS]JSON转Model系列之二 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 上一篇<[疯狂造轮子-iOS]JSON转Model系列之一> ...

  5. 【疯狂造轮子-iOS】JSON转Model系列之一

    [疯狂造轮子-iOS]JSON转Model系列之一 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 之前一直看别人的源码,虽然对自己提升比较大,但毕竟不是自己写的,很容易遗 ...

  6. iOS总结_UI层自我复习总结

    UI层复习笔记 在main文件中,UIApplicationMain函数一共做了三件事 根据第三个参数创建了一个应用程序对象 默认写nil,即创建的是UIApplication类型的对象,此对象看成是 ...

  7. iOS代码规范(OC和Swift)

    下面说下iOS的代码规范问题,如果大家觉得还不错,可以直接用到项目中,有不同意见 可以在下面讨论下. 相信很多人工作中最烦的就是代码不规范,命名不规范,曾经见过一个VC里有3个按钮被命名为button ...

  8. JS调用Android、Ios原生控件

    在上一篇博客中已经和大家聊了,关于JS与Android.Ios原生控件之间相互通信的详细代码实现,今天我们一起聊一下JS调用Android.Ios通信的相同点和不同点,以便帮助我们在进行混合式开发时, ...

  9. 告别被拒,如何提升iOS审核通过率(上篇)

    iOS审核一直是每款移动产品上架苹果商店时面对的一座大山,每次提审都像是一次漫长而又悲壮的旅行,经常被苹果拒之门外,无比煎熬.那么问题来了,我们有没有什么办法准确把握苹果审核准则,从而提升审核的通过率 ...

随机推荐

  1. 二叉查找树的平衡(DSW算法)

    树适合于表示某些领域的层次结构(比如Linux的文件目录结构),使用树进行查找比使用链表快的多,理想情况下树的查找复杂度O(log(N)),而链表为O(N),但理想情况指的是什么情况呢?一般指树是完全 ...

  2. Spring Boot整合Mybatis报错InstantiationException: tk.mybatis.mapper.provider.base.BaseSelectProvider

    Spring Boot整合Mybatis时一直报错 后来发现原来主配置类上的MapperScan导错了包 由于我使用了通用Mapper,所以应该导入通用mapper这个包

  3. python网络爬虫之入门[一]

    目录 前言 一.探讨什么是python网络爬虫? 二.一个针对于网络传输的抓包工具fiddler 三.学习request模块来爬取第一个网页 * 扩展内容(爬取top250的网页) 后记 @(目录) ...

  4. Mybatis拦截器实现原理深度分析

    1.拦截器简介 拦截器可以说使我们平时开发经常用到的技术了,Spring AOP.Mybatis自定义插件原理都是基于拦截器实现的,而拦截器又是以动态代理为基础实现的,每个框架对拦截器的实现不完全相同 ...

  5. IdentityServer4 自定义授权模式

    IdentityServer4除了提供常规的几种授权模式外(AuthorizationCode.ClientCredentials.Password.RefreshToken.DeviceCode), ...

  6. 【翻译】.NET Core3.1发布

    .NET Core3.1发布 我们很高兴宣布.NET Core 3.1的发布.实际上,这只是对我们两个多月前发布的.NET Core 3.0的一小部分修复和完善.最重要的是.NET Core 3.1是 ...

  7. 《手把手教你》系列练习篇之6-python+ selenium自动化测试(详细教程)

    1. 简介 前面文章我们了解了如何获取元素的text属性值,和判断元素是否显示在页面(is_displayed()方法),本文我们来学习下,判断一个控件是否被选中状态. 2. 验证控件是否被选中 还是 ...

  8. centos7更新php5.4到php5.6/php7

    centos7系统yum安装的php版本为5.4. 因业务需求,开发可能需要php5.6环境. 本文应需而生,介绍从php5.4升级到php5.6. 如需更新到php7环境,步骤一样. 如果是线上应用 ...

  9. JVM虚拟机详解+Tomcat性能优化

    1.JVM(java virtual mechinal) ()JVM有完善的硬件架构,如处理器.堆栈.寄存器当,还具有相应的指令系统. ()JVM的主要工作时解释自己的指令集(即字节码),并映射到本地 ...

  10. LeetCode-7.reverse-integer 【翻转字符串】【数学】

    PS: 第一次写文章好累啊,没想到这么短的文章写完这么累,大家给我点反馈,多给我留言啊.