最终效果如下:
一、简单说明
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