demo.gif

1. Edit [Undo, redo] 2. Use NSKeyedArchiver to store path records

1. The source code

  • You can refer to the source code to adapt to new requirements
  • CanvasView.m
#pragma mark ---------------- CanvasView
#define kStrokeColor [UIColor blackColor].CGColor
#define kStrokeWidth 2.0

@protocol CanvasViewDelegate 

-(void)canUndo: (BOOL)can;
-(void)canRedo: (BOOL)can;
-(void)canFinish: (BOOL)can;
-(void)canClean: (BOOL)can;

@end

@interface CanvasView: UIView
@end

@interface CanvasView()

@property(nonatomic,assign)CGMutablePathRef drawPath;
@property(nonatomic,strong)NSMutableArray *pathArray; //绘制的路径
@property(nonatomic,strong)NSMutableArray *tempPathArray; //重做时临时存放撤销的路径
// 路径是否被释放,防止内存问题
@property(nonatomic,assign)BOOL pathReleased;
@property(nonatomic,weak)id delegate;

@end

@implementation CanvasView

-(instancetype)initWithFrame: (CGRect)frame
                    delegate: (id)delegate{

    if (self = [super initWithFrame:frame]) {
        self.backgroundColor = [UIColor whiteColor];
        self.delegate = delegate;

        [self addObserver:self forKeyPath:@"pathArray.@count" options:NSKeyValueObservingOptionNew context:nil];
        [self addObserver:self forKeyPath:@"tempPathArray.@count" options:NSKeyValueObservingOptionNew context:nil];

        [self refresh];
        [self tempPathArray];
    }

    return self;
}

// kvo
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
    if ([keyPath isEqualToString:@"pathArray.@count"]) {
        NSInteger count = [change[NSKeyValueChangeNewKey] integerValue];
        if (!_delegate) {
            return;
        }
        [_delegate canUndo:count > 0];
        [_delegate canFinish:count > 0];
        [_delegate canClean:count > 0];
    }
    else if ([keyPath isEqualToString:@"tempPathArray.@count"]) {
        NSInteger count = [change[NSKeyValueChangeNewKey] integerValue];
        if (!_delegate) {
            return;
        }
        [_delegate canRedo:count > 0];
    }
}

-(void)drawRect:(CGRect)rect {
    // 绘制上次保存的路径
    for (UIBezierPath *path in [self arrayPath]) {
        [self drawPath:path.CGPath];
    }

    // 如果路径没被释放,绘制新路径
    if (!self.pathReleased) {
        [self drawPath:self.drawPath];
    }
}

#pragma mark 触摸开始
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    // 记录起始点
    UITouch *touch = [touches anyObject];
    CGPoint curLoc = [touch locationInView:self];

    // 创建可变路径
    self.drawPath = CGPathCreateMutable();

    // 设置该路径的起始点
    CGPathMoveToPoint(self.drawPath, NULL, curLoc.x, curLoc.y);

    self.pathReleased = NO;
}

#pragma mark 触摸移动
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
    UITouch *touch = [touches anyObject];
    CGPoint curLoc = [touch locationInView:self];

    // 将当前点加到路径上
    CGPathAddLineToPoint(self.drawPath, NULL, curLoc.x, curLoc.y);

    [self refresh];
}

#pragma mark 触摸结束
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
    UIBezierPath *path = [UIBezierPath bezierPathWithCGPath:self.drawPath];
    // 将该路径保存到数组
    [[self mutableArrayValueForKey:@"pathArray"] addObject:path];

    // 释放路径
    CGPathRelease(self.drawPath);

    self.pathReleased = YES;

    // 只要绘制新路径,就不可再撤销
    [[self arrayTempPath] removeAllObjects];
}

#pragma mark 绘制路径
-(void)drawPath: (CGPathRef)path{
    CGContextRef context = UIGraphicsGetCurrentContext();

    // 设置线宽、颜色、圆角
    CGContextSetLineWidth(context, kStrokeWidth);
    CGContextSetStrokeColorWithColor(context, kStrokeColor);
    CGContextSetLineCap(context, kCGLineCapRound);
    CGContextSetLineJoin(context, kCGLineJoinRound);

    CGContextAddPath(context, path);
    CGContextDrawPath(context, kCGPathStroke);
}

#pragma mark - getters
-(NSMutableArray *)pathArray{
    if (!_pathArray) {
        _pathArray = [NSMutableArray array];
        NSMutableArray *arr = [NSKeyedUnarchiver unarchiveObjectWithFile:[self undoFilePath]];
        arr ? [[self arrayPath] addObjectsFromArray:arr] : nil;
    }
    return _pathArray;
}

-(NSMutableArray *)tempPathArray{
    if (!_tempPathArray) {
        _tempPathArray = [NSMutableArray array];
        NSMutableArray *arr = [NSKeyedUnarchiver unarchiveObjectWithFile:[self redoFilePath]];
        arr ? [[self arrayTempPath] addObjectsFromArray:arr] : nil;
    }
    return _tempPathArray;
}

#pragma mark - public method
-(void)undo{
    [[self arrayTempPath] addObject:[[self arrayPath] lastObject]];
    [[self arrayPath] removeLastObject];
    [self refresh];
}

-(void)redo{
    [[self arrayPath] addObject:[[self arrayTempPath] lastObject]];
    [[self arrayTempPath] removeLastObject];
    [self refresh];
}

-(void)clean{
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"确定清空所有绘制?" message:@"清空后将不可撤销" delegate:self cancelButtonTitle:@"取消" otherButtonTitles:@"清空", nil];
    alert.tag = 1000;
    [alert show];
}

-(UIImage *)renderImage{
    // 归档存储,以便再次编辑
    [NSKeyedArchiver archiveRootObject:self.pathArray toFile:[self undoFilePath]];
    [NSKeyedArchiver archiveRootObject:self.tempPathArray toFile:[self redoFilePath]];

    // 渲染
    UIGraphicsBeginImageContextWithOptions(self.bounds.size, YES, [UIScreen mainScreen].scale);
    [self.layer renderInContext:UIGraphicsGetCurrentContext()];
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    return [self clipImageFromOriginalImage:image inRect:[self getOutlineRectOfCurrentPaths]];
}

#pragma mark - private method

-(NSMutableArray*)arrayPath{
    return [self mutableArrayValueForKey:@"pathArray"];
}

-(NSMutableArray*)arrayTempPath{
    return [self mutableArrayValueForKey:@"tempPathArray"];
}

// undo file path
-(NSString*)undoFilePath{
    return [NSString stringWithFormat:@"%@/painting.undo",[NSHomeDirectory() stringByAppendingPathComponent:@"Documents"]];
}

// redo file path
-(NSString*)redoFilePath{
    return [NSString stringWithFormat:@"%@/painting.redo",[NSHomeDirectory() stringByAppendingPathComponent:@"Documents"]];
}

// 裁剪 - !!!rect记得 x 缩放比
-(UIImage *)clipImageFromOriginalImage: (UIImage*)orgImage
                                inRect: (CGRect)rect{

    rect.origin.x *= orgImage.scale;
    rect.origin.y *= orgImage.scale;
    rect.size.width *= orgImage.scale;
    rect.size.height *= orgImage.scale;

    UIImage *image = [UIImage imageWithCGImage:CGImageCreateWithImageInRect(orgImage.CGImage, rect)];
    return image;
}

//轮廓矩形
-(CGRect)getOutlineRectOfCurrentPaths{
    CGFloat xmin = CGRectGetMaxX(self.bounds);
    CGFloat ymin = CGRectGetMaxY(self.bounds);
    CGFloat xmax = 0;
    CGFloat ymax = 0;

    for (UIBezierPath *path in self.pathArray)
    {
        NSMutableArray *points = [NSMutableArray array];
        CGPathApply(path.CGPath, (__bridge void *)points, getPointsFromBezier);

        for (int i=0; i xmax) {
                xmax = x;
            }
            if (y < ymin) {
                ymin = y;
            }
            if (y > ymax) {
                ymax = y;
            }
        }
    }

    CGRect rect = CGRectMake(xmin, ymin, xmax-xmin, ymax-ymin);
    return rect;
}

// 获取bezierPath上所有的point
void getPointsFromBezier (void *info, const CGPathElement *element) {
    NSMutableArray *bezierPoints = (__bridge NSMutableArray *)info;

    CGPoint *points = element->points;
    CGPathElementType type = element->type;

    switch(type) {
        case kCGPathElementMoveToPoint: // contains 1 point
            [bezierPoints addObject:[NSValue valueWithCGPoint:points[0]]];
            break;

        case kCGPathElementAddLineToPoint: // contains 1 point
            [bezierPoints addObject:[NSValue valueWithCGPoint:points[0]]];
            break;

        case kCGPathElementAddQuadCurveToPoint: // contains 2 points
            [bezierPoints addObject:[NSValue valueWithCGPoint:points[0]]];
            [bezierPoints addObject:[NSValue valueWithCGPoint:points[1]]];
            break;

        case kCGPathElementAddCurveToPoint: // contains 3 points
            [bezierPoints addObject:[NSValue valueWithCGPoint:points[0]]];
            [bezierPoints addObject:[NSValue valueWithCGPoint:points[1]]];
            [bezierPoints addObject:[NSValue valueWithCGPoint:points[2]]];
            break;

        case kCGPathElementCloseSubpath: // contains no point
            break;
    }
}

-(void)refresh{
    [self setNeedsDisplay];
}

-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{
    if (alertView.tag == 1000 && buttonIndex == 1) { //清空
        [[self arrayPath] removeAllObjects];
        [[self arrayTempPath] removeAllObjects];
        [self refresh];
    }
}

-(void)dealloc{
    [self removeObserver:self forKeyPath:@"pathArray.@count"];
    [self removeObserver:self forKeyPath:@"tempPathArray.@count"];
}

@end


文/菲拉兔(简书作者)
原文链接:http://www.jianshu.com/p/5b0eb262e290
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。Copy the code
  • HandWriteController.h
typedef void (^HandWriteControllerCompletion)(UIImage *image);

@interface HandWriteController : UIViewController

@property(nonatomic)HandWriteControllerCompletion completionHandler;

@endCopy the code
  • HandWriteController.m
@interface HandWriteController () @property(nonatomic,strong)UINavigationBar *navBar; @property(nonatomic,strong)CanvasView *canvas; @end @implementation HandWriteController - (void)viewDidLoad { [super viewDidLoad]; The self. The backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent: 0.6]; [self.view addSubview:self.navBar]; [self.view addSubview:self.canvas]; } -(void)viewWillLayoutSubviews{ [super viewWillLayoutSubviews]; CGFloat navH = 44; _navBar.frame = CGRectMake(0, 0, CGRectGetWidth(self.view.bounds), navH); _canvas.frame = CGRectMake(0, navH, CGRectGetWidth(self.view.bounds), CGRectGetHeight(self.view.bounds)-navH); } -(CanvasView *)canvas{ if (! _canvas) { _canvas = [[CanvasView alloc] initWithFrame:CGRectZero delegate:self]; } return _canvas; } -(UINavigationBar *)navBar{ if (! _navBar) { _navBar = [UINavigationBar new]; _navBar.translucent = false; _navBar.barTintColor = [[UIColor blackColor] colorWithAlphaComponent:1]; _navBar.tintColor = [UIColor whiteColor]; _navBar.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleBottomMargin; UIBarButtonItem *cancel = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"icon_handwrite_close"] style:UIBarButtonItemStylePlain target:self action:@selector(navBarButtonItemDidClick:)]; UIBarButtonItem * ok = [[UIBarButtonItem alloc] initWithTitle: @ "complete" style: UIBarButtonItemStylePlain target: the self action:@selector(navBarButtonItemDidClick:)]; UINavigationItem *item = [[UINavigationItem alloc] initWithTitle:@""]; _navBar.items = @[item]; UIBarButtonItem *undo = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"icon_handwrite_undo"] style:UIBarButtonItemStylePlain target:self action:@selector(navBarButtonItemDidClick:)]; UIBarButtonItem *clean = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"icon_handwrite_clean"] style:UIBarButtonItemStylePlain target:self action:@selector(navBarButtonItemDidClick:)]; UIBarButtonItem *redo = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"icon_handwrite_redo"] style:UIBarButtonItemStylePlain target:self action:@selector(navBarButtonItemDidClick:)]; item.leftBarButtonItems = @[cancel,ok]; item.rightBarButtonItems = @[redo,clean,undo]; ok.enabled = false; undo.enabled = false; redo.enabled = false; clean.enabled = false; cancel.tag = 1000; ok.tag =1001; undo.tag = 1002; clean.tag = 1003; redo.tag = 1004; } return _navBar; } -(void)navBarButtonItemDidClick: (UIBarButtonItem*)sender{ switch (sender.tag - 1000) { case 0: // Close [self finishWithImage:nil]; break; Case 1: // Complete [self finishWithImage:[_canvas renderImage]]; break; Case 2: // Undo [_canvas undo]; break; Case 3: // Clear [_canvas Clean]; break; Case 4: // redo [_canvas redo]; break; default: break; } } -(void)finishWithImage: (UIImage*)image{ _completionHandler ? _completionHandler(image) : nil; [self dismissViewControllerAnimated:true completion:nil]; } #pragma mark - CanvasViewDelegate -(void)canUndo:(BOOL)can{ UIBarButtonItem *undo = [[_navBar.items[0] rightBarButtonItems] lastObject]; undo.enabled = can; } -(void)canRedo:(BOOL)can{ UIBarButtonItem *redo = [[_navBar.items[0] rightBarButtonItems] firstObject]; redo.enabled = can; } -(void)canFinish:(BOOL)can{ UIBarButtonItem *ok = [[_navBar.items[0] leftBarButtonItems] lastObject]; ok.enabled = can; } -(void)canClean:(BOOL)can{ UIBarButtonItem *clean = [_navBar.items[0] rightBarButtonItems][1]; clean.enabled = can; } -(UIInterfaceOrientationMask)supportedInterfaceOrientations{ return UIInterfaceOrientationMaskPortrait; } -(BOOL)prefersStatusBarHidden{ return true; } -(void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration{ [_canvas refresh]; } @endCopy the code

2. Usage

- (void)viewDidLoad {
    [super viewDidLoad];
    _iv = [[UIImageView alloc] initWithFrame:CGRectMake(0, 200, 200, 200)];
    [self.view addSubview:_iv];
}
//action
- (IBAction)paintingBoard:(id)sender {
    HandWriteController *vc = [HandWriteController new];
    vc.completionHandler = ^(UIImage *image){
        _iv.image = image;
    };
    vc.modalPresentationStyle = UIModalPresentationOverCurrentContext;
    [self presentViewController:vc animated:true completion:nil];
}Copy the code

Some people suspect that memory will soar, I tested the next, found ok




before




after