简介
图片编辑是基于 CLImageEditor 第三方进行改装得到仿微信的图片编辑交互。原CLImageEditor库是一个日本人写的图片编辑,支持涂鸦,裁剪,旋转,文字帖功能。但有几项我们进行改进。
- 所有的这些图片操作我们汇总到一起进行编辑,最终保存的时候才处理所有操作汇总成一张处理图片。
- 涂鸦撤销功能实现真正的上一步撤销,而不是仅仅透明色进行橡皮擦擦除。
- 新增马赛克操作功能。
- 把图片裁剪和旋转进行结合到一个页面操作,另外新增裁剪、旋转之后的图片恢复。
- 改变颜色选择,文字编辑的交互,新增图片裁剪区域的边角重点标注。CLImageToolBase 工具基类:持有editor编辑器方便拿到图片,同时具有setup, cleanUp 这几个“生命周期”方法,做相关工具的视图创建与清除。
1
2
3
4
5
6
7
8
9
10
11
12
13CLImageEditorViewController:
usedToolDic:使用过的工具【toolName:CLImageToolBase】
-(CLImageToolBase *)currentEditTool; 当前正在使用的工具。
-(UIImage *)getMergeDrawImageForCropTool; 最后合成的图片。用于业务转发、保存、回调给用户。
//图片编辑器所支持的工具类都继承于CLImageToolBase, 通过CLClassList辅助类runtime方式自动将相关工具类注入到菜单项中。
+ (NSArray*)subtools
{
return [CLImageToolInfo toolsWithToolClass:[CLImageToolBase class]];
}画笔涂鸦
画笔手势:通过 UIPanGestureRecongnizer 手势实现在 imageview 的表层新 ImageView,通过画布生成 image 给 ImageView 赋值。
画笔撤销-旧方案
旧撤销:CLImageEditor 开源库涂鸦撤销原本是一个橡皮檫功能,通过设置BlendMode为透明色来实现擦除画笔线条。假“撤销”。 CGContextSetBlendMode(context, kCGBlendModeClear);
撤销-方案演变:
新方案1: 为了可以真正的实现撤销,我们把 Pan 手势由开始到触摸结束表示为一条线,然后用一个数据源存储好这些线条。当撤销的时候,实际为移除最后一个线条即可。表层即一个drawImageView,所有画笔涂鸦合成图片最终只赋值给drawImageView显示。
在CLDrawTool 类上新增三个属性:用来记录画布图层上划线的操作。由于每条线可切换颜色进行涂鸦,所以每条线都对应lineColorsArray 颜色数组来记录。一条线即为nowPointArray数组。记录一条线上的所有点。而linePointArray用来记录所有线。1
2
3
4@property(nonatomic,strong) NSMutableArray *nowPointArray; //当前线的所有点
@property(nonatomic,strong) NSMutableArray *lineColorsArray;//每条线对应的颜色
@property(nonatomic,strong) NSMutableArray *linePointArray;
//所有线【nowPointArray】撤销的时候:移除最后一条线以及颜色。重新绘制到当前的画布上,然后生成image赋值给表层的drawImageView.
缺点1:每次撤销移除最后一条线,都需要重绘所有线。以及每次新涂鸦时候,都需要重新实时绘制所有线来达到实时显示的效果。性能消耗太大,不利于用于快速绘制。
缺点2:当图片旋转后,无法对所有线上的点做相应的旋转和放大。旋转图片后,线条错位。
缺陷:这种“正常”撤销无法满足当图片旋转后,所有线条的位置变化,即 我们应该根据角度去旋转所有点的旋转后的点坐标新方案2、为了解决图片旋转后的的坐标点改变和计算问题,我们换了一种思路:即一次 Pan 手势生成一张图层,旋转时旋转所有画笔图层进行旋转。 每次画完一条线,即在表层生成一张新的drawImageView. 当画多条线的时候,实则【drawImageView】集合。
优点:此方案既保证了绘制时不再走重绘,每条线都在单独的图层上。同时在图片发生旋转时,旋转每一条线锁在的图片drawImageView,保证了每一条线显示都正确。1
2
3
4if(sender.state == UIGestureRecognizerStateEnded){
//结束的时候,再创建一张图drawImageView。
[self generateDrawImageView];
}
图片旋转
当图片发生旋转或者裁剪操作时候,需要从 usedToolDic 中拿出CLDrawTool 和 CLMosialTool ,取出其中的【drawImageView】集合,对其中的每一张图层进行旋转和裁剪,再赋回给对应的每一项图层drawImageView. 再调整每一项drawImageView的frame. 调用此方法。
//重置画笔层&马赛克层的frame。1
- (void)resetDrawImageViewFrame:(UIImage *)image withDrawImageView:(UIImageView *)drawingView
画笔撤销-新方案
马赛克
马赛克的思路和画笔差不多,也是根据用户 pan 手势进行触摸,所画的颜色为原图经 RGB 模糊换算得到的马赛克图片,算法使用 XRGBTool 第三方的一个类。
马赛克初始方案
原先的方案是创建一个 imageLayer,然后添加到 imageView 的 layer: [self.editor.imageView.layer insertSublayer:_imageLayer atIndex:0]; //确定马赛克在最底部.
1 | CALayer *imageLayer = [CALayer layer]; |
1 | 在pan手势时候,通过改变shareLayer.path |
最后合成图片的时候,通过 imageview.layer 渲染在上下文中: [self.editor.imageView.layer renderInContext:context];
存在的问题(一):path 路径不可撤回马赛克撤回
创建一个可变的 path 路径 CGMutablePathRef _path = CGPathCreateMutable(); 并将每条线进行叠加,CGPathAddPath(_path, nil, _onePath);最后将总的_path 赋给_shapeLayer.path。
1 | - (void)drawingViewDidPan:(UIPanGestureRecognizer*)sender |
马赛克撤销-旧方案
流程:当撤销时,仅移除数组记录的最后一条线。然后要重新调用一遍渲染。把每条线的所有点进行连接 CGPathAddPath,赋值给总的可变_path,最后把_path 给_shapeLayer.path1 | //撤回上一步 |
1 | -(void)drawAllPanLine |
马赛克撤销-新方案
文字帖
富文本框
doneAppendNewText 点击完成的时候,调用此方法。
[_CLTextView setActiveTextView:nil]; 使当前label 失去焦点。
键盘显示时候,调整textview的高度。1
2
3
4
5
6
7
8- (void)keyBoardWillShow:(NSNotification *)notificatioin
{
NSDictionary * info = [notificatioin userInfo];
CGSize kbSize = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size;
[_textView mas_updateConstraints:^(MASConstraintMaker *make) {
make.bottom.mas_equalTo(-kbSize.height);
}];
}
drawingViewDidPan事件穿透
1 | - (void)drawingViewDidPan:(UIPanGestureRecognizer*)sender |
View容器layer渲染到画布
1 | -(UIImage *)mergeDrawImage:(UIImage *)mergeImage |
旋转
在CLClippingTool 旋转完成时候
executeWithCompletionBlock:(void (^)(UIImage , NSError , NSDictionary *))completionBlock
对文字帖进行处理,根据旋转裁剪后得到的图片大小决定workingView的容器frame大小。1
2
3
4
5
6CLTextTool *_textTool = [self.editor.usedToolDic objectForKey:@"CLTextTool"];
if(_textTool){
//将workingView 进行transform旋转
CGAffineTransform transform = CATransform3DGetAffineTransform([self rotateTransform:CATransform3DIdentity clockwise:NO]);
[self.editor resetTextWorkingViewFrame:_textTool.workingView byResultImage:result transform:transform];
对workingView 容器进行旋转,设置其transform.
resetTextWorkingViewFrame的代码实现
裁剪旋转
旋转
用户通过点击旋转按钮- (void)tappedRoteMenu:(UITapGestureRecognizer*)sender
旋转通过_rotateImageView 来展示旋转的动画和实时效果,
rotateStateDidChange方法实现旋转动画1
2
3
4- (void)rotateStateDidChange
{
CATransform3D transform = [self rotateTransform:CATransform3DIdentity clockwise:NO];
_rotateImageView.layer.transform 设置。
旋转动画结束后:重新设置一下裁剪格子。1
2
3
4
5
6 _rotateView.hidden = YES;
_gridView = [[CLClippingPanel alloc] initWithSuperview:self.editor.imageView.superview frame:_rotateImageView.frame];
_gridView.backgroundColor = [UIColor clearColor];
_gridView.bgColor = [self.editor.view.backgroundColor colorWithAlphaComponent:0.8];
_gridView.gridColor = [[UIColor whiteColor] colorWithAlphaComponent:0.8];
_gridView.clipsToBounds = NO;
- 图片旋转:
将原图的CIImage注入到CIFilter中,设置CIFilter的旋转系数,CIFilter导出旋转后的CIImage, 通过CIContext 将导出的新图片CIImage转换成CGImage,最后拿到UIImage。
参考以下代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19- (UIImage*)buildRoteImage:(UIImage*)image
{
CIImage *ciImage = [[CIImage alloc] initWithImage:image];
CIFilter *filter = [CIFilter filterWithName:@"CIAffineTransform" keysAndValues:kCIInputImageKey, ciImage, nil];
[filter setDefaults];
CGAffineTransform transform = CATransform3DGetAffineTransform([self rotateTransform:CATransform3DIdentity clockwise:YES]);
[filter setValue:[NSValue valueWithBytes:&transform objCType:@encode(CGAffineTransform)] forKey:@"inputTransform"];
CIContext *context = [CIContext contextWithOptions:@{kCIContextUseSoftwareRenderer : @(NO)}];
CIImage *outputImage = [filter outputImage];
CGImageRef cgImage = [context createCGImage:outputImage fromRect:[outputImage extent]];
UIImage *result = [UIImage imageWithCGImage:cgImage];
CGImageRelease(cgImage);
return result;
}
裁剪
裁剪方法crop:1
2
3
4
5
6
7
8
9
10
11
12
13- (UIImage*)crop:(CGRect)rect
{
CGPoint origin = CGPointMake(-rect.origin.x, -rect.origin.y);
UIImage *img = nil;
UIGraphicsBeginImageContextWithOptions(rect.size, NO, self.scale);
[self drawAtPoint:origin];
img = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return img;
}
裁剪视图:
CLClippingPanel:
subLayer: CLGridLayar -> drawInContext。横竖各画四条线。
CLClippingCircle->drawRect: 四个边角框视图进行边角划线操作。
四个边角视图添加panCircleView手势,实现clippingRect裁剪区域的变更。
CLClippingPanl 自身事件panGridView,拖动格子进行上下左右移动改变clippingRect区域。
原图恢复
resetOriginImage 方法。
原图恢复只恢复原图的frame,原图的方向。 已画涂鸦和马赛克操作不进行恢复。
先将_originalImage 赋值给_imageView,重新计算最新originFrame. 并且将马赛克和涂鸦工具对象的[drawImageView]集合遍历每一层的drawImageView设置为_imageView最新frame,并再次渲染每张图的一条线的涂鸦图片赋值给drawImageView,并更改drawImageView最新的frame.
最后走一遍重新关和开,展示恢复后的裁剪格和图片。这样就完成了原图恢复这样一个功能。
1 | - (void)resetOriginImage{ |
确定
走pushDone方法,调用自身的executeWithCompletionBlock方法,检查UseToolDic是否含有涂鸦,马赛克、文字帖等,进行相关视图的旋转裁剪设置frame等操作。 [self.currentTool executeWithCompletionBlock:
取消
重置currentTool 为nil , 走cleanUp方法,移除相关视图。
此篇文章仅为个人总结,仅为图片编辑中所遇难点提供几点思路。