Events in CALayer - objective-c

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.

Related

How can I detect which subview is touched when dealing various moving subviews?

The following code produces an animation of an image of a shape from the top of the screen and it drifts downward using core animation. When the user taps, it will log whether the user tapped the image (the shape) or if they missed the shape and therefore touched the background. This seems to work fine. However what about when I add in other images of shapes? I'm looking for suggestions as to how to build onto this code to allow for more detailed information to be logged.
Let's say I want to programmatically add in a UIImage of triangle, a UIImage of a square, and a UIImage of a circle. I want all three images to start drifting from top to bottom. They may even overlap each other as they transition. I want to be able to log "You touched the square!" or whatever the appropriate shape I've touched. I want to be able to do so even if the square is positioned in between the triangle and the circle but part of the square is showing so I can tap it. (This example shows I'm not just wanting to interact with the top-most layer)
How do I tweak this code to programmatically add in different UIImages (various shape images perhaps) and be able to log which shape I'm touching?
- (void)viewDidLoad
{
[super viewDidLoad];
CGPoint endPoint = CGPointMake([[self view] bounds].size.width,
[[self view] bounds].size.height);
CABasicAnimation *animation = [CABasicAnimation
animationWithKeyPath:#"position"];
animation.fromValue = [NSValue valueWithCGPoint:[[_imageView layer] position]];
animation.toValue = [NSValue valueWithCGPoint:endPoint];
animation.duration = 30.0f;
[[_imageView layer] addAnimation:animation forKey:#"position"];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *t = [touches anyObject];
CGPoint thePoint = [t locationInView:self.view];
thePoint = [[_imageView layer] convertPoint:thePoint toLayer:[[self view] layer]];
if([[_imageView layer].presentationLayer hitTest:thePoint])
{
NSLog(#"You touched a Shape!");
// for now I'm just logging this information. Eventually I want to have the shape follow my figure as I move it to a new location. I want everything else to continue animating but I when I touch a particular shape I want to have complete control on repositioning that specific shape. That's just some insight beyond the scope of this question. However feel free to comment about this if you have suggestions.
}
else{
NSLog(#"backgound touched");
}
}
I'm thinking the answer to this may have something to do with looping the the various subviews. Look at how I'm thinking I might change the -touchesBegan method:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *t = [t anyObject];
CGPoint thePoint = [t locationInView:self.view];
for (UIView *myView in viewArray) {
if (CGRectContainsPoint(myView.frame, thePoint)) {....
Notice here I set up a viewArray and have put all my subviews in the viewArray. Is this something I should be using? Or perhaps something like the following if I was going to loop through my layers:
for(CALayer *mylayer in self.view.layer.sublayers)
No matter how much I try looping through my views and or layers I can't seem to get this to work. I feel like I may just be missing something obvious...
I think that the culprit is the line where you change the coordinate system for thePoint. It should probably read convertPoint:fromLayer: as prior to the execution of that line, your point is in the coordinate system of self.view and I'm assuming that you would like it to be in that of the imaveView. Alternately, you might skip that line altogether and call [t locationInView:_imageView] instead.

If statement to choose between multiple CALayer

I have a Core Animation image on boxLayer and I'm duplicating it, changing the action of and shifting the position of the 2nd (boxLayer2) so that someone can choose between the 2.
I want the user to be able to tap the image for boxLayer and the boxLayer2 image does nothing but boxLayer moves (I didn't include my animation code beyond receiving the touch) and viceversa.
I cannot get an if statement to work. I've tried multiple variations self.layer == boxLayer or CALayer == boxlayer ... sublayer is an array so that's out. Any help/explanation as I know I'm missing something would be greatly appreciated.
Thanks!
UIView *BounceView is declared in the VC
In BounceView I have 2 CALayers declared: boxlayer & boxlayer2
BounceView.m
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self setBackgroundColor:[UIColor clearColor]];
// Create the new layer object
boxLayer = [[CALayer alloc] init];
boxLayer2 = [[CALayer alloc] init];
// Give it a size
[boxLayer setBounds:CGRectMake(0.0, 0.0, 185.0, 85.0)];
[boxLayer2 setBounds:CGRectMake(0.0, 0.0, 185.0, 85.0)];
// Give it a location
[boxLayer setPosition:CGPointMake(150.0, 140.0)];
[boxLayer2 setPosition:CGPointMake(150.0, 540.0)];
// Create a UIImage
UIImage *layerImage = [UIImage imageNamed:#"error-label.png"];
UIImage *layerImage2 = [UIImage imageNamed:#"error-label.png"];
// Get the underlying CGImage
CGImageRef image = [layerImage CGImage];
CGImageRef image2 = [layerImage2 CGImage];
// Put the CGImage on the layer
[boxLayer setContents:(__bridge id)image];
[boxLayer2 setContents:(__bridge id)image2];
// Let the image resize (without changing the aspect ratio)
// to fill the contentRect
[boxLayer setContentsGravity:kCAGravityResizeAspect];
[boxLayer2 setContentsGravity:kCAGravityResizeAspect];
// Make it a sublayer of the view's layer
[[self layer] addSublayer:boxLayer];
[[self layer] addSublayer:boxLayer2];
}
return self;
}
- (void)touchesBegan:(NSSet *)touches
withEvent:(UIEvent *)event
{
if (CAlayer == boxLayer)
{
// do something
}
else
{
// do something else
}
}
It looks to me like you are trying to know what layer the user tapped on inside touched began and that this is your problem.
How to find out what layer was tapped
CALayer has an instance method - (CALayer *)hitTest:(CGPoint)thePoint that
Returns the farthest descendant of the receiver in the layer hierarchy (including itself) that contains a specified point.
So to find out what layer you tapped you should do something like
- (void)touchesBegan:(NSSet *)touches
withEvent:(UIEvent *)event {
UITouch *anyTouch = [[event allTouches] anyObject];
CGPoint pointInView = [anyTouch locationInView:self];
// Now you can test what layer that was tapped ...
if ([boxLayer hitTest:pointInView]) {
// do something with boxLayer
}
// the rest of your code
}
This works because hitTest will return nil if the point is outside the layer's bounds.
David Rönnqvist's post tells you how to use hitTest on the layer to figure out which layer was touched. That should work. I would code that method slightly differently, though. I would have my view's layer include boxLayer and boxLayer2 as sub-layers, and then send the hitTest method to the parent layer. It would then return the layer that contains the touch.
It would be much simpler, though, if you use separate views, each with a layer that contains your content. Then you can use gesture recognizers on each view, and higher level Cocoa Touch code rather than CA code to manage taps. Cleaner and easier to maintain.

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.

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.

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

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.