下载
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);
}
这里一定要注意线条的宽度,线条是有宽度的,绘制路径位于线条的中心位置。

巴黎佐岸丶