【iOS】实现一个简单的画板 controller

848 阅读4分钟
原文链接: www.jianshu.com

demo.gif

1.支持再次编辑【撤销、重做】
2.用NSKeyedArchiver实现绘制路径记录的存储

1.源码

  • 可参考源码自己改动以适应新需求
  • 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
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。
  • HandWriteController.h
typedef void (^HandWriteControllerCompletion)(UIImage *image);

@interface HandWriteController : UIViewController

@property(nonatomic)HandWriteControllerCompletion completionHandler;

@end
  • HandWriteController.m
@interface HandWriteController ()

@property(nonatomic,strong)UINavigationBar *navBar;
@property(nonatomic,strong)CanvasView *canvas;

@end

@implementation HandWriteController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.view.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:@"完成" style:UIBarButtonItemStylePlain target: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: //关闭
            [self finishWithImage:nil];
            break;
        case 1: //完成
            [self finishWithImage:[_canvas renderImage]];
            break;
        case 2: //撤销
            [_canvas undo];
            break;
        case 3: //清空
            [_canvas clean];
            break;
        case 4: //重做
            [_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];
}

@end

2.用法

- (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];
}

有人怀疑内存会飙升的问题,我测试了下,发现还好


before

after