最终效果如下:
一、简单说明 1、使用一个数组 strokesArr(笔画数组)记录所有笔画,数组中保存的是一个个的笔画字典,一个字典就是一个笔画,笔画字典中有三项:笔画的大小、颜色、pointsArrInOneStroke数组,(保存的是touch begin时的落笔点和touch move过程中经过的点) 2、绘制的时候,从strokesArr(笔画数组)里取出每一个字典(一个字典就是一个笔画),根据字典中笔画的大小、颜色、笔画所经过的点坐标(pointsArrInOneStroke数组),使用UIBezierPath类完成笔画绘制 二、撤销和回撤 一个笔画就是一个字典。 撤销: 使用abandonedStrokesArr (被丢弃的笔画数组)保存要撤销的笔画,即所有笔画数组中的最后一划, 同时将 strokesArr 笔画数组中的最后一个元素删除。 反之,重做: 即将abandonedStrokesArr (被丢弃的笔画数组)中最后一个元素添加到所有笔画数组中,同时将(被丢弃的笔画数组)中的最后一个元素删除。
Main.storyboard
主控制器
Canvas类封装了画画的所有核心代码
方法列表
// // Canvas.h // 24_Canvas画画板 // // Created by beyond on 14-8-26. // Copyright (c) 2014年 com.beyond. All rights reserved. /* 一、简单说明 1、使用一个数组 strokesArr(笔画数组)记录所有笔画,数组中保存的是一个个的笔画字典,一个字典就是一个笔画,笔画字典中有三项:笔画的大小、颜色、pointsArrInOneStroke数组,(保存的是touch begin时的落笔点和touch move过程中经过的点) 2、绘制的时候,从strokesArr(笔画数组)里取出每一个字典(一个字典就是一个笔画),根据字典中笔画的大小、颜色、笔画所经过的点坐标(pointsArrInOneStroke数组),使用UIBezierPath类完成笔画绘制 二、撤销和回撤 一个笔画就是一个字典。 撤销: 使用abandonedStrokesArr (被丢弃的笔画数组)保存要撤销的笔画,即所有笔画数组中的最后一划, 同时将 strokesArr 笔画数组中的最后一个元素删除。 反之,重做: 即将abandonedStrokesArr (被丢弃的笔画数组)中最后一个元素添加到所有笔画数组中,同时将(被丢弃的笔画数组)中的最后一个元素删除。 */ #import <UIKit/UIKit.h> // 自定义的颜色选择控制器,点击之后,它会告诉代理,选中了什么颜色 @class ColorPickerController; @interface Canvas : UIView #pragma mark - 属性列表 // 标签,显示笔刷大小 @property (nonatomic,retain) IBOutlet UILabel *labelSize; // 滑块 笔刷大小 @property (nonatomic,retain) IBOutlet UISlider *sliderSize; // 三个按钮,分别是撤销、重做、清除 @property (nonatomic,retain) IBOutlet UIBarButtonItem *undoBtn; @property (nonatomic,retain) IBOutlet UIBarButtonItem *redoBtn; @property (nonatomic,retain) IBOutlet UIBarButtonItem *clearBtn; // toolBar,目的是截图的时候,隐藏掉toolBar @property (nonatomic,retain) IBOutlet UIToolbar *toolBar; #pragma mark - 方法列表 // 初始化所有的准备工作 -(void) viewJustLoaded; // 选择相册 被点击 -(IBAction) didClickChoosePhoto; // 滑块滑动,设置笔刷大小 -(IBAction) setBrushSize:(UISlider*)sender; // 撤销 被点击 -(IBAction) undo; // 重做 被点击 -(IBAction) redo; // 清除画布 被点击 -(IBAction) clearCanvas; // 保存图片 被点击 -(IBAction) savePic; // 颜色选择 被点击 - (IBAction) didClickColorButton; // 重要~~开放给另一个控制器调用,它在调用代理时,会传入参数:即选择好的颜色 - (void) pickedColor:(UIColor*)color; @end
// // Canvas.h // 24_Canvas画画板 // // Created by beyond on 14-8-26. // Copyright (c) 2014年 com.beyond. All rights reserved. /* 这儿仅仅是做演示demo,直接让Canvas与控制器绑定,开始画画,监听事件 如果,要更好的抽取出来,则需要创建一个模型类(model)来提供数据源(比如_strokesArr,_abandonedStrokesArr),供CanvasView显示 UIView的setNeedsDisplay和setNeedsLayout方法 首先两个方法都是异步执行的。而setNeedsDisplay会调用自动调用drawRect方法,这样可以拿到 UIGraphicsGetCurrentContext,就可以画画了。 UIUserInterfaceIdiomPad iPad上专用 */ #import "Canvas.h" #import "ColorPickerController.h" #import "BeyondViewController.h" @interface Canvas ()<UIImagePickerControllerDelegate,UINavigationControllerDelegate> { // 所有笔画 NSMutableArray *_strokesArr; // 丢弃(撤销)的笔画 NSMutableArray *_abandonedStrokesArr; // 当前笔刷颜色 UIColor *_currentColor; // 当前的笔刷大小 float currentSize; // 选中的图片 UIImage *_pickedImg; // 截屏图片 UIImage *_screenImg; // 自定义的 颜色选择控制器 ColorPickerController *_colorPickerCtrl; // 相片选择器 UIImagePickerController *_imagePickerCtrl; } @end @implementation Canvas #pragma mark - 生命周期方法 // 禁止多点触摸 -(BOOL)isMultipleTouchEnabled { return NO; } // 最重要的画图方法 - (void) drawRect: (CGRect) rect { // 1.先把获取的图片,画到画布上 [self drawPickedImgToCanvas]; // 2.如果【笔画数组】有笔画字典,则按顺序将笔画取出,画到画布上 [self drawStrokesArrToCanvas]; } // 1.先把获取的图片,画到画布上 - (void)drawPickedImgToCanvas { int width = _pickedImg.size.width; int height = _pickedImg.size.height; CGRect rectForImage = CGRectMake(0, 0, width, height); [_pickedImg drawInRect:rectForImage]; } // 2.如果【笔画数组】有笔画字典,则按顺序将笔画取出,画到画布上 - (void)drawStrokesArrToCanvas { // 如果【笔画数组】为空,则直接返回 if (_strokesArr.count == 0) return; // 遍历【笔画数组】,取出每一个笔画字典,每一次迭代,画一个stroke for (NSDictionary *oneStrokeDict in _strokesArr) { // 取出点数组 NSArray *pointsArr = [oneStrokeDict objectForKey:@"points"]; // 取出颜色 UIColor *color = [oneStrokeDict objectForKey:@"color"]; // 取出笔刷尺寸 float size = [[oneStrokeDict objectForKey:@"size"] floatValue]; // 设置颜色 [color set]; // line segments within a single stroke (path) has the same color and line width // 画一个stroke, 一条接着一条,使用圆接头 round joint // 创建一个贝塞尔路径 UIBezierPath* bezierPath = [UIBezierPath bezierPath]; // 点数组 中的第一个,就是 起点 CGPoint startPoint = CGPointFromString([pointsArr objectAtIndex:0]); // 将路径移动到 起点 [bezierPath moveToPoint:startPoint]; // 遍历点数组,将每一个点,依次添加到 bezierPath for (int i = 0; i < (pointsArr.count - 1); i++) { // 依次取出下一个点 CGPoint pointNext = CGPointFromString([pointsArr objectAtIndex:i+1]); // 添加到路径 [bezierPath addLineToPoint:pointNext]; } // 设置线宽 bezierPath.lineWidth = size; // 线连接处为 圆结头 bezierPath.lineJoinStyle = kCGLineJoinRound; // 线两端为 圆角 bezierPath.lineCapStyle = kCGLineCapRound; // 调用路径的方法 画出一条线 [bezierPath stroke]; } } // 重要~~~初始化所有东东 -(void) viewJustLoaded { // 1.初始化颜色选择控制器 [self addColorPickerCtrl]; // 2.初始化【相片选择器】 [self addUIImagePickerCtrl]; // 3.其他成员初始化 // 【笔画数组】 _strokesArr = [NSMutableArray array]; // 【被丢弃的笔画数组】 _abandonedStrokesArr = [NSMutableArray array]; // 笔画大小 currentSize = 5.0; // toolBar上笔画标签显示文字 self.labelSize.text = @"Size: 5"; // 设置笔刷 黑色 [self setStrokeColor:[UIColor blackColor]]; // 4.设置重做、撤销、清空三个按钮的状态 [self updateToolBarBtnStatus]; } // 1.初始化颜色选择控制器 - (void)addColorPickerCtrl { // 1.添加【颜色选择控制器】ColorPickerController,因为要添加到主控制器中 BeyondViewController *mainVC = [BeyondViewController sharedBeyondViewController]; // 初始化自己封装的颜色选择控制器,并设置好代理,目的是颜色设置好了之后,回调告诉当前的canvas画布 _colorPickerCtrl = [[ColorPickerController alloc] init]; _colorPickerCtrl.pickedColorDelegate = self; // 控制器成为父子关系,视图也成为父子关系 [mainVC addChildViewController:_colorPickerCtrl]; [mainVC.view addSubview:_colorPickerCtrl.view]; // 暂时隐藏【颜色选择控制器】,只有在点击了ToolBar上面的按钮时候,才显示出来 _colorPickerCtrl.view.hidden = YES; } // 2.初始化【相片选择器】 - (void)addUIImagePickerCtrl { if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypePhotoLibrary]) { _imagePickerCtrl = [[UIImagePickerController alloc] init]; _imagePickerCtrl.delegate = self; _imagePickerCtrl.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; // 2) 设置允许修改 // [_imagePickerCtrl setAllowsEditing:YES]; } } // 3.自定义方法,设置 撤销、重做、清空三个按钮的可点击状态 - (void)updateToolBarBtnStatus { _redoBtn.enabled = _abandonedStrokesArr.count > 0; _undoBtn.enabled = _strokesArr.count > 0; _clearBtn.enabled = _strokesArr.count > 0; } #pragma mark - 控件连线方法 // 滑块滑动 - (IBAction)setBrushSize:(UISlider*)sender { currentSize = sender.value; self.labelSize.text = [NSString stringWithFormat:@"Size: %.0f",sender.value]; } // 撤销按钮点击事件 -(IBAction) undo { // 如果笔画数组中有笔画字典 if ([_strokesArr count]>0) { // 最后一个笔画字典,即,被丢弃的笔画字典 NSMutableDictionary* abandonedStrokeDict = [_strokesArr lastObject]; // 将最后一个笔画字典,添加到被丢弃的笔画字典数组里面保存,以供drawRect [_abandonedStrokesArr addObject:abandonedStrokeDict]; // 从所有笔画数组中移除掉最后一笔 [_strokesArr removeLastObject]; // 重新调用drawRect进行绘制 [self setNeedsDisplay]; } // 2.设置重做、撤销、清空三个按钮的状态 [self updateToolBarBtnStatus]; } // 重做 -(IBAction) redo { // 如果 被丢弃的笔画数组,里面有值 if ([_abandonedStrokesArr count]>0) { // 取出最后一个被仍进来的 笔画字典,(即最先书写的,而且是在撤销的操作里面,最后被添加到【被丢弃的笔画数组】) NSMutableDictionary* redoStrokeDict = [_abandonedStrokesArr lastObject]; // 将需要重画的笔画字典,添加到【所有笔画数组】中 [_strokesArr addObject:redoStrokeDict]; // 并且,从【被丢弃的笔画数组】中移除,该笔画字典 [_abandonedStrokesArr removeLastObject]; // 重新调用drawRect进行绘制 [self setNeedsDisplay]; } // 2.设置重做、撤销、清空三个按钮的状态 [self updateToolBarBtnStatus]; } // 清空画布,只需清空【所有笔画数组】和【被丢弃的笔画数组】 -(IBAction) clearCanvas { // 建议不要将选择出来的背景图片清空,只清空没写好的笔画算了 // _pickedImg = nil; [_strokesArr removeAllObjects]; [_abandonedStrokesArr removeAllObjects]; // 重新调用drawRect进行绘制 [self setNeedsDisplay]; // 2.设置重做、撤销、清空三个按钮的状态 [self updateToolBarBtnStatus]; } // 保存图片 -(IBAction) savePic { // 暂时移除 工具条 //[_toolBar removeFromSuperview]; // 截图代码 // 1,开启上下文 UIGraphicsBeginImageContext(self.bounds.size); // 2.将图层渲染到上下文 [self.layer renderInContext:UIGraphicsGetCurrentContext()]; // 开启上下文,使用参数之后,截出来的是原图(YES 0.0 质量高) //UIGraphicsBeginImageContextWithOptions(self.frame.size, YES, 0.0); // 3.从上下文中取出图片 _screenImg = UIGraphicsGetImageFromCurrentImageContext(); // 4.关闭上下文 UIGraphicsEndImageContext(); // 重新添加 工具条,并置最上方 //[self addSubview:_toolBar]; //[self bringSubviewToFront:self.labelSize]; // 调用自定义方法,保存截屏到相册 [self performSelector:@selector(saveToPhoto) withObject:nil afterDelay:0.0]; } // 自定义方法,保存截屏到相册 -(void) saveToPhoto { // 一句话,写到相册 UIImageWriteToSavedPhotosAlbum(_screenImg, nil, nil, nil); // UIAlertView 提示成功 UIAlertView* alertView= [[UIAlertView alloc] initWithTitle:nil message:@"Image Saved" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil]; [alertView show]; } // 点击选择颜色按钮 - (IBAction) didClickColorButton { // 显示或隐藏 自己的【颜色选择控制器】 _colorPickerCtrl.view.hidden = !_colorPickerCtrl.view.hidden; } // 当_colorPickerCtrl选择颜色完毕,会调用代理 的本方法 - (void) pickedColor:(UIColor*)color { // 将【颜色选择控制器】,回调的颜色,设置到控件上,并隐藏 【颜色选择控制器】 [self setStrokeColor:color]; _colorPickerCtrl.view.hidden = !_colorPickerCtrl.view.hidden; } // 重要,设置笔刷 新的颜色 -(void) setStrokeColor:(UIColor*)newColor { _currentColor = newColor; } // 点击,选择相片按钮 -(IBAction) didClickChoosePhoto { // 展现,相片选择控制器 [self addSubview:_imagePickerCtrl.view]; } #pragma mark - imagePicker代理方法 - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info { // 必须手动,关闭照片选择器 [picker.view removeFromSuperview]; // 从info字典得到编辑后的照片【UIImagePickerControllerEditedImage】 _pickedImg = [info valueForKey:@"UIImagePickerControllerOriginalImage"]; // 将图片画到画板上去 [self setNeedsDisplay]; } // 【相片选择器】的代理方法,点击取消时,也要隐藏相片选择器 - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker { [_imagePickerCtrl.view removeFromSuperview]; } #pragma mark - 核心代码,重要~~~画布上手势处理 // 手势开始(画笔落下) // 开始一个新的字典,为每一笔,包括点 和 颜色 // Start new dictionary for each touch, with points and color - (void) touchesBegan:(NSSet *) touches withEvent:(UIEvent *) event { // 一个笔画中的所有点,触摸开始时的【起点】 NSMutableArray *pointsArrInOneStroke = [NSMutableArray array]; NSMutableDictionary *strokeDict = [NSMutableDictionary dictionary]; [strokeDict setObject:pointsArrInOneStroke forKey:@"points"]; // 笔的颜色 [strokeDict setObject:_currentColor forKey:@"color"]; // 笔的大小 [strokeDict setObject:[NSNumber numberWithFloat:currentSize] forKey:@"size"]; // 落笔点 CGPoint point = [[touches anyObject] locationInView:self]; [pointsArrInOneStroke addObject:NSStringFromCGPoint(point)]; [_strokesArr addObject:strokeDict]; } // 将每一个点添加到 点数组 // Add each point to points array - (void) touchesMoved:(NSSet *) touches withEvent:(UIEvent *) event { // 移动后的一个点 CGPoint point = [[touches anyObject] locationInView:self]; // 前一个点 CGPoint prevPoint = [[touches anyObject] previousLocationInView:self]; // 字典中先前的点数组 NSMutableArray *pointsArrInOneStroke = [[_strokesArr lastObject] objectForKey:@"points"]; // 在后面追加 新的点 [pointsArrInOneStroke addObject:NSStringFromCGPoint(point)]; CGRect rectToRedraw = CGRectMake(\ ((prevPoint.x>point.x)?point.x:prevPoint.x)-currentSize,\ ((prevPoint.y>point.y)?point.y:prevPoint.y)-currentSize,\ fabs(point.x-prevPoint.x)+2*currentSize,\ fabs(point.y-prevPoint.y)+2*currentSize\ ); [self setNeedsDisplayInRect:rectToRedraw]; } // 手势结束(画笔抬起) // Send over new trace when the touch ends - (void) touchesEnded:(NSSet *) touches withEvent:(UIEvent *) event { [_abandonedStrokesArr removeAllObjects]; // 2.设置重做、撤销、清空三个按钮的状态 [self updateToolBarBtnStatus]; } @end
颜色选择控制器
ColorPickerController
// // ColorPickerController.h // 24_Canvas画画板 // // Created by beyond on 14-8-26. // Copyright (c) 2014年 com.beyond. All rights reserved. // #import <UIKit/UIKit.h> @interface ColorPickerController : UIViewController #pragma mark - 属性列表 // xib上的imgView @property (nonatomic,retain) IBOutlet UIImageView *imgView; // 代理用weak @property (weak) id pickedColorDelegate; #pragma mark - 方法列表 // 核心,根据位图引用 创建基于该位图的上下文对象 - (CGContextRef) createARGBBitmapContextFromImage:(CGImageRef)inImage; // 核心,根据触摸点,从上下文中取出对应位置像素点的颜色值 - (UIColor*) getPixelColorAtLocation:(CGPoint)point; @end
核心代码
// // ColorPickerController.m // 24_Canvas画画板 // // Created by beyond on 14-8-26. // Copyright (c) 2014年 com.beyond. All rights reserved. // #import "ColorPickerController.h" #import "Canvas.h" @implementation ColorPickerController #pragma mark - 点击结束 - (void) touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event { UITouch* touch = [touches anyObject]; // tap点击的位置 CGPoint point = [touch locationInView:self.imgView]; // 1.调用自定义方法,从【点】中取颜色 UIColor *selectedColor = [self getPixelColorAtLocation:point]; // 2.告诉代理,解析出来的颜色 [_pickedColorDelegate pickedColor:selectedColor]; } // 核心代码:关于下面两个方法更多的详细资料,敬请查阅【iOS Developer Library 】 #pragma mark - 核心代码,将图片写入内存,再依据【点】中取颜色 - (UIColor *) getPixelColorAtLocation:(CGPoint)point { UIColor *color = nil; // 得到取色图片的引用 CGImageRef colorImage = _imgView.image.CGImage; // Create off screen bitmap context to draw the image into. Format ARGB is 4 bytes for each pixel: Alpa, Red, Green, Blue // 调用自定义方法:从_imgView里面的image的引用,创建并返回对应的上下文 CGContextRef contexRef = [self createARGBBitmapContextFromImage:colorImage]; // 如果创建该图片对应的上下文失败 if (contexRef == NULL){ NSLog(@"取色图片--创建对应的上下文失败~"); return nil; } // 准备将【取色图片】写入刚才创建出来的上下文 size_t w = CGImageGetWidth(colorImage); // problem! size_t h = CGImageGetHeight(colorImage); CGRect rect = {{0,0},{w,h}}; log_rect(rect) // 调试输出rect:--{{0, 0}, {225, 250}} int bytesPerRow = CGBitmapContextGetBytesPerRow(contexRef); log_int(bytesPerRow) //调试输出int:--900 // Draw the image to the bitmap context. Once we draw, the memory // allocated for the context for rendering will then contain the // raw image data in the specified color space. // 将位图写入(渲染)已经分配好的内存区域 CGContextDrawImage(contexRef, rect, colorImage); // 得到位图上下文 内存数据块的首地址,用指针记住,作为基地址 unsigned char* dataPoint = CGBitmapContextGetData (contexRef); NSLog(@"----首地址,指针%p",dataPoint); // ----首地址,指针0x8b3f000 if (dataPoint != NULL) { //offset 即:根据触摸点的xy,定位到位图内存空间中的一个特定像素 //4 的意思是每一个像素点,占4个字节 // w是每一行所有点的总数 // 根据所在行,所在列,算出在内存块中的偏移地址,然后乘以4,因为每一个点在内存中占四个字节 int offset = 4*((w*round(point.y))+round(point.x)); // alpha 为内存基地址+偏移地址 int alpha = dataPoint[offset]; // red 为内存基地址+偏移地址+1 其他类似 int red = dataPoint[offset+1]; int green = dataPoint[offset+2]; int blue = dataPoint[offset+3]; NSLog(@"偏移地址: %i colors: RGBA %i %i %i %i",offset,red,green,blue,alpha); // offset: 150908 colors: RGB A 255 0 254 255 // 根据RGBA 生成颜色对象 color = [UIColor colorWithRed:(red/255.0f) green:(green/255.0f) blue:(blue/255.0f) alpha:(alpha/255.0f)]; } // 操作完成后,释放上下文对象 CGContextRelease(contexRef); // 从内存中释放掉 加载到内存的图像数据 if (dataPoint) { free(dataPoint); } return color; } // 自定义方法2:通过_imgView里面的image的引用,创建并返回对应的上下文 - (CGContextRef) createARGBBitmapContextFromImage:(CGImageRef) inImage { // 要创建的上下文 CGContextRef context = NULL; // 色彩空间 CGColorSpaceRef colorSpace; // 位图数据在内存空间的首地址 void * bitmapData; // 每一行的字节数 int bitmapBytesPerRow; // 图片总的占的字节数 int bitmapByteCount; // 得到图片的宽度和高度,将要使用整个图片,创建上下文 size_t pixelsWide = CGImageGetWidth(inImage); size_t pixelsHigh = CGImageGetHeight(inImage); // 每一行占多少字节. 本取色图片中的每一个像素点占4个字节; // 红 绿 蓝 透明度 各占一个字节(8位 取值范围0~255) // 每一行的字节数,因为每一个像素点占4个字节(包含RGBA)(其中一个R就是一个字节,占8位,取值是2的8次方 0~255) bitmapBytesPerRow = (pixelsWide * 4); // 图片总的占的字节数 bitmapByteCount = (bitmapBytesPerRow * pixelsHigh); // 使用指定的 色彩空间(RGB) colorSpace = CGColorSpaceCreateDeviceRGB(); if (colorSpace == NULL) { fprintf(stderr, "创建并分配色彩空间 出错\n"); return NULL; } // This is the destination in memory // where any drawing to the bitmap context will be rendered. // 为取色图片数据 分配所有的内存空间 // 所有画到取色图片上下文的操作,都将被渲染到此内存空间 bitmapData = malloc( bitmapByteCount ); if (bitmapData == NULL) { fprintf (stderr, "内存空间分配失败~"); CGColorSpaceRelease( colorSpace ); return NULL; } // 创建位图上下文. 使用 pre-multiplied ARGB, ARGB中的每一个成员都占8个bit位,即一字节,一个像素共占4个字节 // 无论原取色图片的格式是什么(CMYK或Grayscale),都将通过CGBitmapContextCreate方法,转成指定的ARGB格式 context = CGBitmapContextCreate (bitmapData, pixelsWide, pixelsHigh, 8, // bits per component bitmapBytesPerRow, colorSpace, kCGImageAlphaPremultipliedFirst); if (context == NULL) { free (bitmapData); fprintf (stderr, "位图上下文创建失败~"); } // 在返回上下文之前 必须记得释放 色彩空间 CGColorSpaceRelease( colorSpace ); return context; } @end