由于項目需求需要用到一個畫板功能,需要這個畫板可以實時的畫窍侧,并且需要保存畫板點集合從一端發(fā)送給另一端 達到一個實時同步的功能,前后使用了三種方法,每一種都遇到各種坑(后面會提到,每一種方法的優(yōu)缺點),而且現(xiàn)在能百度到的demo普遍偏簡單,分享出來給大家一個參照吧朱庆。<UIBezierPath畫線,NSUndoManager+ Quartz2D ,OpenGLES>
Demo UI寫的很是糙 大家不要吐槽斥滤。
一. UIBezierPath 畫板方法的實現(xiàn)
1 UIBezierPath
使用UIBezierPath可以創(chuàng)建基于矢量的路徑滚澜,此類是Core Graphics框架關(guān)于路徑的封裝蓉媳。使用此類可以定義簡單的形狀八酒,如橢圓空民、矩形或者有多個直線和曲線段組成的形狀等。
UIBezierPath是CGPathRef數(shù)據(jù)類型的封裝羞迷。如果是基于矢量形狀的路徑界轩,都用直線和曲線去創(chuàng)建。我們使用直線段去創(chuàng)建矩形和多邊形衔瓮,使用曲線去創(chuàng)建圓蛔腔(arc)、圓或者其他復雜的曲線形狀热鞍。
使用UIBezierPath畫圖步驟:
創(chuàng)建一個UIBezierPath對象
調(diào)用-moveToPoint:設置初始線段的起點
添加線或者曲線去定義一個或者多個子路徑
改變UIBezierPath對象跟繪圖相關(guān)的屬性葫慎。如,我們可以設置畫筆的屬性薇宠、填充樣式等
基本介紹就不多說了,大家可以參照下面這位簡友的博客,寫的很詳細了:
2 下面介紹一下實現(xiàn)細節(jié)
2.1 因為畫板涉及到橡皮擦和切換畫筆顏色的功能偷办,所以需要一個 繼承UIBezierPath的子類,添加兩個屬性用于記錄路徑的顏色和是否是橡皮擦狀態(tài)
//畫筆的顏色
@property (nonatomic,copy) UIColor *lineColor;
//是否是橡皮擦
@property (nonatomic,assign) BOOL isErase;`
2.2 畫板的View里實現(xiàn)touchesBegan澄港,touchesMoved椒涯,touchesEnded的代理方法
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
UITouch *touch = [touches anyObject];
CGPoint currentPoint = [touch locationInView:self];
//touchesBegan方法中初始化beziPath moveToPoint
self.beziPath = [[DCBeizierPath alloc] init];
self.beziPath.lineColor = self.lineColor;
self.beziPath.isErase = self.isErase;
[self.beziPath moveToPoint:currentPoint];
// 并將路徑保存到數(shù)組中以保存數(shù)據(jù)
[self.beziPathArrM addObject:self.beziPath];
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
UITouch *touch = [touches anyObject];
CGPoint currentPoint = [touch locationInView:self];
CGPoint previousPoint = [touch previousLocationInView:self];
CGPoint midP = midPoint(previousPoint,currentPoint);
// touchesMoved方法中添加每一個點到self.beziPath中
// 使用二次貝塞爾曲線比使用addLine畫線更圓潤拐點沒有尖角
[self.beziPath addQuadCurveToPoint:currentPoint controlPoint:midP];
// 并主動調(diào)用繪畫方法 <setNeedsDisplay會自動調(diào)用drawrect方法 不要直接調(diào)用drawrect方法>
[self setNeedsDisplay];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
UITouch *touch = [touches anyObject];
CGPoint currentPoint = [touch locationInView:self];
CGPoint previousPoint = [touch previousLocationInView:self];
CGPoint midP = midPoint(previousPoint,currentPoint);
[self.beziPath addQuadCurveToPoint:currentPoint controlPoint:midP];
// touchesMoved
[self setNeedsDisplay];
}
// 計算中間點
CGPoint midPoint(CGPoint p1, CGPoint p2)
{
return CGPointMake((p1.x + p2.x) * 0.5, (p1.y + p2.y) * 0.5);
}
2.3 實現(xiàn)drawrect方法
#pragma mark - 繪畫方法
- (void)drawRect:(CGRect)rect
{
//獲取上下文
if(self.beziPathArrM.count){
for (DCBeizierPath *path in self.beziPathArrM) {
if (path.isErase) {
// 橡皮擦設置無色
[[UIColor clearColor] setStroke];
}else{
// 設置畫筆顏色
[path.lineColor setStroke];
}
path.lineJoinStyle = kCGLineJoinRound;
path.lineCapStyle = kCGLineCapRound;
if (path.isErase) {
path.lineWidth = kEraseLineWidth;
// 設置橡皮擦狀態(tài)的畫線的模式
[path strokeWithBlendMode:kCGBlendModeCopy alpha:1.0];
} else {
path.lineWidth = kLineWidth;
// 設置正常畫線的畫線模式
[path strokeWithBlendMode:kCGBlendModeNormal alpha:1.0];
}
[path stroke];
}
}
[super drawRect:rect];
}
2.4 設置清楚畫板功能
- (void)clear{
[self.beziPathArrM removeAllObjects];
[self setNeedsDisplay];
}
3. 總結(jié)看到這里有人可能會說了 這不很簡單么沒什么難的啊,都是最基礎(chǔ)的東西回梧。下面我來說說我發(fā)現(xiàn)的優(yōu)缺點和這種方法的使用場
優(yōu)點
1. 這種實現(xiàn)方式最簡單 而且也是直接調(diào)用oc的API實現(xiàn)废岂,方法實現(xiàn)較為簡單祖搓。
2.用已知存儲的點 添加路徑 再繪制,速度很快湖苞。缺點
1.如果需要保持每條你畫的線都在拯欧,你需要保存每一條繪畫路徑。
2.每次在繪畫新添加的繪畫線條的時候袒啼,都要把這條線段之前所有的線段在重繪一次哈扮,浪費系統(tǒng)性能。
3.如果你不在乎這點性能的浪費,那么還有問題,當你越畫線段越多的時候 屏幕識別點的距離會越來越大蚓再,并且明顯能感覺到繪畫速度變慢 逐漸能看到之前線段繪畫的軌跡滑肉。
- 應用場景
1.一次性畫一些簡單的線段,并且不做修改的情況下可以使用。
2.UI上需要做一些效果的簡單線段可以使用摘仅。
3.需要頻繁修改和繪畫的情況下靶庙,不建議使用。
二. NSUndoManager+ Quartz2D 畫板方法的實現(xiàn)
根據(jù)上面一種的方法的優(yōu)缺點和明確我們的需求娃属,我們要找的是一種效率更高更底層的方法,并且不需要重繪我們已經(jīng)畫好的線段六荒。所以我們就有了NSUndoManager+ Quartz2D這種組合。
1矾端、NSUndoManager簡單說明
每個人都會犯錯誤掏击。多虧了 Foundation 庫提供了比拼寫錯誤更強大的功能來解救我們。Cocoa 有一套簡單強壯的 NSUndoManager API 管理撤銷和重做秩铆。
默認地砚亭,每個應用的 window 都有一個 undo manager,每一個響應鏈條中的對象都可以管理一個自定義的 undo manager 來管理各自頁面上本地操作的撤銷和重做操作殴玛。UITextField 和 UITextView 用這個功能自動提供了文本編輯的撤銷重做支持捅膘。然而,標明哪些動作可以被撤銷是留給應用開發(fā)工程師的工作滚粟。
創(chuàng)建一個可以撤銷的動作需要三步:做出改變寻仗,注冊一個可以逆向的 "撤銷操作",響應撤銷改變的動作凡壤。
詳細參照 :http://nshipster.cn/nsundomanager/
2署尤、 Quartz2D簡單說明
在畫線的時候,方法的內(nèi)部默認創(chuàng)建一個path亚侠。它把路徑都放到了path里面去曹体。
>1.創(chuàng)建路徑 CGMutablePathRef 調(diào)用該方法相當于創(chuàng)建了一個路徑,這個路徑用來保存繪圖信息盖奈。
> 2.把繪圖信息添加到路徑里邊混坞。 以前的方法是點的位置添加到ctx(圖形上下文信息)中狐援,ctx 默認會在內(nèi) 部創(chuàng)建一個path用來保存繪圖信息钢坦。在圖形上下文中有一塊存儲空間專門用來存儲繪圖信息,其實這塊空間就是CGMutablePathRef爹凹。
>3.把路徑添加到上下文中禾酱。
3微酬、結(jié)合這兩個方法既可以做到提高繪畫效率又可以實時保存避免重繪已經(jīng)畫好的線段
4、具體實現(xiàn)細節(jié)
- (void)awakeFromNib{
[self setup];
}
// 畫筆的初始化設置
-(void)setup
{
self.multipleTouchEnabled = YES;
// 設置初始筆寬
self.lineWidth = 5;
// 設置初始畫筆顏色
self.lineColor =[UIColor blackColor];
//初始化NSUndoManager
NSUndoManager *tempUndoManager = [[NSUndoManager alloc] init];
[tempUndoManager setLevelsOfUndo:10];
[self setUndoManager:tempUndoManager];
}
// 清楚畫面和存儲的數(shù)據(jù)
-(void)clear
{
// [self setImage:nil];
self.previousPoint1=CGPointMake(0, 0);
self.previousPoint2=CGPointMake(0, 0);
self.currentPoint = CGPointMake(0, 0);
[self setNeedsDisplay];
}
// 計算中間點
CGPoint midPoint1(CGPoint p1, CGPoint p2)
{
return CGPointMake((p1.x + p2.x) * 0.5, (p1.y + p2.y) * 0.5);
}
pragma mark -touchesBegan方法
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint currentPoint = [touch locationInView:self];
self.previousPoint1 = [touch locationInView:self];
self.previousPoint2 = [touch locationInView:self];
self.currentPoint = [touch locationInView:self];
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, self.previousPoint1.x, self.previousPoint1.y);
[self.pointsArrM removeAllObjects];
// 添加點集合
NSDictionary *dict = @{@"x":@(currentPoint.x),@"y":@(currentPoint.y)};
[self.pointsArrM addObject:dict];
}
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint currentPoint = [touch locationInView:self];
self.previousPoint2 = self.previousPoint1;
self.previousPoint1 = [touch previousLocationInView:self];
self.currentPoint = [touch locationInView:self];
CGPoint mid1 = midPoint1(self.previousPoint1, self.previousPoint2);
CGPoint mid2 = midPoint1(self.currentPoint, self.previousPoint1);
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, mid1.x, mid1.y);
CGPathAddQuadCurveToPoint(path, NULL, self.previousPoint1.x, self.previousPoint1.y, mid2.x, mid2.y);
CGRect bounds = CGPathGetBoundingBox(path);
CGPathRelease(path);
CGRect drawBox = bounds;
//Pad our values so the bounding box respects our line width
drawBox.origin.x -= self.lineWidth * 2;
drawBox.origin.y -= self.lineWidth * 2;
drawBox.size.width += self.lineWidth * 4;
drawBox.size.height += self.lineWidth * 4;
UIGraphicsBeginImageContext(drawBox.size);
[self.layer renderInContext:UIGraphicsGetCurrentContext()];
self.curImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
[self setNeedsDisplayInRect:drawBox];
//
NSDictionary *dict = @{@"x":@(currentPoint.x),@"y":@(currentPoint.y)};
[self.pointsArrM addObject:dict];
}
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
// if([touches count] >= 2)return;
UITouch *touch = [touches anyObject];
CGPoint currentPoint = [touch locationInView:self];
NSDictionary *dict = @{@"x":@(currentPoint.x),@"y":@(currentPoint.y)};
self.previousPoint2 = self.previousPoint1;
self.previousPoint1 = [touch previousLocationInView:self];
self.currentPoint = [touch locationInView:self];
CGPoint mid1 = midPoint1(self.previousPoint1, self.previousPoint2);
CGPoint mid2 = midPoint1(self.currentPoint, self.previousPoint1);
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, mid1.x, mid1.y);
CGPathAddQuadCurveToPoint(path, NULL, self.previousPoint1.x, self.previousPoint1.y, mid2.x, mid2.y);
//繪畫
CGRect bounds = CGPathGetBoundingBox(path);
CGPathRelease(path);
CGRect drawBox = bounds;
//Pad our values so the bounding box respects our line width
drawBox.origin.x -= self.lineWidth * 2;
drawBox.origin.y -= self.lineWidth * 2;
drawBox.size.width += self.lineWidth * 4;
drawBox.size.height += self.lineWidth * 4;
UIGraphicsBeginImageContext(drawBox.size);
[self.layer renderInContext:UIGraphicsGetCurrentContext()];
self.curImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
[self setNeedsDisplayInRect:drawBox];
[self.pointsArrM addObject:dict];
}
pragma mark - 繪畫方法
- (void)drawRect:(CGRect)rect
{
//獲取上下文
[self.curImage drawAtPoint:CGPointMake(0, 0)];
CGPoint mid1 = midPoint1(self.previousPoint1, self.previousPoint2);
CGPoint mid2 = midPoint1(self.currentPoint, self.previousPoint1);
self.context = UIGraphicsGetCurrentContext();
[self.layer renderInContext:self.context];
CGContextMoveToPoint(self.context, mid1.x, mid1.y);
// 添加畫點
CGContextAddQuadCurveToPoint(self.context, self.previousPoint1.x, self.previousPoint1.y, mid2.x, mid2.y);
// 設置圓角
CGContextSetLineCap(self.context, kCGLineCapRound);
// 設置線寬
CGContextSetLineWidth(self.context, self.isErase? kEraseLineWidth:kLineWidth);
// 設置畫筆顏色
CGContextSetStrokeColorWithColor(self.context, self.isErase?[UIColor clearColor].CGColor:self.lineColor.CGColor);
CGContextSetLineJoin(self.context, kCGLineJoinRound);
// 根據(jù)是否是橡皮擦設置設置畫筆模式
CGContextSetBlendMode(self.context, self.isErase ? kCGBlendModeDestinationIn:kCGBlendModeNormal);
CGContextStrokePath(self.context);
[super drawRect:rect];
}
5颗管、總結(jié)及優(yōu)缺點
優(yōu)點
1. 繪畫方法較為底層實現(xiàn)效率更高更快滓走。
2.每次繪畫都很流暢 不會有延遲感 不會重繪已經(jīng)畫好的繪畫路徑垦江。
3.線段更加圓潤缺點
1.如果有已知點集合,重繪所有點路徑 會消耗很長時間才能畫完搅方。
2.如果App消耗性能過多的話<我們的App起著一個視頻會話,
一個通信會話還有一些很多控件的交互>衩藤,在Pad3上繪畫 會有斷點
<Pad2,mini2 3涛漂,Air都沒有這個問題<iPhone還沒測試過4s和5>>,
原因可能在于:Pad3是Retina屏幕 分辨率增長一倍
但是Pad3的CPU GPU比Pad2卻只增長了50%左右底哗,
導致不能連續(xù)識別到屏幕的觸點锚沸,從而導致出現(xiàn)斷點哗蜈。使用場景
1.App不太消耗性能的情況下且不需要重繪的情況下可以使用。
2.只在一個頁面繪畫且繪畫后不需要重繪的炼列。
3.不care這點時間消耗的音比。
三、OpenGLES畫板的實現(xiàn)
雖然上面各有各的優(yōu)點,但是一項項無法跨過的坑悲催著我再找另一種解決方案,更底層的openGLES,本人之前也沒有用過,了解的也不是太多,基本知識大多也是從別人的博客上扒下來的.能應用到實際場景也要歸功于apple的官方demo
地址:http://www.reibang.com/p/7d4710b815c2/ 里面有一個demo是說如何通過openGLES來實現(xiàn)畫板功能的一個demo稽犁。我也是依葫蘆畫瓢套了一下已亥。下面說下具體的實現(xiàn)細節(jié):
-
準備工作
1.需要添加OpenGLES.framework系統(tǒng)庫熊赖。并導入頭文件
#import <OpenGLES/EAGL.h>
#import <OpenGLES/ES2/gl.h>
#import <OpenGLES/ES2/glext.h>
#import <GLKit/GLKit.h>2.導入配置文件 具體作用就不詳述了。
#import "shaderUtil.h"
#import "fileUtil.h"
#import "debug.h"
屏幕快照 2016-05-20 14.37.58.png3.這個半透明的圖片很重要 相當于筆觸 通過他的透明度來控制渲染筆畫顏色的深淺
屏幕快照 2016-05-20 14.41.41.png -
初始化設置
1.OpenGLES的初始化設置// 創(chuàng)建一個紋理的圖像 - (textureInfo_t)textureFromName:(NSString *)name { CGImageRef brushImage; CGContextRef brushContext; GLubyte *brushData; size_t width, height; GLuint texId; textureInfo_t texture; // First create a UIImage object from the data in a image file, and then extract the Core Graphics image brushImage = [UIImage imageNamed:name].CGImage; // Get the width and height of the image width = CGImageGetWidth(brushImage); height = CGImageGetHeight(brushImage); // Make sure the image exists if(brushImage) { // Allocate memory needed for the bitmap context brushData = (GLubyte *) calloc(width * height * 4, sizeof(GLubyte)); // Use the bitmatp creation function provided by the Core Graphics framework. brushContext = CGBitmapContextCreate(brushData, width, height, 8, width * 4, CGImageGetColorSpace(brushImage), kCGImageAlphaPremultipliedLast); // After you create the context, you can draw the image to the context. CGContextDrawImage(brushContext, CGRectMake(0.0, 0.0, (CGFloat)width, (CGFloat)height), brushImage); // You don't need the context at this point, so you need to release it to avoid memory leaks. CGContextRelease(brushContext); // Use OpenGL ES to generate a name for the texture. // //創(chuàng)建渲染緩沖管線 glGenTextures(1, &texId); // Bind the texture name. //綁定渲染緩沖管線 glBindTexture(GL_TEXTURE_2D, texId); // Set the texture parameters to use a minifying filter and a linear filer (weighted average) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); // Specify a 2D texture image, providing the a pointer to the image data in memory glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (int)width, (int)height, 0, GL_RGBA, GL_UNSIGNED_BYTE, brushData); // Release the image data; it's no longer needed free(brushData); texture.id = texId; texture.width = (int)width; texture.height = (int)height; } return texture; } // 初始化GL 并設置筆觸 - (BOOL)initGL { // Generate IDs for a framebuffer object and a color renderbuffer ////創(chuàng)建幀緩沖管線 glGenFramebuffers(1, &viewFramebuffer); //綁定渲染緩沖管線 glGenRenderbuffers(1, &viewRenderbuffer); //綁定幀緩沖管線 glBindFramebuffer(GL_FRAMEBUFFER, viewFramebuffer); //將渲染緩沖區(qū)附加到幀緩沖區(qū)上 glBindRenderbuffer(GL_RENDERBUFFER, viewRenderbuffer); // This call associates the storage for the current render buffer with the EAGLDrawable (our CAEAGLLayer) // allowing us to draw into a buffer that will later be rendered to screen wherever the layer is (which corresponds with our view). [context renderbufferStorage:GL_RENDERBUFFER fromDrawable:(id<EAGLDrawable>)self.layer]; glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, viewRenderbuffer); glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &backingWidth); glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &backingHeight); // For this sample, we do not need a depth buffer. If you do, this is how you can create one and attach it to the framebuffer: // glGenRenderbuffers(1, &depthRenderbuffer); // glBindRenderbuffer(GL_RENDERBUFFER, depthRenderbuffer); // glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, backingWidth, backingHeight); // glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthRenderbuffer); if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { NSLog(@"failed to make complete framebuffer object %x", glCheckFramebufferStatus(GL_FRAMEBUFFER)); return NO; } //創(chuàng)建顯示區(qū)域 glViewport(0, 0, backingWidth, backingHeight); // Create a Vertex Buffer Object to hold our data glGenBuffers(1, &vboId); // Load the brush texture // 設置筆頭 brushTexture = [self textureFromName:@"Particle"]; // Load shaders [self setupShaders]; // Enable blending and set a blending function appropriate for premultiplied alpha pixel data glEnable(GL_BLEND); glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); return YES; }
- (BOOL)resizeFromLayer:(CAEAGLLayer *)layer
{
// Allocate color buffer backing based on the current layer size
glBindRenderbuffer(GL_RENDERBUFFER, viewRenderbuffer);
[context renderbufferStorage:GL_RENDERBUFFER fromDrawable:layer];
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &backingWidth);
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &backingHeight);
// For this sample, we do not need a depth buffer. If you do, this is how you can allocate depth buffer backing:
// glBindRenderbuffer(GL_RENDERBUFFER, depthRenderbuffer);
// glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, backingWidth, backingHeight);
// glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthRenderbuffer);if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
{
NSLog(@"Failed to make complete framebuffer objectz %x", glCheckFramebufferStatus(GL_FRAMEBUFFER));
return NO;
}// Update projection matrix
GLKMatrix4 projectionMatrix = GLKMatrix4MakeOrtho(0, backingWidth, 0, backingHeight, -1, 1);
GLKMatrix4 modelViewMatrix = GLKMatrix4Identity; // this sample uses a constant identity modelView matrix
GLKMatrix4 MVPMatrix = GLKMatrix4Multiply(projectionMatrix, modelViewMatrix);glUseProgram(program[PROGRAM_POINT].id);
glUniformMatrix4fv(program[PROGRAM_POINT].uniform[UNIFORM_MVP], 1, GL_FALSE, MVPMatrix.m);// Update viewport
glViewport(0, 0, backingWidth, backingHeight);return YES;
}-
(void)setupShaders
{
for (int i = 0; i < NUM_PROGRAMS; i++)
{
char *vsrc = readFile(pathForResource(program[i].vert));
char *fsrc = readFile(pathForResource(program[i].frag));
GLsizei attribCt = 0;
GLchar *attribUsed[NUM_ATTRIBS];
GLint attrib[NUM_ATTRIBS];
GLchar *attribName[NUM_ATTRIBS] = {
"inVertex",
};
const GLchar *uniformName[NUM_UNIFORMS] = {
"MVP", "pointSize", "vertexColor", "texture",
};// auto-assign known attribs
for (int j = 0; j < NUM_ATTRIBS; j++)
{
if (strstr(vsrc, attribName[j]))
{
attrib[attribCt] = j;
attribUsed[attribCt++] = attribName[j];
}
}glueCreateProgram(vsrc, fsrc,
attribCt, (const GLchar **)&attribUsed[0], attrib,
NUM_UNIFORMS, &uniformName[0], program[i].uniform,
&program[i].id);
free(vsrc);
free(fsrc);// Set constant/initalize uniforms
if (i == PROGRAM_POINT)
{
glUseProgram(program[PROGRAM_POINT].id);// the brush texture will be bound to texture unit 0 glUniform1i(program[PROGRAM_POINT].uniform[UNIFORM_TEXTURE], 0); // viewing matrices GLKMatrix4 projectionMatrix = GLKMatrix4MakeOrtho(0, backingWidth, 0, backingHeight, -1, 1); GLKMatrix4 modelViewMatrix = GLKMatrix4Identity; // this sample uses a constant identity modelView matrix GLKMatrix4 MVPMatrix = GLKMatrix4Multiply(projectionMatrix, modelViewMatrix); glUniformMatrix4fv(program[PROGRAM_POINT].uniform[UNIFORM_MVP], 1, GL_FALSE, MVPMatrix.m); // point size glUniform1f(program[PROGRAM_POINT].uniform[UNIFORM_POINT_SIZE], brushTexture.width / kBrushScale); // initialize brush color glUniform4fv(program[PROGRAM_POINT].uniform[UNIFORM_VERTEX_COLOR], 1, brushColor);
}
}
glError();
}
- (BOOL)resizeFromLayer:(CAEAGLLayer *)layer
4.繪畫方法
// 根據(jù)兩點畫線的方法
- (void)renderLineFromPoint:(CGPoint)start toPoint:(CGPoint)end
{
// NSLog(@"drawLineWithPoints--%@--%@",NSStringFromCGPoint(start),NSStringFromCGPoint(end));
static GLfloat* vertexBuffer = NULL;
static NSUInteger vertexMax = 64;
NSUInteger vertexCount = 0,
count,
i;
[EAGLContext setCurrentContext:context];
glBindFramebuffer(GL_FRAMEBUFFER, viewFramebuffer);
// Convert locations from Points to Pixels
CGFloat scale = self.contentScaleFactor;
start.x *= scale;
start.y *= scale;
end.x *= scale;
end.y *= scale;
// Allocate vertex array buffer
if(vertexBuffer == NULL)
vertexBuffer = malloc(vertexMax * 2 * sizeof(GLfloat));
// Add points to the buffer so there are drawing points every X pixels
count = MAX(ceilf(sqrtf((end.x - start.x) * (end.x - start.x) + (end.y - start.y) * (end.y - start.y)) / kBrushPixelStep), 1);
for(i = 0; i < count; ++i) {
if(vertexCount == vertexMax) {
vertexMax = 2 * vertexMax;
vertexBuffer = realloc(vertexBuffer, vertexMax * 2 * sizeof(GLfloat));
}
vertexBuffer[2 * vertexCount + 0] = start.x + (end.x - start.x) * ((GLfloat)i / (GLfloat)count);
vertexBuffer[2 * vertexCount + 1] = start.y + (end.y - start.y) * ((GLfloat)i / (GLfloat)count);
vertexCount += 1;
}
// Load data to the Vertex Buffer Object
glBindBuffer(GL_ARRAY_BUFFER, vboId);
glBufferData(GL_ARRAY_BUFFER, vertexCount*2*sizeof(GLfloat), vertexBuffer, GL_STATIC_DRAW);
glEnableVertexAttribArray(ATTRIB_VERTEX);
glVertexAttribPointer(ATTRIB_VERTEX, 2, GL_FLOAT, GL_FALSE, 0, 0);
// Draw
glUseProgram(program[PROGRAM_POINT].id);
// 畫線
glDrawArrays(GL_POINTS, 0, (int)vertexCount);
// Display the buffer
glBindRenderbuffer(GL_RENDERBUFFER, viewRenderbuffer);
[context presentRenderbuffer:GL_RENDERBUFFER];
}
5. 清楚已繪畫畫板
// 清楚
- (void)clearDrawImageView
{
[EAGLContext setCurrentContext:context];
// Clear the buffer
glBindFramebuffer(GL_FRAMEBUFFER, viewFramebuffer);
glClearColor(0.0, 0.0, 0.0, 0.0);
glClear(GL_COLOR_BUFFER_BIT);
// Display the buffer
glBindRenderbuffer(GL_RENDERBUFFER, viewRenderbuffer);
[context presentRenderbuffer:GL_RENDERBUFFER];
}
6.設置畫筆顏色
- (void)setBrushColorWithRed:(CGFloat)red green:(CGFloat)green blue:(CGFloat)blue alpha:(CGFloat)alpha
{
// Update the brush color
brushColor[0] = red ;
brushColor[1] = green ;
brushColor[2] = blue ;
brushColor[3] = alpha;
if (initialized) {
glUseProgram(program[PROGRAM_POINT].id);
// 設置畫筆顏色
glUniform4fv(program[PROGRAM_POINT].uniform[UNIFORM_VERTEX_COLOR], 1, brushColor);
}
}
7. 釋放內(nèi)存
- (void)dealloc
{
// Destroy framebuffers and renderbuffers
if (viewFramebuffer) {
glDeleteFramebuffers(1, &viewFramebuffer);
viewFramebuffer = 0;
}
if (viewRenderbuffer) {
glDeleteRenderbuffers(1, &viewRenderbuffer);
viewRenderbuffer = 0;
}
if (depthRenderbuffer)
{
glDeleteRenderbuffers(1, &depthRenderbuffer);
depthRenderbuffer = 0;
}
// texture
if (brushTexture.id) {
glDeleteTextures(1, &brushTexture.id);
brushTexture.id = 0;
}
// vbo
if (vboId) {
glDeleteBuffers(1, &vboId);
vboId = 0;
}
// tear down context
if ([EAGLContext currentContext] == context)
[EAGLContext setCurrentContext:nil];
}
8. 通過UIView的touch代理方法獲得觸點
// touchesBegan 方法 設置起點
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// CGRect bounds = [self bounds];
UITouch* touch = [[event touchesForView:self] anyObject];
// firstTouch = YES;
// 轉(zhuǎn)換觸點從UIView引用到OpenGL 1(倒翻轉(zhuǎn))
// Convert touch point from UIView referential to OpenGL one (upside-down flip)
_previousLocation = [touch locationInView:self];
_previousLocation.y = self.height - _previousLocation.y;
CGPoint currentPoint = [touch locationInView:self];
NSDictionary *dict = @{@"x":@(currentPoint.x),@"y":@(currentPoint.y)};
[self.pointsArrM addObject:dict];
// NSLog(@"touchesBegan--%@",dict);
}
// touch moved方法 開始繪畫
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// CGRect bounds = [self bounds];
UITouch* touch = [[event touchesForView:self] anyObject];
_location = [touch locationInView:self];
_location.y = self.height - _location.y;
_previousLocation = [touch previousLocationInView:self];
_previousLocation.y = self.height - _previousLocation.y;
[self renderLineFromPoint:_previousLocation toPoint:_location];
CGPoint currentPoint = [touch locationInView:self];
NSDictionary *dict = @{@"x":@(currentPoint.x),@"y":@(currentPoint.y)};
[self.pointsArrM addObject:dict];
// NSLog(@"touchesMoved--%@",dict);
}
// touchesEnded方法 設置終點
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
UITouch* touch = [[event touchesForView:self] anyObject];
_location = [touch locationInView:self];
_location.y = self.height - _location.y;
_previousLocation = [touch previousLocationInView:self];
_previousLocation.y = self.height - _previousLocation.y;
[self renderLineFromPoint:_previousLocation toPoint:_location];
_location = CGPointMake(0, 0);
_previousLocation = CGPointMake(0, 0);
CGPoint currentPoint = [touch locationInView:self];
NSDictionary *dict = @{@"x":@(currentPoint.x),@"y":@(currentPoint.y)};
[self.pointsArrM addObject:dict];
// NSLog(@"touchesEnded--%@",dict);
}
//touchesCancelled 方法 設置終點 這里需要解釋一下 墨缘,因為我們的畫板是一個scrollView,
// 需要一個兩指拖動畫板位置的功能,而兩指觸到畫板抬起的時候镊讼,
//有時候會造成不進入touchesEnded 進入touchesCancelled方法
//所以在這里和touchesEnded的處理方式是一樣的
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
UITouch* touch = [[event touchesForView:self] anyObject];
_location = [touch locationInView:self];
_location.y = self.height - _location.y;
_previousLocation = [touch previousLocationInView:self];
_previousLocation.y = self.height - _previousLocation.y;
[self renderLineFromPoint:_previousLocation toPoint:_location];
_location = CGPointMake(0, 0);
_previousLocation = CGPointMake(0, 0);
CGPoint currentPoint = [touch locationInView:self];
NSDictionary *dict = @{@"x":@(currentPoint.x),@"y":@(currentPoint.y)};
[self.pointsArrM addObject:dict];
}
2. 需要重寫View的layerClass方法,否則無法渲染到當前l(fā)ayer層,但是覆蓋之后也不能通過原生的方法繪畫,這是一個注意點平夜。
+ (Class)layerClass
{
return [CAEAGLLayer class];
}
//設置畫筆顏色
- (void)setLineColor:(UIColor *)lineColor{
_lineColor = lineColor;
if (lineColor == [UIColor blackColor]) {
[self setBrushColorWithRed:0 green:0 blue:0 alpha:1];
}
else if (lineColor == [UIColor redColor]) {
[self setBrushColorWithRed:1 green:0 blue:0 alpha:1];
}
else if (lineColor == [UIColor greenColor]) {
[self setBrushColorWithRed:0 green:1 blue:0 alpha:1];
}
else if (lineColor == [UIColor greenColor])
{
[self setBrushColorWithRed:0 green:0 blue:1 alpha:1];
}else {
[self setBrushColorWithRed:0 green:0 blue:0 alpha:1];
}
}
// 設置是否是橡皮擦
- (void)setIsErase:(BOOL)isErase
{
_isErase = isErase;
if (isErase) {
[self setBrushColorWithRed:0 green:0 blue:0 alpha:0];
// 設置繪畫模式
glBlendFunc(GL_ONE, GL_ZERO);
}else{
[self setBrushColorWithRed:1 green:0 blue:0 alpha:1];
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
}
}
如果是通過storyboard添加的畫板需要在-initWithCoder:(NSCoder*)coder中完成初始化設置
- (id)initWithCoder:(NSCoder*)coder {
if ((self = [super initWithCoder:coder])) {
// 2忽妒、在init的方法中段直,從基類獲取layer屬性,并將其轉(zhuǎn)型至CAEAGLLayer
CAEAGLLayer *eaglLayer = (CAEAGLLayer *)super.layer;
eaglLayer.opaque = YES;//無需Quartz處理透明度
// In this application, we want to retain the EAGLDrawable contents after a call to presentRenderbuffer.
eaglLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:YES], kEAGLDrawablePropertyRetainedBacking, kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat, nil];
/*
后面的參數(shù)有兩個選擇
kEAGLRenderingAPIOpenGLES1=1 表示用渲染庫的API版本是1.1
kEAGLRenderingAPIOpenGLES2=2 表示用渲染庫的API版本是2.0
*/
context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
// //設定當期上下文對象
if (!context || ![EAGLContext setCurrentContext:context]) {
return nil;
}
// Set the view's scale factor as you wish
self.contentScaleFactor = [[UIScreen mainScreen] scale];
// Make sure to start with a cleared buffer
needsErase = YES;
}
return self;
}
3.//如果我們的視圖的大小,我們將要求布局子視圖决侈。
//這是一個完美的機會來更新framebuffer這樣
//同樣大小的顯示區(qū)域喧务。
-(void)layoutSubviews
{
[EAGLContext setCurrentContext:context];
if (!initialized) {
initialized = [self initGL];
}
else {
[self resizeFromLayer:(CAEAGLLayer*)self.layer];
}
// Clear the framebuffer the first time it is allocated
if (needsErase) {
[self clearDrawImageView];
needsErase = NO;
}
}
總結(jié)
優(yōu)點
1. 很底層,繪畫速度更快,直接通過硬件的渲染,解決了上一個在iPad3硬件下繪畫會有斷點的bug功茴。
2.性能更好。缺點
1.暫時我還沒找到 畫弧線的方法展父。
2.更底層,API可讀性太差 沒有注釋 根本看不懂 有注釋的也沒看懂幾個。
3.通過已知點,重繪的速度也慢,好在于相對于上一種方法的慢他是可以看到繪畫軌跡的旭等,可能適用于一些特殊的需求衡载。
個人集成后遇到的坑
1.橡皮擦和畫筆狀態(tài)切換的時候回造成狀態(tài)失效,原因不詳...解決方案:每次touchBegain的時候都再次設置一次隙袁。
2.橡皮擦狀態(tài)下,擦除畫筆的時候會有一個小圓點一直跟隨筆跡,原因不詳...解決方案同上。應用場景
1.一次性畫一些簡單的線段,并且不做修改的情況下可以使用菩收。
2.UI上需要做一些效果的簡單線段可以使用。
3.需要頻繁修改和繪畫的情況下娜饵,不建議使用箱舞。
四、Demo地址
先奉上demo:https://github.com/ddc391565320/DCDrawView 如有不當之處歡迎指正