iOS_24_畫畫板(含取色板) – iPhone手機開發技術文章 iPhone軟體開發教學課程

最終效果如下:

一、簡單說明
 1、使用一個數組 strokesArr(筆畫數組)記錄所有筆畫,數組中保存的是一個個的筆畫字典,一個字典就是一個筆畫,筆畫字典中有三項:筆畫的大小、顏色、pointsArrInOneStroke數組,(保存的是touch  begin時的落筆點和touch move過程中經過的點)
 
 2、繪制的時候,從strokesArr(筆畫數組)裡取出每一個字典(一個字典就是一個筆畫),根據字典中筆畫的大小、顏色、筆畫所經過的點坐標(pointsArrInOneStroke數組),使用UIBezierPath類完成筆畫繪制
 
 二、撤銷和回撤
 一個筆畫就是一個字典。 
 撤銷:
 使用abandonedStrokesArr (被丟棄的筆畫數組)保存要撤銷的筆畫,即所有筆畫數組中的最後一劃,
 同時將 strokesArr 筆畫數組中的最後一個元素刪除。
 反之,重做:
 即將abandonedStrokesArr (被丟棄的筆畫數組)中最後一個元素添加到所有筆畫數組中,同時將(被丟棄的筆畫數組)中的最後一個元素刪除。

Main.storyboard

vcD4KPHA+PGJyPgo8L3A+CjxwPtb3v9jWxsb3PC9wPgo8cD48aW1nIHNyYz0=”https://www.2cto.com/uploadfile/Collfiles/20140828/20140828084655121.png” alt=”\”>

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 
// 自定義的顏色選擇控制器,點擊之後,它會告訴代理,選中瞭什麼顏色
@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 ()
{
    // 所有筆畫
	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  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 


@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

發佈留言