Cocoa-touch - drawing with core graphics to a touch point - objective-c

so i want to do something which seems pretty simple but is proving other wise, i want to just draw a square at the point that touch i registered. i cant seem to get it to work though. in touchesBegan i am calling a custom method called drawSquare that is sent a CGRect. i know i must be doing something simple wrong but i don't know enough about drawing primitives in xcode/cocoa-touch. any help would be greatly appreciated. also here is my code:
- (void)drawSquare:(CGRect)rect{
//Get the CGContext from this view
CGContextRef context = UIGraphicsGetCurrentContext();
//Draw a rectangle
CGContextSetFillColorWithColor(context, [UIColor redColor].CGColor);
//Define a rectangle
CGContextAddRect(context, rect);
//Draw it
CGContextFillPath(context);
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
for (UITouch *touch in touches) {
// gets the coordinats of the touch with respect to the specified view.
CGPoint touchPoint = [touch locationInView:self];
CGRect rect = CGRectMake(touchPoint.x, touchPoint.y, 50, 50);
[self drawSquare:rect];
}
}

Don't try to do your drawing in your event methods. Update your list of what you want to draw, and send -setNeedsDisplay.

Related

Highlighting the PDF - iOS

I want to implement a Highlight function in my pdf reader app. Unfortunately, my research yielded very few information about this. However, I came to believe that I will have to use an "overlay" where the drawing or "highlighting" must be done. What I plan to do now is add a CALayer into the pdf. I am successful in rendering shapes into the layer (e.g a simple line, circle, and a square), but I can't seem to draw freely into it (like in Draw Something). Here is the code I used:
When the user begins highlighting:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
prevPoint = [touch locationInView:theContentView];
drawImageLayer = [CALayer layer];
drawImageLayer.frame = theContentView.frame;
[theContentView.layer addSublayer:drawImageLayer];
}
When the user is starting the highlighting:
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
currPoint = [touch locationInView:theContentView];
drawImageLayer.delegate = self;
[drawImageLayer setNeedsDisplay];
}
This is the code where the drawing happens:
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx{
NSLog(#"DrawLayer being called..");
CGContextSaveGState(ctx);
CGContextSetLineCap(ctx, kCGLineCapRound);
CGContextSetLineWidth(ctx, 1.0);
CGContextSetRGBStrokeColor(ctx, 1, 0, 0, 1);
CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, prevPoint.x, prevPoint.y);
CGContextAddLineToPoint(ctx, currPoint.x, currPoint.y);
CGContextStrokePath(ctx);
prevPoint = currPoint;
CGContextRestoreGState(ctx);
}
What happens is that it draws a point and that point follows the cursor everywhere! Can anyone tell me what's wrong with this code?
drawLayer: redraws the entire layer; it does not keep what was previously drawn. You draw a line from prevPoint to currPoint then update currPoint. Since drawLayer: will be called whenever you update currPoint (since you call setNeedsDisplay), prevPoint will be very close to currPoint, which is why you basically just see a point following the user's finger.
If you want a straight line starting where the user touched down and ending where the user's finger currently is, you probably want to just get rid of the line
prevPoint = currPoint;, which will thus always draw a line from where the user first touched down to where the users' finger currently is.
If you want a smooth line that follows the user's finger, then you'll need to keep track of a list of points, and connect all of them together in drawLayer. In practice, since touchesMoved: is not called after every single-pixel move, you might have to interpolate a curve that smoothly connects all the points.

How can I make a UIImageView follow a certain path? [duplicate]

I am developing a simple animation where an UIImageView moves along a UIBezierPath, now I want to provide user interation to the moving UIImageView so that user can guide the UIImageView by touching the UIImageView and drag the UIImageview around the screen.
Change the frame of the position of the image view in touchesMoved:withEvent:.
Edit: Some code
- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
UITouch *touch = [[event allTouches] anyObject];
if ([touch.view isEqual: self.view] || touch.view == nil) {
return;
}
lastLocation = [touch locationInView: self.view];
}
- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {
UITouch *touch = [[event allTouches] anyObject];
if ([touch.view isEqual: self.view]) {
return;
}
CGPoint location = [touch locationInView: self.view];
CGFloat xDisplacement = location.x - lastLocation.x;
CGFloat yDisplacement = location.y - lastLocation.y;
CGRect frame = touch.view.frame;
frame.origin.x += xDisplacement;
frame.origin.y += yDisplacement;
touch.view.frame = frame;
lastLocation=location;
}
You should also implement touchesEnded:withEvent: and touchesCanceled:withEvent:.
So you want the user to be able to touch an image in the middle of a keyframe animation along a curved path, and drag it to a different location? What do you want to happen to the animation at that point?
You have multiple challenges.
First is detecting the touch on the object while a keyframe animation is "in flight".
To do that, you want to use the parent view's layer's presentation layer's hitTest method.
A layer's presentation layer represents the state of the layer at any given instant, including animations.
Once you detect touches on your view, you will need to get the image's current location from the presentation layer, stop the animation, and take over with a touchesMoved/touchesDragged based animation.
I wrote a demo application that shows how to detect touches on an object that's being animated along a path. That would be a good starting point.
Take a look here:
Core Animation demo including detecting touches on a view while an animation is "in flight".
Easiest way would be subclassing UIImageView.
For simple dragging take a look at the code here (code borrowed from user MHC):
UIView drag (image and text)
Since you want to drag along Bezier path you'll have to modify touchesMoved:
-(void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *aTouch = [touches anyObject];
//here you have location of user's finger
CGPoint location = [aTouch locationInView:self.superview];
[UIView beginAnimations:#"Dragging A DraggableView" context:nil];
//commented code would simply move the view to that point
//self.frame = CGRectMake(location.x-offset.x,location.y-offset.y,self.frame.size.width, self.frame.size.height);
//you need some kind of a function
CGPoint calculatedPosition = [self calculatePositonForPoint: location];
self.frame = CGRectMake(calculatedPosition.x,calculatedPosition.y,self.frame.size.width, self.frame.size.height);
[UIView commitAnimations];
}
What exactly you would like to do in -(CGPoint) calculatePositionForPoint:(CGPoint)location
is up to you. You could for example calculate point in Bezier path that is the closest to location. For simple test you can do:
-(CGPoint) calculatePositionForPoint:(CGPoint)location {
return location;
}
Along the way you're gonna have to decide what happens if user wonders off to far from your
precalculated Bezier path.

Events in CALayer

I am using CALayers animation in my code.
Below is my code
CALayer *backingLayer = [CALayer layer];
backingLayer.contentsGravity = kCAGravityResizeAspect;
// set opaque to improve rendering speed
backingLayer.opaque = YES;
backingLayer.backgroundColor = [UIColor whiteColor].CGColor;
backingLayer.frame = CGRectMake(0, 0, templateWidth, templateHeight);
[backingLayer addSublayer:view.layer];
CGFloat scale = [[UIScreen mainScreen] scale];
CGSize size = CGSizeMake(backingLayer.frame.size.width*scale, backingLayer.frame.size.height*scale);
UIGraphicsBeginImageContextWithOptions(size, NO, scale);
CGContextRef context = UIGraphicsGetCurrentContext();
[backingLayer renderInContext:context];
templateImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
This backingLayer has many sublayers added like this, and this view is my subView.
But now how can i get the events of the view in the respective UIViews since i have added them as sublayers , i am trying to achieve something like the Flipboard application, where they have the page navigation and click events even though they are sub layers.
The point of CALayers is that they are light weight, specifically not having the event handling overhead. That is what UIView's are for. Your options are to either convert your code to using UIViews for event tracking, or write your own event passing code. For the second one, basically, you would have your containing UIView do a bunch of "is point in rect" queries for each sub-layer's bounds, and pass the event to (a custom method in) the CALayer with the highest z-position.
As claireware mentioned, CALayers don't support event handling directly. However, you can capture the event in the UIView that contains the CALayer and send the UIView's implicit layer a 'hitTest' message to determine which layer was touched. For example:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInView:self];
CALayer *target = [self.layer hitTest:location];
// target is the layer that was tapped
}
Here is more info about hitTest from Apple's documentation:
Returns the farthest descendant of the receiver in the layer hierarchy (including itself) that contains a specified point.
Return Value
The layer that contains thePoint, or nil if the point lies outside the receiver’s bounds rectangle.

What's the best way to animate (simple shapes) in response to touches?

I have a custom UIView on which I would like to show simple animated shapes in response to touches. For instance:
whenever the user taps the screen a circle appears, with an animation, under the finger
if the user ends touching the circle should disappear with an animation
if the user reaches a certain part of the screen the circle should increase its size (smoothly)
What I am doing now (see code below) is to use touchesBegan, touchesMoved, etc to have a set with all the touches. Then in drawRect I draw a circle for each of the touches. Everything is super static.
What is the best way to animate?
Thanks!
PS: for completeness here is part of my code
- (void)drawRect:(CGRect)rect
{
for (UITouch *touch in ts) { //ts is the set with all touches
CGPoint location = [touch locationInView:touch.view];
CGContextRef context = UIGraphicsGetCurrentContext();
CGRect rectangle = CGRectMake(location.x-circleSize/2, location.y-circleSize/2+correctionY,
circleSize*1.15, circleSize*1.15);
CGContextAddRect(context, rectangle);
CGContextSetFillColorWithColor(context, [UIColor redColor].CGColor);
CGContextFillEllipseInRect(context, rectangle);
/* etc etc */
}
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
/* etc etc */
ts = [[NSMutableSet setWithSet: [event touchesForView:self]] retain];
/* etc etc */
}
Rather than using drawRect in your main view, I would create a new subview for each object. Those subviews could be UIImageViews, or standard UIViews that you then continue to draw by subclassing the subview's drawRect.
Then, it would be trivial to animate those subviews using UIView's animation methods:
[UIView animateWithDuration:0.3
animations:^(void) {
// Do something to the view...
}];
Clearly you'd then need to add code to manage the touches and remove the subviews when needed, but that shouldn't be too onerous.

Draw and Erase Lines with Touch

I have a background image (ImageBG) and a foreground image (ImageFG) in separate UIImageViews. They are the same size and, as the name implies, ImageBG is behind ImageFG so you can't see ImageBG.
If the user starts to "draw" on them, instead of a line appearing where the user touches the screen, I want that portion of ImageFG to become transparent and reveal ImageBG.
The closest thing I can think of is a scratch-and-win.
I know how to do the drawing on a graphics context, but when I draw a transparent line (alpha = 0), well... I get a transparent line on top of whatever is currently in my graphics context, so basically nothing is drawn. This is what I use to set the stroke color.
CGContextSetRGBStrokeColor(UIGraphicsGetCurrentContext(), 1.0, 0.0, 0.0, 0.0)
What am I doing wrong? I know this is possible because there are apps out there that do it.
Any hints, tips or direction is appreciated.
So you have a layer that is some image like the grey stuff you want to scratch off, you write this image (in this example scratchImage which is a UIImageView and its UIImage .image )to the graphics context. You then stroke it with the blend mode set to kCGBlendModeClear then you save that image (with the cleared paths). This will reveal an image that you have underneath perhaps in another UIImageView. Note that in this instance, self is a UIView.
So outside touchesMoved create a variable to hold a CGPoint
CGPoint previousPoint;
CGPoint currentPoint;
Then in touchesBegan, set it to the previous point.
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
previousPoint = [touch locationInView:self];
}
In touchesMoved, write the image to the context, stroke the image with the clear blend mode, save the image from the context, and set the previous point to the current point.
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
currentPoint = [touch locationInView:self];
UIGraphicsBeginImageContext(self.frame.size);
//This may not be necessary if you are just erasing, in my case I am
//adding lines repeatedly to an image so you may want to experiment with moving
//this to touchesBegan or something. Which of course would also require the begin
//graphics context etc.
[scratchImage.image drawInRect:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height)];
CGContextSaveGState(UIGraphicsGetCurrentContext());
CGContextSetShouldAntialias(UIGraphicsGetCurrentContext(), YES);
CGContextSetLineCap(UIGraphicsGetCurrentContext(), kCGLineCapRound);
CGContextSetLineWidth(UIGraphicsGetCurrentContext(), 5.0);
CGContextSetRGBStrokeColor(UIGraphicsGetCurrentContext(), 0.25, 0.25, 0.25, 1.0);
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, nil, previousPoint.x, previousPoint.y);
CGPathAddLineToPoint(path, nil, currentPoint.x, currentPoint.y);
CGContextSetBlendMode(UIGraphicsGetCurrentContext(), kCGBlendModeClear);
CGContextAddPath(UIGraphicsGetCurrentContext(), path);
CGContextStrokePath(UIGraphicsGetCurrentContext());
scratchImage.image = UIGraphicsGetImageFromCurrentImageContext();
CGContextRestoreGState(UIGraphicsGetCurrentContext());
UIGraphicsEndImageContext();
previousPoint = currentPoint;
}