先上个图,有画箭头,画方框,画曲线,旋转,裁剪,上一步,下一步,清空画布,扩充图片,放大缩小,拖拽等,然后可以对画过的图形进行编辑功能

一.画板的整体实现
主要参考了这篇文章,https://www.jianshu.com/p/8c145884cf2c
然后采用了NSUndoManager+ Quartz2D 画图方法。底部一个UIImageview 用来显示图片,然后再来一个drawView当做画布。
self.imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 44, CGRectGetWidth(self.view.bounds), CGRectGetHeight(self.view.bounds) - 44)];
self.imageView.image = self.image;
self.imageView.contentMode = UIViewContentModeScaleAspectFit;
[self.view addSubview:self.imageView];
[self.imageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.view);
}];
self.drawView = [EDDrawView new];
self.drawView.backgroundColor = [UIColor clearColor];
self.drawView.image = self.image;
self.drawView.drawType = EDDrawViewToolType_Arrow;
self.drawView.lineColor = getColorWithColorType(EDDrawPenColorType_Red);
self.drawView.lineWidth = getLineWithPenWithType(EDDrawPenWidthType_W9);
self.drawView.fontSize = getFontSizeWithPenWithType(EDDrawPenWidthType_W9);
self.drawView.selectLineColor = [UIColor colorWithHex:0x8FC4FF];
self.drawView.selectLineWidth = 2;
self.drawView.delegate = self;
[self.view addSubview:self.drawView];
[self.drawView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.view);
}];
为了实现画布的放大缩小、拖拽功能加上手势操作,通过手势改变背景图 和画布的CGAffineTransform
CGAffineTransform move = CGAffineTransformMakeTranslation(self.transformPoint.x, self.transformPoint.y);
CGAffineTransform scale = CGAffineTransformMakeScale(self.scale, self.scale);
CGAffineTransform transform = CGAffineTransformConcat(move, scale);
[self.layer setAffineTransform:transform];
if ([self.delegate respondsToSelector:@selector(didDrawTransformUpdate:)]) {
[self.delegate didDrawTransformUpdate:transform];
}
二.画图操作
画图功能是算是这个功能模块里面的难点,主要监听touchesBegan,touchesMoved,touchesEnded这三个代理方法,然后记录关键点实现。
1.画箭头
CGContextTranslateCTM(context, _startPoint.x, _startPoint.y);
CGPoint newEndPont = CGPointMake(_endPoint.x - _startPoint.x, _endPoint.y - _startPoint.y);
CGFloat length = sqrt( pow(fabs(newEndPont.x), 2) + pow(fabs(newEndPont.y), 2) );
if (length < 10) {
return;
}
CGFloat angle = 0.0;
if (newEndPont.x > 0 && newEndPont.y >= 0) {
angle = atan(fabs(newEndPont.y / newEndPont.x * 1.0));
}else if (newEndPont.x <= 0 && newEndPont.y > 0){
angle = atan(fabs(newEndPont.x / newEndPont.y * 1.0)) + M_PI_2;
}else if (newEndPont.x < 0 && newEndPont.y <= 0){
angle = atan(fabs(newEndPont.y / newEndPont.x * 1.0)) + M_PI;
}else if (newEndPont.x >= 0 && newEndPont.y < 0){
angle = atan(fabs(newEndPont.x / newEndPont.y * 1.0)) + M_PI + M_PI_2;
}
CGContextRotateCTM(context, angle);
if (_path) {
CGPathRelease(_path);
}
_path = CGPathCreateMutable();
CGContextBeginPath(context);
CGPathMoveToPoint(_path, NULL, length, 0);
CGPathAddLineToPoint(_path, NULL, length - 10 * 0.25 - self.lineWidth * 2.5, -6 * 0.25 - self.lineWidth * 0.6 * 2.5);
CGPathAddLineToPoint(_path, NULL,length - 9 * 0.25 - self.lineWidth * 0.9 * 2.5, -3 * 0.25 - self.lineWidth * 0.3 * 2.5);
CGPathAddLineToPoint(_path, NULL, 0, -2);
CGPathAddLineToPoint(_path, NULL, 0, 2);
CGPathAddLineToPoint(_path, NULL,length - 9 * 0.25 - self.lineWidth * 0.9 * 2.5, 3 * 0.25 + self.lineWidth * 0.3 * 2.5);
CGPathAddLineToPoint(_path, NULL,length - 10 * 0.25 - self.lineWidth * 2.5, 6 * 0.25 + self.lineWidth * 0.6 * 2.5);
CGPathCloseSubpath(_path);
CGContextAddPath(context, _path);
CGContextSetFillColorWithColor(context, self.color.CGColor);
CGContextFillPath(context);
画箭头如果想通过2个点,计算出7个关键点的坐标实际上是非常复杂的,这里采用的是先通过2个点画一个水平向右的箭头,然后通过2个点计算出角度旋转画布。
2.画方框
CGPathMoveToPoint(_path, NULL, _startPoint.x, _endPoint.y);
CGPathAddLineToPoint(_path, NULL, _startPoint.x, _startPoint.y);
CGPathAddLineToPoint(_path, NULL, _endPoint.x, _startPoint.y);
CGPathAddLineToPoint(_path, NULL, _endPoint.x, _endPoint.y);
CGPathCloseSubpath(_path);
CGContextSetLineJoin(context, kCGLineJoinRound);
CGContextAddPath(context, _path);
CGContextSetStrokeColorWithColor(context, self.color.CGColor);
CGContextSetLineWidth(context, self.lineWidth);
这个比较简单,两个点可以很容易的计算出4个关键点的坐标
3.画曲线
_path = CGPathCreateMutable();
[self.pointArray enumerateObjectsUsingBlock:^(NSValue *pointValue, NSUInteger idx, BOOL * _Nonnull stop) {
CGPoint point = [pointValue CGPointValue];
if (idx == 0) {
CGPathMoveToPoint(_path, NULL, point.x, point.y);
}else{
CGPoint prePoint = [self.pointArray[idx - 1] CGPointValue];
CGPoint currentPoint = point;
CGPoint midPoint = CGPointMake((prePoint.x + currentPoint.x) * 0.5, (prePoint.y + currentPoint.y) * 0.5);
CGPathAddQuadCurveToPoint(_path, NULL, midPoint.x, midPoint.y, currentPoint.x, currentPoint.y);
}
}];
CGContextSetLineCap(context, kCGLineCapRound);
CGContextSetLineJoin(context, kCGLineJoinRound);
CGContextAddPath(context, _path);
CGContextSetStrokeColorWithColor(context, self.color.CGColor);
CGContextSetLineWidth(context, self.lineWidth);
CGContextStrokePath(context);
曲线里面记录了经过的每一个点,然后任意两点通过贝塞尔曲线连接,这样画的曲线就比较圆润。
4.画文字
CGRect rect = CGRectMake(_startPoint.x, _startPoint.y, _textBoundingSize.width, _textBoundingSize.height);
CGContextTranslateCTM(context, _startPoint.x, _startPoint.y);
CGContextTranslateCTM(context, 0, _textBoundingSize.height);
CGContextScaleCTM(context, 1, -1);
CGContextSetShouldSmoothFonts(context, YES);
_path = CGPathCreateMutable();
CGPathAddRect(_path, NULL, (CGRect){CGPointMake(4, -6),rect.size});
NSAttributedString *contentString = [[NSAttributedString alloc] initWithString:_content attributes:@{NSForegroundColorAttributeName:self.color,NSFontAttributeName:self.fontSize}];
{
NSMutableAttributedString *attString = [[NSMutableAttributedString alloc] initWithAttributedString:contentString];
[attString addAttributes:@{NSForegroundColorAttributeName:[UIColor whiteColor],
NSStrokeColorAttributeName:[UIColor whiteColor],
NSStrokeWidthAttributeName:@(20),
} range:NSMakeRange(0, attString.length)];
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFTypeRef)attString);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attString.length), _path, NULL);
CTFrameDraw(frame, context);
}
{
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFTypeRef)contentString);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, contentString.length), _path, NULL);
CTFrameDraw(frame, context);
}
画文字有点复杂,首先要搞清楚屏幕坐标映射关系和矩阵变换,可以看看这篇文章,https://www.jianshu.com/p/5ab7ed3f8958,

然后就是这种文字的描边效果实际上画两遍文字实现的。拖拽文字的边框实现文字的自由排版目前做的不是很好。
三.图形的编辑操作

每个图形要实现一个边缘检测方法,用来判断当前点是否在图形内部。如果是点击了图形,图形变为选中状态,这时手势的操作就是针对该图形的操作,通过改变坐标重新绘制图形即可。
四.图片的扩充功能

正常情况下,画图只能在图片上进行,如果图形超出图片的大小,就要进行裁剪。这里采用的扩充图片的方式,每次画完一个图形就检测是否超出图片边缘的限制,超出了就计算超出的最远点,然后图片就被扩冲到该最远点。
五.图片的导出功能
目前图片导出是通过截屏方式,然后裁剪出画图区域。这种方式生成的图片大小好像比较固定,质量不高。比较好的做法是先画上原图,然后把画过的图形依次画到原图上。
CGFloat scale = [UIScreen mainScreen].scale;
UIGraphicsBeginImageContextWithOptions(self.drawView.frame.size, NO, scale);
CGContextRef context = UIGraphicsGetCurrentContext();
[self.imageView.layer renderInContext:context];
[self.drawView.layer renderInContext:context];
UIImage *snap = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
UIImage *retImage = [snap imageByCropToRect:cutRect];