2019-05-28 画板-擦除-批注-撤销还原等功能实现解决方
通过查阅网上资料总结而得:
1.画板功能
实现方案很多种:
DrawRect+UIBezierPath:实现比较简单,橡皮擦也好实现,但是CPU较高,内存消耗大。
CAShapeLayer+UIBezierPath:性能较好,使用GPU绘制,内存占用也小,但是橡皮擦比较难实现。
综合以上各个方式的特点,产品中比较合适的方案是CAShapeLayer+UIBezierPath,绘制办法很简单:
1.为了支持多方不同的线色、线宽,每一笔都创建一个新的CAShapeLayer,每一笔都是覆盖绘制;
2.使用UIBezierPath描述路径,并将path传递给CAShapeLayer;
调用UIView.layer的addSublayer,将CAShapeLayer绘制到当前View上;
3.缓存CAShapeLayer,在需要撤销一笔的时候只需要调用CAShapeLayer的removeFromSuperlayer,在需要重画一笔的时候重新调用UIView.layer的addSublayer。
我与原文作者的做法不谋而合。
2.擦除功能
橡皮擦本质上跟画线是一样的动作,只是线色是背景色,如果没有背景图片,只需要把线色设置成背景色就可以实现橡皮功能。但是在有背景图片的场景下会比较麻烦,单一背景色会覆盖背景图片。这个时候需要把背景色设置成背景图片,但是需要进行一定的变换,否则橡皮擦的背景图片会发生翻转,
//CTM变换,调整坐标系,*重要*,否则橡皮擦使用的背景图片会发生翻转。
CGContextScaleCTM(context, 1, -1);
CGContextTranslateCTM(context, 0, -self.bounds.size.height);
//swift 写法如下
context?.scaleBy(x: 1, y: -1)
//Changes the scale of the user coordinate system in a context.该方法改变x与y坐标系:(x1,y1) 变成(x1*sx,y1*sy)
//这里sx =1 sy =-1, (x1,y1)变成(x1,-y1)
context?.translateBy(x: 0, y: -self.bounds.size.height)
//Changes the origin of the user coordinate system in a context.该方法修改原点位置
设置画板的背景色的核心代码:
//创建一个新的Context
UIGraphicsBeginImageContext(self.frame.size);
//获得当前Context
CGContextRef context = UIGraphicsGetCurrentContext();
//CTM变换,调整坐标系,*重要*,否则橡皮擦使用的背景图片会发生翻转。
CGContextScaleCTM(context, 1, -1);
CGContextTranslateCTM(context, 0, -self.bounds.size.height);
//图片适配到当前View的矩形区域,会有拉伸
[image drawInRect:self.bounds];
//获取拉伸并翻转后的图片
UIImage *stretchedImg = UIGraphicsGetImageFromCurrentImageContext();
//将变换后的图片设置为背景色
[self setBackgroundColor:[[UIColor alloc] initWithPatternImage:stretchedImg]];
//View的图层设置为原始图片,这里会自动翻转,经过这步后图层显示和橡皮背景都设置为正确的图片。
self.layer.contents = (_Nullable id)image.CGImage;
UIGraphicsEndImageContext();
注意:我们只是改变了画板的坐标系,画板的显示内容还是image的原始内容。
CAShapeLayer+UIBezierPath的橡皮擦实现实际上也比较简单,只要设置颜色为画板的背景色即可。
另一种解决方案:
另外有人的实现方式是把所有的CAShapeLayer图层合并成一张图片,然后在合并后的图片里使用UIBeizerPath的strokeWithBlendMode:kCGBlendModeClear进行混色,这也是一种办法,但是混色占用的CPU也会很高,相对来说,使用背景图片来当橡皮擦的画刷,性能是较好的。
备注:这篇文章很好的介绍了CGContextRef、UIBezierPath、CGMutablePathRef
对应的方法如下:
介绍属性 | CGContextRef | UIBezierPath | CGMutablePathRef |
---|---|---|---|
设置一个起点 | CGContextMoveToPoint | moveToPoint | CGPathMoveToPoint |
添加一个中间点 | CGContextAddLineToPoint | addLineToPoint | CGPathAddRect |
画一个矩形 | CGContextAddRect | bezierPathWithRect | CGPathAddRect |
画一个圆形 | CGContextAddEllipseInRect | bezierPathWithOvalInRect | CGPathAddEllipseInRect |
画一个扇形 | CGContextAddArc | addArcWithCenter | CGPathAddArcToPoint |
二阶曲线 | CGContextAddQuadCurveToPoint | addQuadCurveToPoint | CGPathAddQuadCurveToPoint |
三阶曲线 | CGContextAddCurveToPoint | addCurveToPoint | CGPathAddCurveToPoin |
备注:CAShapeLayer可以制作自定义图案的动画效果,此处不详细叙述。
3.批注功能
绘制文字只能有CGContext开启才行
1.使用UIKit提供的方法进行绘制.
方法说明:
- drawAtPoint:要画到哪个位置
withAttributes:文本的样式.
[str drawAtPoint:CGPointZero withAttributes:nil]; - drawInRect:要将文字绘制到哪个区域
withAttributes:文本的样式.
[str drawInRect:rect withAttributes:nil];
2.drawAtPoint:和drawInRect:的区别?
drawAtPoint:不能够自动换行
drawInRect:能够自动换行
//1.设置要绘制的文字
NSString * str = @"绘制文字练习绘制文字练习绘制文字练习绘制文字练习绘制文字练习";
//2.设置文字的属性,通过Attribute设置
NSMutableDictionary * dict = [[NSMutableDictionary alloc] init];
//设置文字字体
dict[NSFontAttributeName] = [UIFont systemFontOfSize:45];
//设置文字颜色
dict[NSForegroundColorAttributeName] = [UIColor yellowColor];
//设置文字边的宽度
dict[NSStrokeWidthAttributeName] = @5;
//设置文字的描边
dict[NSStrokeColorAttributeName] = [UIColor greenColor];
//设置文字的阴影
NSShadow * shadow = [[NSShadow alloc] init];
shadow.shadowColor = [UIColor cyanColor];
shadow.shadowOffset = CGSizeMake(10, 10);
shadow.shadowBlurRadius = 10;
dict[NSShadowAttributeName] = shadow;
//3.将文字绘制到当前view 的layer上
[str drawAtPoint:CGPointZero withAttributes:nil];
[str drawInRect:rect withAttributes:dict];
使用此方案截图,会产生一张张图片,而不是layer,所以它的撤销与还原处理方式是不一样的。
撤销还原方案,类似栈的管理方式,此处略去。