I'm creating a simple drawing application and would like some help with drawing rectangles based on the user's touch events. I'm able to draw a rectangle using Core Graphics from the point in touchesBegan to the point in touchesEnded. This rectangle is then displayed once the touchesEnded event is fired.
However, I would like to be able to do this in a 'live' fashion. Meaning, the rectangle is updated and displayed as the user drags their finger. Is this possible? Any help would be greatly appreciated, thank you!
UPDATE:
I was able to get this to work by using the following code. It works perfectly fine for small images, however it get's very slow for large images. I realize my solution is hugely inefficient and was wondering if anyone could suggest a better solution. Thanks.
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
_firstPoint = [touch locationInView:self.view];
_firstPoint.y -= 40;
_lastPoint = _firstPoint;
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint currentPoint = [touch locationInView:self.view];
currentPoint.y -= 40;
[self drawSquareFrom:_firstPoint to:currentPoint];
_lastPoint = currentPoint;
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint currentPoint = [touch locationInView:self.view];
currentPoint.y -= 40;
[self drawSquareFrom:_firstPoint to:currentPoint];
[_modifiedImage release];
_modifiedImage = [[UIImage alloc] initWithCGImage:_imageView.image.CGImage];
}
- (void)drawSquareFrom:(CGPoint)firstPoint to:(CGPoint)lastPoint
{
_imageView.image = _modifiedImage;
CGFloat width = lastPoint.x - firstPoint.x;
CGFloat height = lastPoint.y - firstPoint.y;
_path = [UIBezierPath bezierPathWithRect:CGRectMake(firstPoint.x, firstPoint.y, width, height)];
UIGraphicsBeginImageContext( _originalImage.size );
[_imageView.image drawInRect:CGRectMake(0, 0, _originalImage.size.width, _originalImage.size.height)];
_path.lineWidth = 10;
[[UIColor redColor] setStroke];
[[UIColor clearColor] setFill];
[_path fill];
[_path stroke];
_imageView.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
What you are doing right now is creating a paired CGImage and CGBitmapContext for every call to drawSquareFrom:to: and disposing them each time. Instead, create a single paired CGImage and CGBitmapContext that you reuse for each call.
-(void) drawSquareFrom:(CGPoint)from to:(CGPoint)to {
CGContextRef context = [self offscreenContext];
CGRect draw , full = [self offscreenContextBounds];
draw.origin = from;
draw.size.width = to.x - from.x;
draw.size.height = to.y - from.y;
draw = CGRectStandardize( draw );
[[UIColor redColor] setStroke];
[[UIColor clearColor] setFill];
CGContextClearRect( context , full );
CGContextFillRect( context , draw );
CGContextStrokeRectWithWidth( context , draw , 10 );
[_imageView setNeedsDisplay];
}
You create a paired CGImage and CGContext like this, although there are some ... to fill in. All the myX variables are intended to be class members.
-(CGContextRef) offscreenContext {
if ( nil == myContext ) {
size_t width = 400;
size_t height = 300;
CFMutableDataRef data = CFDataCreateMutable( NULL , width * height * 4 ); // 4 is bytes per pixel
CGDataProviderRef provider = CGDataProviderCreateWithCFData( data );
CGImageRef image = CGImageCreate( width , height , ... , provider , ... );
CGBitmapContextRef context = CGBitmapContextCreate( CFDataGetMutableBytePtr( data ) , width , height , ... );
CFRelease( data ); // retained by provider I think
CGDataProviderRelease( provider ); // retained by image
myImage = image;
myContext = context;
myContextFrame.origin = CGPointZero;
myContextFrame.size.width = width;
myContextFrame.size.height = height;
_imageView.image = [UIImage imageWithCGImage:image];
}
return myContext;
}
-(CGRect) offscreenContextBounds {
return myContextFrame;
}
This sets the _imageView.image to the newly created image. In drawSquareFrom:to: I assume that setNeedsDisplay is sufficient to draw the changes to made, but it may be that you need to assign a new UIImage each time that wraps the same CGImage.
Just use two Contexts.
In one you just "preview" draw the Rectangle and you can clear it on TouchesMoved.
On TouchesEnd you finally draw on your original context and then on the Image.
Related
I'm writing a small word search game for the iPad.
i have a UIViewController and I tile the letters random in a UIView (lettersView). I set the label's tag in a 10x10 size matrix, using a UILabel for each letter.
Here is my problem: I don't know how to draw over letters (UILabels).
I have researched the matter and am confused which way to use (touch, opengl, quartz, hittest).
I just want to draw a straight line from the first point to the last point (touch down to touch up).
when user taps on the lettersView, I must find which letter was tapped. I can find the letter if I get the which UILabel was tapped first.
while the user moves their finger, it should draw the line, but while the user moves it must delete the old ones.
when the user touches up how can I get the last UILabel? i can check if its drawable, if I get the first and last uilabel.
(i found a small drawing sample written in ARC mode. but i cant convert it to non-ARC.)
i did all what you said. but i have some problems.
it draws the line on drawRect method. when i draw second line, it deletes the old one.
here is the codes:
-(void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
touchStart = [[touches anyObject] locationInView:self];
float xPos=touchStart.x;
float yPos=touchStart.y;
startIndexCol = floor(xPos / letterWidth);
startIndexRow = floor(yPos / letterHeight);
xPos = (startIndexCol * letterWidth) + letterWidth/2;
yPos = (startIndexRow * letterHeight) + letterHeight/2;
touchStart = CGPointMake(xPos, yPos);
touchCurrent = touchStart;
[self setNeedsDisplay];
}
-(void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
touchCurrent = [[touches anyObject] locationInView:self];
//CGRect frame = CGRectMake(touchStart.x, touchStart.y, touchCurrent.x, touchCurrent.y);
//[self setNeedsDisplayInRect:frame];
[self setNeedsDisplay];
}
-(void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
touchCurrent = [[touches anyObject] locationInView:self];
//[self hitTest:touchCurrent withEvent:event];
float xPos=touchCurrent.x;
float yPos=touchCurrent.y;
endIndexCol = floor(xPos / letterWidth);
endIndexRow = floor(yPos / letterHeight);
xPos = (endIndexCol * letterWidth) + letterWidth/2;
yPos = (endIndexRow * letterHeight) + letterHeight/2;
touchCurrent = CGPointMake(xPos, yPos);
//CGRect frame = CGRectMake(touchStart.x, touchStart.y, touchCurrent.x, touchCurrent.y);
//[self setNeedsDisplayInRect:frame];
//check if it can be drawn
if ((startIndexCol == endIndexCol) && (startIndexRow == endIndexRow)) {
//clear, do nothing
touchStart = touchCurrent = (CGPoint) { -1, -1 };
startIndexCol = startIndexRow = endIndexCol = endIndexRow = -1;
}
else{
if (startIndexRow == endIndexRow) {//horizontal
if (endIndexCol > startIndexCol) {
[delegate checkWordInHorizontal:startIndexRow start:startIndexCol end:endIndexCol];
}
else{
[delegate checkWordInHorizontalBack:startIndexRow start:startIndexCol end:endIndexCol];
}
}
else if (startIndexCol == endIndexCol){ //vertical
if (endIndexRow > startIndexRow) {
[delegate checkWordInVertical:startIndexCol start:startIndexRow end:endIndexRow];
}
else{
[delegate checkWordInVerticalBack:startIndexCol start:startIndexRow end:endIndexRow];
}
}
}
[self setNeedsDisplay];
}
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
UIView *subview = [super hitTest:point withEvent:event];
if ([subview isEqual:self]) {
NSLog(#"point:%f - %f", point.x, point.y);
return self;
}
return nil;
}
-(void)drawLine{
NSLog(#"draw it");
drawIt=YES;
[self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
if (drawIt) {
NSLog(#"drawit");
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(ctx, 20.0);
CGContextSetRGBStrokeColor(ctx, 1.0, 2.0, 0, 0.5);
CGContextMoveToPoint(ctx, touchStart.x, touchStart.y);
CGContextAddLineToPoint(ctx, touchCurrent.x, touchCurrent.y);
CGContextStrokePath(ctx);
drawIt=NO;
touchStart = touchCurrent = (CGPoint) { -1, -1 };
startIndexCol = startIndexRow = endIndexCol = endIndexRow = -1;
}
[[UIColor redColor] setStroke];
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:touchStart];
[path addLineToPoint:touchCurrent];
[path setLineWidth:20.0];
[path strokeWithBlendMode:kCGBlendModeNormal alpha:0.5];
// [path stroke];
}
First, the UILabels are likely to cause you more trouble than good. For such as simple grid, it is likely easier to just draw the letters by hand in drawRect:, along with your connecting line. The overall structure would look like this:
In touchesBegan:withEvent:, you would make a note of the starting point in an ivar.
In touchesMoved:withEvent:, you would update the end-point in an ivar and call [self setNeedsDisplayInRect:]. Pass the rectangle defined by your two points.
In touchesEnded:withEvent:, you would make a hit-test, do whatever processing that entails, and then clear your starting and end points and call [self setNeedsDisplayInRect:].
By "make a hit-test," I mean calculate which letter is under the touch. Since you're laying these out in a grid, this should be very simple math.
In drawRect:, you would use [NSString drawAtPoint:withFont:] to draw each letter. You would then use UIBezierPath to draw the line (you could also use CGPathRef, but I'd personally use the UIKit object).
I currently have the following code to try and allow the user to draw a dotted path and make a custom shape. Once they have made this shape, I wanted it to automatically be filled with colour. That isn't happening.
At the moment I am getting this error for the code below:
<Error>: CGContextClosePath: no current point.
Here's the code I'm using:
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint previous = [touch previousLocationInView:self];
CGPoint current = [touch locationInView:self];
#define SQR(x) ((x)*(x))
//Check for a minimal distance to avoid silly data
if ((SQR(current.x - self.previousPoint2.x) + SQR(current.y - self.previousPoint2.y)) > SQR(10))
{
float dashPhase = 5.0;
float dashLengths[] = {10, 10};
CGContextSetLineDash(context,
dashPhase, dashLengths, 2);
CGContextSetFillColorWithColor(context, [[UIColor lightGrayColor] CGColor]);
CGContextFillPath(context);
CGContextSetLineWidth(context, 2);
CGFloat gray[4] = {0.5f, 0.5f, 0.5f, 1.0f};
CGContextSetStrokeColor(context, gray);
self.brushSize = 5;
self.brushColor = [UIColor lightGrayColor];
self.previousPoint2 = self.previousPoint1;
self.previousPoint1 = previous;
self.currentPoint = current;
// calculate mid point
self.mid1 = [self pointBetween:self.previousPoint1 andPoint:self.previousPoint2];
self.mid2 = [self pointBetween:self.currentPoint andPoint:self.previousPoint1];
if(self.paths.count == 0)
{
UIBezierPath* newPath = [UIBezierPath bezierPath];
CGContextBeginPath(context);
[newPath moveToPoint:self.mid1];
[newPath addLineToPoint:self.mid2];
[self.paths addObject:newPath];
CGContextClosePath(context);
}
else
{
UIBezierPath* lastPath = [self.paths lastObject];
CGContextBeginPath(context);
[lastPath addLineToPoint:self.mid2];
[self.paths replaceObjectAtIndex:[self.paths indexOfObject:[self.paths lastObject]] withObject:lastPath];
CGContextClosePath(context);
}
//Save
[self.pathColors addObject:self.brushColor];
self.needsToRedraw = YES;
[self setNeedsDisplayInRect:[self dirtyRect]];
//[self setNeedsDisplay];
}
}
Why is this happening and why is the inside of the path not being filled with colour?
Your code has a few issues:
You should be doing your drawing in your view's drawRect: method, not the touch handler.
You never set the variable context with a value for the current context. Do that with the UIGraphicsGetCurrentContext() method. Again, within your drawRect: method.
You go through the trouble of creating a UIBezierPath object, but you never use it. Do that by calling CGContextAddPath( context, newPath.CGPath ), changing the variable name as needed in the two places you appear to the using a UIBezierPath.
Keep the call to setNeedsDisplayInRect: in your touch handler method. That tells the system to do the work to update your view using the drawing that your (yet to be implemented) drawRect: method draws.
I'm trying to develop a chess game on the iPhone, and have become stuck on an otherwise insignificant detail but can not seem to get past it. Can anybody see the piece that's missing?
Here's the problem. When a user clicks on a square, it should highlight in some faded color. What actually is happening though, is it just draws black, totally regardless of what color I set it to.
Here's an image of what I get. The black circle should be red, or any color.
Here, notice the UIView I'm drawing to is behind the pieces view. Doubt it matters, but I want to be complete.
Finally, the code for the layer:
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
self.backgroundColor = [UIColor clearColor];
rowSelected = 0;
colSelected = 0;
}
return self;
}
-(void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextClearRect(context, rect);
CGContextSetRGBStrokeColor(context, 255.0, 0.0, 0.0, 1.0);
if (rowSelected && colSelected)
{
int gridsize = (self.frame.size.width / 8);
int sx = (colSelected-1) * gridsize;
int sy = (rowSelected-1) * gridsize;
int ex = gridsize;
int ey = gridsize;
CGContextFillEllipseInRect(context, CGRectMake(sx,sy,ex,ey));
}
}
// Handles the start of a touch
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
int gridsize = (self.frame.size.width / 8);
// Position of touch in view
UITouch *touch = [[event allTouches] anyObject];
CGPoint loc = [touch locationInView:self.superview];
int x = loc.x / gridsize;
int y = loc.y / gridsize;
rowSelected = y + 1;
colSelected = x + 1;
[self setNeedsDisplay];
}
I have tried a number of things, using a value of 1 rather than 255, playing with the alpha, etc, but I can't get anything other than a solid black.
EDIT:
Also, here's the code of this view's superview:
#implementation ChessBoard
-(id)initWithFrame:(CGRect)frame
{
if (self != [super initWithFrame:frame])
return self;
int SQUARE_SIZE = frame.size.width / 8;
currentSet = BW;
UIImage *img = [UIImage imageNamed:#"board.png"];
background = [[UIImageView alloc] initWithImage:img];
[background setFrame:frame];
[self addSubview:background];
overlay = [[ChessBoardOverlay alloc] initWithFrame:frame];
[self addSubview:overlay];
In the function CGContextSetRGBStrokeColor color compontent values (red, green,blue, alpha) need to be between 0 and 1. Since you are assuming that 255 is the maximum for a color you need to divide all of your color component values by 255.0f.
CGContextSetRGBStrokeColor(context, 255.0f/255.0f, 0.0/255.0f, 0.0/255.0f, 1.0);
You say you're drawing to a layer... but a CALayer doesn't have a drawRect: method, it has drawInContext:
I would guess by trying to call drawRect: in your instance of a CALayer that the true context hasn't been built correctly, or it has but you're not tying in to it.
Maybe you should state your color this way:
CGContextSetRGBStrokeColor(context, 255.0/255.0f, 0.0/255.0f, 0.0/255.0f, 1.0f);
This happened to me once using:
[UIColor colorWithRed:12.0/255.0f green:78.0/255.0f blue:149.0/255.0f alpha:1.0f];
until I added the /255.0f so that may work.
I have drawn the polygon by using the Core Graphics. But I can't able to resize the polygon. I used UIBezierPath to draw Polygon. This is my code
CGPoint gestureStartPoint,currentPosition;
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
currentPath = [[UIBezierPath alloc]init];
currentPath.lineWidth=1;
xx1 = 30;
yy1 = 30;
xx = 30;
yy = 30;
CGPoint gestureStartPoint,currentPosition;
}
return self;
}
- (void)drawRect:(CGRect)rect {
if(drawColor==nil){
[[UIColor redColor]setStroke];
[currentPath stroke];
}
else {
[drawColor setStroke];
[currentPath stroke];
}
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
gestureStartPoint = [touch locationInView:self];
currentPosition.x = xx;
currentPosition.y = yy;
xx = gestureStartPoint.x;
yy = gestureStartPoint.y;
[currentPath moveToPoint:(currentPosition)];
[currentPath addLineToPoint:(gestureStartPoint)];
[self setNeedsDisplay];
}
This is the link of the sample resizable Polygon. How to draw the polygon with the resizable property like this? I don't know where to start to make a resizable polygon.
This is more complicated than simply invoking some CoreGraphics magic.
To simply duplicate the logic on the site you linked to, I'd start by breaking down the problem:
Two types of gestures are recognized: taps, and tap-hold-drag.
Tap should add an x,y (point) to a list of points you are storing and redraw.
Tap-hold-drag should use the x,y location of the user's tap to determine the closest vertex - and you should probably do some max distance check as well. Once you've determined which vertex the user is "dragging", you can manipulate that point in your list and redraw.
I have a basic map view in my app which contains a set of user definable way points. When the view is loaded I draw a path connecting the way points. This works well.
However, when the user drags a way point around the view, I would like it to re-draw the paths and that is my problem. I dont know how to hacant get it to draw anything after the first time. Here's my code:
- (void)drawRect:(CGRect)rect { ////// works perfectly
context = UIGraphicsGetCurrentContext();
CGContextSetRGBStrokeColor(context, 1, 0, 1, .7);
CGContextSetLineWidth(context, 20.0);
WaypointViewController *w = [arrayOfWaypoints objectAtIndex:0];
CGPoint startPoint = w.view.center;
CGContextMoveToPoint(context, startPoint.x, startPoint.y);
for (int i = 1; i<[arrayOfWaypoints count]; i++) {
WaypointViewController *w2 = [arrayOfWaypoints objectAtIndex:i];
CGPoint nextPoint = w2.view.center;
CGContextAddLineToPoint(context,nextPoint.x, nextPoint.y);
}
CGContextStrokePath(context);
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
if (moving) {
UITouch *touch = [touches anyObject];
currentWaypoint.view.center = [touch locationInView:self];
[delegate setUserInteraction:NO];
[self drawInContext:context];
[NSThread detachNewThreadSelector:#selector(drawInContext:) toTarget:self withObject:nil];
}
}
- (void)drawInContext:(CGContextRef)context { ///gets called, but does nothing
CGContextSetRGBStrokeColor(context, 1, 0, 1, .7);
CGContextSetLineWidth(context, 20.0);
WaypointViewController *w = [arrayOfWaypoints objectAtIndex:0];
CGPoint startPoint = w.view.center;
CGContextMoveToPoint(context, startPoint.x, startPoint.y);
for (int i = 1; i<[arrayOfWaypoints count]; i++) {
WaypointViewController *w2 = [arrayOfWaypoints objectAtIndex:i];
CGPoint nextPoint = w2.view.center;
CGContextAddLineToPoint(context,nextPoint.x, nextPoint.y);
}
CGContextStrokePath(context);
}
You can not use the context that is active in drawRect: anywhere else.
Instead of calling [self drawInContext:context] call [self setNeedsDisplay]
The context that you are pointing to in your drawRect: might be freed or might still be active you don't know, but either way there is no way to get the bits you put into that context onto the screen.
Also, please read the docs on drawing on iOS here
You may not save the context for use after drawRect: returns. What you need to do is call [self setNeedsDisplay] (or setNeedsDisplayInRect:, if you can determine the bounding rectangle of the change), which will cause the system to call drawRect: again for you.
You can do all your drawing only in drawRect ...Hope this helps ... hence - (void)drawInContext:(CGContextRef)context will not be able to do any drawing. if after user's touch action , you need to do some redrawing then you need to call [self setNeedsDisplay] in your touchmethods.