(五十四)涂鸦的实现和截图的保存

利用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];
}





posted on 2015-02-16 11:31  张大大123  阅读(282)  评论(0编辑  收藏  举报

导航