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
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];
}
有人怀疑内存会飙升的问题,我测试了下,发现还好