(五十四)涂鸦的实现和截图的保存
利用touchesMoved来获取各个触摸点,并存入一个数组。
在drawRect方法内,循环生成这些点,当i=0时,使用CGContextMoveToPoint方法移动到起点,其余点都通过CGContextAddLineToPoint方法连线。
这样的问题是起点只有一个,画完一条线如果再开始画,会把上次的终点连接到这次的起点;另一个问题是无法回退到上一笔,因为没有记录每次的起点,只是记录了每一个移动微元。
因此应该使用数组存储一条线(从开始触摸到停止触摸),用一个大数组存储这些小数组,为了方便描述,设大数组为S,小数组为C。
在touchesBegan方法中,新建一个小数组C1,将起点装入C1,并将C1装入S,最后还要调用setNeedsDisplay来更新屏幕上的点。
在touchesMoved方法中,使用S的lastObject方法取出现在所在的C,这时取出的为C1,将新点加入C1,注意因为都是指针指向数组,只要改了C1,就改了C中的C1。不要忘记最后调用setNeedsDisplay来更新屏幕上的点。
在touchesEnded方法中,可以发现和touchesMoved方法过程一致,因此只要调用一次touchesMoved即可,传入自己的参数。
这样不仅记录了所有的点,还记录了每一次开始到终止的位置。
最初我的一个思路是每次drawRect中只重新画最后一个数组,但是发现这样以前的线会消失,经过思考发现是drawRect每次调用前会自动清屏,后来想想这是非常科学的,想想以前用单片机做UI还得手动清屏,这个的确方便。
这样的话,清屏只需要调用S数组的removeAllObjects方法,而回退只需要调用S数组的removeLastObject方法。
具体过程为:
首先新建S数组,并且重写get方法初始化:
@property (nonatomic, strong) NSMutableArray *totalPathPoints;
- (NSMutableArray *)totalPathPoints { if (_totalPathPoints == nil) { _totalPathPoints = [NSMutableArray array]; } return _totalPathPoints; }起点的操作为新建字数组C,将当前点加入,最后装入S:不要忘记更新屏幕!
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch = [touches anyObject]; CGPoint startPos = [touch locationInView:touch.view]; // 每一次开始触摸, 就新建一个数组来存放这次触摸过程的所有点(这次触摸过程的路径) NSMutableArray *pathPoints = [NSMutableArray array]; [pathPoints addObject:[NSValue valueWithCGPoint:startPos]]; // 添加这次路径的所有点到大数组中 [self.totalPathPoints addObject:pathPoints]; [self setNeedsDisplay]; }
移动操作为将当前点加入S数组的最后一个元素:由于是使用指针,因此改变后不必赋值回去。
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch = [touches anyObject]; CGPoint pos = [touch locationInView:touch.view]; // 取出这次路径对应的数组 NSMutableArray *pathPoints = [self.totalPathPoints lastObject]; [pathPoints addObject:[NSValue valueWithCGPoint:pos]]; [self setNeedsDisplay]; }
终点的操作与移动操作完全一致,因此调用一次移动操作:
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { [self touchesMoved:touches withEvent:event]; }最后是drawRect方法,因为每次调用drawRect方法都会清屏,因此要重绘所有的点和线。
使用双层循环,外层从S中取得C,内层遍历C中的点,同样地,将第一个点作为起点(MoveToPoint),后面通过连线到达(AddLineToPoint)。
这里顺带复习了使用Quartz2D绘图的方法,先获取上下文,然后调用绘图函数、设置状态等,最后通过Stroke或者Fill方法将上下文中的图像呈现在View上。
- (void)drawRect:(CGRect)rect { CGContextRef ctx = UIGraphicsGetCurrentContext(); // 注意每次drawRect都会清屏,因此需要重绘所有的线。 for (NSMutableArray *pathPoints in self.totalPathPoints) { for (int i = 0; i<pathPoints.count; i++) { // 一条路径 CGPoint pos = [pathPoints[i] CGPointValue]; if (i == 0) { CGContextMoveToPoint(ctx, pos.x, pos.y); } else { CGContextAddLineToPoint(ctx, pos.x, pos.y); } } } CGContextSetLineCap(ctx, kCGLineCapRound); CGContextSetLineJoin(ctx, kCGLineJoinRound); CGContextSetLineWidth(ctx, 5); CGContextStrokePath(ctx); }回退和清屏非常简单,只需要移除S中的最后一个或者全部元素即可:
- (void)clear { [self.totalPathPoints removeAllObjects]; [self setNeedsDisplay]; } - (void)back { [self.totalPathPoints removeLastObject]; [self setNeedsDisplay]; }
要实现截图,首先要获取绘图板View的内容,可以通过给UIImage增加分类来实现捕捉视图:注意上下文有开启就应当有结束。
+ (instancetype)captureWithView:(UIView *)view { // 1.开启上下文,第二个参数是是否不透明(opaque)NO为透明,这样可以防止占据额外空间(例如圆形图会出现方框),第三个为伸缩比例,0.0为不伸缩。 UIGraphicsBeginImageContextWithOptions(view.frame.size, NO, 0.0); // 2.将控制器view的layer渲染到上下文 [view.layer renderInContext:UIGraphicsGetCurrentContext()]; // 3.取出图片 UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext(); // 4.结束上下文 UIGraphicsEndImageContext(); return newImage; }
这样就可以得到View上的视图。
要保存图片,使用UIImageWriteToSavedPhotosAlbum函数,注意这是一个C语言函数:
/** * 保存图片到用户相册 * * @param image 要保存的图片 * @param completionTarget 完成后调用的方法所在的对象 * @param completionSelector 完成后调用的方法 * @param contextInfo 额外上下文消息,一般为空 */ void UIImageWriteToSavedPhotosAlbum(UIImage *image, id completionTarget, SEL completionSelector, void *contextInfo);需要注意的是,完成后调用的函数是需要传入参数的,官方在上面的函数上给出了建议:
// Adds a photo to the saved photos album. The optional completionSelector should have the form: // - (void)image:(UIImage *)image didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo;因此应当严格按照这个要求写完成后的回调函数,但是注意的是方法名是image:didFinishSavingWithError:contextInfo:,换句话说就是方法名只包括描述部分和冒号,不包括参数类型和参数名。
应该这样调用:
UIImageWriteToSavedPhotosAlbum(image, self, @selector(image:didFinishSavingWithError:contextInfo:), nil);
并且实现这个方法:注意这里用到一个技巧,如果成功error会是nil,这样就会进入else,否则会进入error的条件分支。
这里使用了一个第三方类库MBProgressHUD来作为指示器,并且使用了李明杰老师进一步封装的版本,在这里感谢李明杰老师。
- (void)image:(UIImage *)image didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo { if (error) { // 保存失败 [MBProgressHUD showError:@"保存失败"]; } else { // 保存成功 [MBProgressHUD showSuccess:@"保存成功"]; } }
需要注意的是第一次保存时系统会询问用户是否允许App访问相册,如果不允许则以后不再弹框,要通过修改设置中的隐私->相册中主动开启,如果保存失败,可以通过截图引导用户来操作。
要保存多个路径,还可以使用CGMultablePathRef来创建路径,然后分别加入上下文绘制,需要注意的是它不是OC对象,因此不能直接放入数组,所以这个方法比较麻烦。
还可以使用UIBezierPath对象来实现这个方法,称为贝塞尔曲线对象,每一个UIBezierPath对应一条完整的曲线。可以简单的理解C语言中的CGMutablePathRef对应OC中的UIBezierPath。使用它的好处是全是OC的调用,比较亲切和简洁。
使用UIBezierPath绘图非常简洁,例如绘制一条(0,0)到(100,100)的直线:与之前过程基本一致,好处是面向对象。
UIBezierPath *path = [UIBezierPath bezierPath]; [path moveToPoint:CGPointZero]; [path addLineToPoint:CGPointMake(100,100)]; [path stroke];
这样实现起来就很简单了,创建UIBezierPath对象的数组即可,每一次完整的触摸过程都对应一个UIBezierPath。
起点时创建一个新的UIBezierPath,并且使用moveToPoint记录起点,最后将UIBezierPath加入对象数组,并更新屏幕。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { // 1.获得当前的触摸点 UITouch *touch = [touches anyObject]; CGPoint startPos = [touch locationInView:touch.view]; // 2.创建一个新的路径 UIBezierPath *currenPath = [UIBezierPath bezierPath]; currenPath.lineCapStyle = kCGLineCapRound; currenPath.lineJoinStyle = kCGLineJoinRound; // 设置起点 [currenPath moveToPoint:startPos]; // 3.添加路径到数组中 [self.paths addObject:currenPath]; [self setNeedsDisplay]; }移动和结束时都是获取当前点,取出最后一个UIBezierPath对象并且使用addLineToPoint方法加入这个点:
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch = [touches anyObject]; CGPoint pos = [touch locationInView:touch.view]; UIBezierPath *currentPath = [self.paths lastObject]; [currentPath addLineToPoint:pos]; [self setNeedsDisplay]; } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { [self touchesMoved:touches withEvent:event]; }
在drawRect方法中,取出每个path,调用stroke方法绘制:在这里可以设置全局的状态,例如线宽。
for (UIBezierPath *path in self.paths) { path.lineWidth = 10; [path stroke]; }