下載 "demo和工具下載鏈接SPClipTool" 使用說明 需求 圖片裁剪,效果如下圖,支持圖片拖拽,縮放,裁剪框自由變換大小。 思路 兩個UIImageView,一個做背景,並加上蒙版效果,另外一個通過蒙版控制顯示區域,並且保證兩個UIImageView平移和縮放的時候完全重疊。最後使用一個U ...
下載
使用說明
[[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);
}
這裡一定要註意線條的寬度,線條是有寬度的,繪製路徑位於線條的中心位置。