zoukankan      html  css  js  c++  java
  • (五十四)涂鸦的实现和截图的保存

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





  • 相关阅读:
    block为什么用copy以及如何解决循环引用
    iOS证书失效
    基于AFNetWorking封装一个网络请求数据的类
    Xcode的内存清理
    block的用法以及block和delegate的比较(转发)
    React-Native 获取node.js提供的接口
    npm创建和发布模块
    React-Native之ViewPagerAndroid的使用
    使用.NET框架、Web service实现Android的文件上传(二)
    使用.NET框架、Web service实现Android的文件上传(一)
  • 原文地址:https://www.cnblogs.com/aiwz/p/6154197.html
Copyright © 2011-2022 走看看