我正试图为iOS写一个简单的绘画应用程序作为第一个非琐碎的项目。基本上,在每个触摸事件上,我需要在位图上打开一个图形上下文,在用户离开的地方画一些东西,然后关闭它。
UIImage
是不可变的,所以它不完全适合我的目的;我必须建立一个新的位图,并把旧的画到新的位图中。我想象不出有什么好表现。UIKit中有可变位图类吗,还是我要找到CGImageRef
?
如果您愿意冒险远离cocoa,我强烈建议您为此使用OpenGL。苹果提供了一个很好的示例应用(GLPaint)来证明这一点。解决OpenGL的学习曲线肯定会在外观、性能和纯粹的能力方面得到回报。灵活性。
然而,如果你不那么做,那么另一种方法是创建一个新的CALayer
子类覆盖drawInContext:
,并存储每个绘图笔画(路径和线条属性)那里。然后你可以将每个'strokeLayer'添加到绘图视图的图层层次结构中,并强制每帧重画一次。cglayer也可以用来提高性能(这可能会成为一个大问题——当用户画一个长笔画时,你会看到帧率下降得非常快)。事实上,在任何情况下,你都可能最终使用CGLayer来绘制。下面是drawRect:
方法的一小段代码,它可能有助于说明这种方法:
- (void)drawRect:(CGRect)rect {
// Setup the layer and it's context to use as a drawing buffer.
CGContextRef context = UIGraphicsGetCurrentContext();
CGLayerRef drawingBuffer = CGLayerCreateWithContext(context, self.bounds.size, NULL);
CGContextRef bufferContext = CGLayerGetContext(drawingBuffer);
// Draw all sublayers into the drawing buffer, and display the buffer.
[self.layer renderInContext:bufferContext];
CGContextDrawLayerAtPoint(context, CGPointZero, drawingBuffer);
CGLayerRelease(drawingBuffer);
}
就可变性而言,最明显的做法是在绘画笔触上绘制背景色。这样,橡皮擦的笔触将与绘画笔触完全相同,只是颜色不同。
你提到使用位图图像,这确实开始暗示OpenGL渲染到纹理,其中一系列点精灵(形成一条线)可以以非常高的帧率绘制到可变纹理上。我不想给你泼冷水,但使用Core Graphics/Quartz以这种方式作画,你将不可避免地遇到性能瓶颈。
您不需要在每次进行新笔画时重新创建屏幕外上下文。你可能会在某个地方积累笔画(NSMutableArray)当达到某个限制时,你会通过首先将背景绘制到屏幕外上下文然后将你积累的笔画放在它上面来平化那些积累的笔画。生成的屏幕外绘图将成为新的背景,因此您可以清空包含笔画的数组并重新开始。这样你就采取了一种混合的方法,将所有的笔画存储在内存中+每次重新绘制它们,并不断地重新创建屏幕外的位图。
这本书http://www.deitel.com/Books/iPhone/iPhoneforProgrammersAnAppDrivenApproach/tabid/3526/Default.aspx中有一整章(7)专门介绍创建一个简单的绘画应用程序。在那里您可以找到代码示例的链接。所采用的方法是将笔画存储在内存中,但是这里有一些MainView.h和.m文件的修改版本,它们采用了我所描述的方法,!!但是请注意两个文件底部的版权说明!!:
// MainView.m
// View for the frontside of the Painter app.
#import "MainView.h"
const NSUInteger kThreshold = 2;
@implementation MainView
@synthesize color; // generate getters and setters for color
@synthesize lineWidth; // generate getters and setters for lineWidth
CGContextRef CreateBitmapContext(NSUInteger w, NSUInteger h);
void * globalBitmapData = NULL;
// method is called when the view is created in a nib file
- (id)initWithCoder:(NSCoder*)decoder
{
// if the superclass initializes properly
if (self = [super initWithCoder:decoder])
{
// initialize squiggles and finishedSquiggles
squiggles = [[NSMutableDictionary alloc] init];
finishedSquiggles = [[NSMutableArray alloc] init];
// the starting color is black
color = [[UIColor alloc] initWithRed:0 green:0 blue:0 alpha:1];
lineWidth = 5; // default line width
flattenedImage_ = NULL;
} // end if
return self; // return this objeoct
} // end method initWithCoder:
// clears all the drawings
- (void)resetView
{
[squiggles removeAllObjects]; // clear the dictionary of squiggles
[finishedSquiggles removeAllObjects]; // clear the array of squiggles
[self setNeedsDisplay]; // refresh the display
} // end method resetView
// draw the view
- (void)drawRect:(CGRect)rect
{
// get the current graphics context
CGContextRef context = UIGraphicsGetCurrentContext();
if(flattenedImage_)
{
CGContextDrawImage(context, CGRectMake(0,0,CGRectGetWidth(self.bounds), CGRectGetHeight(self.bounds)), flattenedImage_);
}
// draw all the finished squiggles
for (Squiggle *squiggle in finishedSquiggles)
[self drawSquiggle:squiggle inContext:context];
// draw all the squiggles currently in progress
for (NSString *key in squiggles)
{
Squiggle *squiggle = [squiggles valueForKey:key]; // get squiggle
[self drawSquiggle:squiggle inContext:context]; // draw squiggle
} // end for
} // end method drawRect:
// draws the given squiggle into the given context
- (void)drawSquiggle:(Squiggle *)squiggle inContext:(CGContextRef)context
{
// set the drawing color to the squiggle's color
UIColor *squiggleColor = squiggle.strokeColor; // get squiggle's color
CGColorRef colorRef = [squiggleColor CGColor]; // get the CGColor
CGContextSetStrokeColorWithColor(context, colorRef);
// set the line width to the squiggle's line width
CGContextSetLineWidth(context, squiggle.lineWidth);
NSMutableArray *points = [squiggle points]; // get points from squiggle
// retrieve the NSValue object and store the value in firstPoint
CGPoint firstPoint; // declare a CGPoint
[[points objectAtIndex:0] getValue:&firstPoint];
// move to the point
CGContextMoveToPoint(context, firstPoint.x, firstPoint.y);
// draw a line from each point to the next in order
for (int i = 1; i < [points count]; i++)
{
NSValue *value = [points objectAtIndex:i]; // get the next value
CGPoint point; // declare a new point
[value getValue:&point]; // store the value in point
// draw a line to the new point
CGContextAddLineToPoint(context, point.x, point.y);
} // end for
CGContextStrokePath(context);
} // end method drawSquiggle:inContext:
// called whenever the user places a finger on the screen
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
NSArray *array = [touches allObjects]; // get all the new touches
// loop through each new touch
for (UITouch *touch in array)
{
// create and configure a new squiggle
Squiggle *squiggle = [[Squiggle alloc] init];
[squiggle setStrokeColor:color]; // set squiggle's stroke color
[squiggle setLineWidth:lineWidth]; // set squiggle's line width
// add the location of the first touch to the squiggle
[squiggle addPoint:[touch locationInView:self]];
// the key for each touch is the value of the pointer
NSValue *touchValue = [NSValue valueWithPointer:touch];
NSString *key = [NSString stringWithFormat:@"%@", touchValue];
// add the new touch to the dictionary under a unique key
[squiggles setValue:squiggle forKey:key];
[squiggle release]; // we are done with squiggle so release it
} // end for
} // end method touchesBegan:withEvent:
// called whenever the user drags a finger on the screen
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
NSArray *array = [touches allObjects]; // get all the moved touches
// loop through all the touches
for (UITouch *touch in array)
{
// get the unique key for this touch
NSValue *touchValue = [NSValue valueWithPointer:touch];
// fetch the squiggle this touch should be added to using the key
Squiggle *squiggle = [squiggles valueForKey:
[NSString stringWithFormat:@"%@", touchValue]];
// get the current and previous touch locations
CGPoint current = [touch locationInView:self];
CGPoint previous = [touch previousLocationInView:self];
[squiggle addPoint:current]; // add the new point to the squiggle
// Create two points: one with the smaller x and y values and one
// with the larger. This is used to determine exactly where on the
// screen needs to be redrawn.
CGPoint lower, higher;
lower.x = (previous.x > current.x ? current.x : previous.x);
lower.y = (previous.y > current.y ? current.y : previous.y);
higher.x = (previous.x < current.x ? current.x : previous.x);
higher.y = (previous.y < current.y ? current.y : previous.y);
// redraw the screen in the required region
[self setNeedsDisplayInRect:CGRectMake(lower.x-lineWidth,
lower.y-lineWidth, higher.x - lower.x + lineWidth*2,
higher.y - lower.y + lineWidth * 2)];
} // end for
} // end method touchesMoved:withEvent:
// called when the user lifts a finger from the screen
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
// loop through the touches
for (UITouch *touch in touches)
{
// get the unique key for the touch
NSValue *touchValue = [NSValue valueWithPointer:touch];
NSString *key = [NSString stringWithFormat:@"%@", touchValue];
// retrieve the squiggle for this touch using the key
Squiggle *squiggle = [squiggles valueForKey:key];
// remove the squiggle from the dictionary and place it in an array
// of finished squiggles
[finishedSquiggles addObject:squiggle]; // add to finishedSquiggles
[squiggles removeObjectForKey:key]; // remove from squiggles
if([finishedSquiggles count] > kThreshold)
{
CGContextRef context = CreateBitmapContext(CGRectGetWidth(self.bounds), CGRectGetHeight(self.bounds));
if(flattenedImage_)
{
CGContextDrawImage(context, CGRectMake(0,0,CGRectGetWidth(self.bounds), CGRectGetHeight(self.bounds)), flattenedImage_);
}
for (Squiggle *squiggle in finishedSquiggles)
[self drawSquiggle:squiggle inContext:context];
CGImageRef imgRef = CGBitmapContextCreateImage(context);
CGContextRelease(context);
if(flattenedImage_ != NULL)
CFRelease(flattenedImage_);
flattenedImage_ = imgRef;
[finishedSquiggles removeAllObjects];
}
} // end for
} // end method touchesEnded:withEvent:
// called when a motion event, such as a shake, ends
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
{
// if a shake event ended
if (event.subtype == UIEventSubtypeMotionShake)
{
// create an alert prompting the user about clearing the painting
NSString *message = @"Are you sure you want to clear the painting?";
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:
@"Clear painting" message:message delegate:self
cancelButtonTitle:@"Cancel" otherButtonTitles:@"Clear", nil];
[alert show]; // show the alert
[alert release]; // release the alert UIAlertView
} // end if
// call the superclass's moetionEnded:withEvent: method
[super motionEnded:motion withEvent:event];
} // end method motionEnded:withEvent:
// clear the painting if the user touched the "Clear" button
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:
(NSInteger)buttonIndex
{
// if the user touched the Clear button
if (buttonIndex == 1)
[self resetView]; // clear the screen
} // end method alertView:clickedButtonAtIndex:
// determines if this view can become the first responder
- (BOOL)canBecomeFirstResponder
{
return YES; // this view can be the first responder
} // end method canBecomeFirstResponder
// free MainView's memory
- (void)dealloc
{
[squiggles release]; // release the squiggles NSMutableDictionary
[finishedSquiggles release]; // release finishedSquiggles
[color release]; // release the color UIColor
[super dealloc];
} // end method dealloc
@end
CGContextRef CreateBitmapContext(NSUInteger w, NSUInteger h)
{
CGContextRef context = NULL;
int bitmapByteCount;
int bitmapBytesPerRow;
bitmapBytesPerRow = (w * 4);
bitmapByteCount = (bitmapBytesPerRow * h);
if(globalBitmapData == NULL)
globalBitmapData = malloc( bitmapByteCount );
memset(globalBitmapData, 0, sizeof(globalBitmapData));
if (globalBitmapData == NULL)
{
return nil;
}
CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB();
context = CGBitmapContextCreate (globalBitmapData,w,h,8,bitmapBytesPerRow,
colorspace,kCGImageAlphaPremultipliedLast);
CGColorSpaceRelease(colorspace);
return context;
}
/**************************************************************************
* (C) Copyright 2010 by Deitel & Associates, Inc. All Rights Reserved. *
* *
* DISCLAIMER: The authors and publisher of this book have used their *
* best efforts in preparing the book. These efforts include the *
* development, research, and testing of the theories and programs *
* to determine their effectiveness. The authors and publisher make *
* no warranty of any kind, expressed or implied, with regard to these *
* programs or to the documentation contained in these books. The authors *
* and publisher shall not be liable in any event for incidental or *
* consequential damages in connection with, or arising out of, the *
* furnishing, performance, or use of these programs. *
* *
* As a user of the book, Deitel & Associates, Inc. grants you the *
* nonexclusive right to copy, distribute, display the code, and create *
* derivative apps based on the code for noncommercial purposes only--so *
* long as you attribute the code to Deitel & Associates, Inc. and *
* reference www.deitel.com/books/iPhoneFP/. If you have any questions, *
* or specifically would like to use our code for commercial purposes, *
* contact deitel@deitel.com. *
*************************************************************************/
// MainView.h
// View for the frontside of the Painter app.
// Implementation in MainView.m
#import <UIKit/UIKit.h>
#import "Squiggle.h"
@interface MainView : UIView
{
NSMutableDictionary *squiggles; // squiggles in progress
NSMutableArray *finishedSquiggles; // finished squiggles
UIColor *color; // the current drawing color
float lineWidth; // the current drawing line width
CGImageRef flattenedImage_;
} // end instance variable declaration
// declare color and lineWidth as properties
@property(nonatomic, retain) UIColor *color;
@property float lineWidth;
// draw the given Squiggle into the given graphics context
- (void)drawSquiggle:(Squiggle *)squiggle inContext:(CGContextRef)context;
- (void)resetView; // clear all squiggles from the view
@end // end interface MainView
/**************************************************************************
* (C) Copyright 2010 by Deitel & Associates, Inc. All Rights Reserved. *
* *
* DISCLAIMER: The authors and publisher of this book have used their *
* best efforts in preparing the book. These efforts include the *
* development, research, and testing of the theories and programs *
* to determine their effectiveness. The authors and publisher make *
* no warranty of any kind, expressed or implied, with regard to these *
* programs or to the documentation contained in these books. The authors *
* and publisher shall not be liable in any event for incidental or *
* consequential damages in connection with, or arising out of, the *
* furnishing, performance, or use of these programs. *
* *
* As a user of the book, Deitel & Associates, Inc. grants you the *
* nonexclusive right to copy, distribute, display the code, and create *
* derivative apps based on the code for noncommercial purposes only--so *
* long as you attribute the code to Deitel & Associates, Inc. and *
* reference www.deitel.com/books/iPhoneFP/. If you have any questions, *
* or specifically would like to use our code for commercial purposes, *
* contact deitel@deitel.com. *
*************************************************************************/
所以你基本上会替换项目中这些文件的原始版本以获得所需的行为