I can get a sublayer at any point through hittest at that point by coding. But i want to get all the layers at that point.
The problem is:
By hit test i can only get the top most layer at hat point, it does not give any information about the layers beneath that top layer at that point.
Is there any way to find all the layers at a specific point in ios?
Hit testing is used for the purpose of delivering events. Since events are always delivered to the topmost view it makes no sense for hit test to continue testing the ones below.
However if you do want to find all the layers at a point you can write your own version of hitTest as a category on CALayer that does this :
-(NSMutableSet*)allLayersAtPoint:(CGPoint)aPoint
{
NSMutableSet *layers = [[NSMutableSet alloc] init];
if(! CGRectContainsPoint([self frame],aPoint))
{
return layers;
}
[layers addObject:self]
for(CALayer *layer in self.sublayers) {
CGPoint converted = [self convertPoint:aPoint toLayer:layer]
[layers unionSet:[layer allLayersAtPoint:converted]]
}
return layers
}
Beware of layers with sublayer transforms though. Not sure how to handle that. Usually this is done on UIViews , I'm not sure why you're doing this on layers. Just implement this as a category on UIView instead of CALayer if you want this to work on UIViews.
Related
I'm new to iOS UIView drawing and I'm really trying to implement a custom UIView class in a standard way.
The UIView class that I'm working on now is simple: it has mostly static background shapes that form a composite shape. I also want to add animation to this UIView class so that a simple shape can animate its path on top of the static shapes.
How I'm currently implementing this
Right now, I'm implementing the static drawing in the drawRect: method of the UIView subclass. This works fine. I also have not implemented animation of the single shape yet.
What I'm looking to have answered
Question 1:
Is it better to have the static shapes drawn in the drawRect: method as I'm currently doing it, or is there a benefit to refactoring all of the shapes being drawn into class-extension-scoped CAShapeLayer properties and doing something like:
-(instancetype) initWithFrame:(CGRect) frame
{
if (self = [super initWithFrame:frame])
{
[self setupView];
}
return self;
}
-(void) setupView // Allocate, init, and setup one-time layer properties like fill colorse.
{
self.shapeLayer1 = [CAShapeLayer layer];
[self.layer addSubLayer:shapeLayer1];
self.shapeLayer2 = [CAShapeLayer layer];
[self.layer addSubLayer:shapeLayer2];
// ... and so on
}
-(void) layoutSubviews // Set frames and paths of shape layers here.
{
self.shapeLayer1.frame = self.bounds;
self.shapeLayer2.frame = self.bounds;
self.shapeLayer1.path = [UIBezierPath somePath].CGPath;
self.shapeLayer2.path = [UIBezierPath somePath].CGPath;
}
Question 2:
Regardless of whether I implement the static shapes as CAShapeLayers or in drawRect:, is the best way to implement an animatable shape for this UIView a CAShapeLayer property that is implemented as the other CAShapeLayers would be implemented above? Creating a whole separate UIView class just for one animated shape and adding it as a subview to this view seems silly, so I'm thinking that a CAShapeLayer is the way to go to accomplish that.
I would appreciate any help with this very much!
You could do it either way, but using shape layers for everything would probably be cleaner and faster. Shape layers have built-in animation support using Core Animation.
It's typically faster to avoid implementing drawRect and instead let the system draw for you.
Edit:
I would actually word this more strongly than I did in 2014. The underlying drawing engine in iOS is optimized for tiled rendering using layers. You will get better performance and smoother animation by using CALayers and CAAnimation (or higher level UIView or SwiftUI animation which is built on CAAnimation.)
I'm creating a simple animation with CocosBuilder that just moves a CCLayerColor from top right to bottom left and for some reason the animations will not perform. I have the timeline set to auto play and over a duration of 2 seconds. I have a class that splits up all layers and then adds those layers to a CCScrollLayer. I'm just wondering if the problem is when i remove the layers from the scene and add then to the CCScrollLayer that the animations are removed and in turn not performed.
CCScene* scene = [CCBReader sceneWithNodeGraphFromFile:#"Untitled.ccbi"];
self.scrollLayer = [[CCScrollLayer alloc] init];
CCLayer* child = [[scene children] objectAtIndex:0];
for (CCNode* layer in [child children]) {
[layer removeFromParent];
[self.scrollLayer addChild:layer];
[layer resumeSchedulerAndActions];
}
[self.scrollLayer updatePages];
self.scrollLayer.delegate = self;
[self addChild:self.scrollLayer];
I can see the CCLayerColor object added to the screen but its just not animating.
i've added some custom code to the CCScrollLayer to deal with the situation but i'm just confused as to why the animations are not performing. any help would be great!
EDIT: Maybe a better question would be in CocosBuilder are the actions on the timeline directly linked to the object performing the action or somehow linked through the scene to that object?
Perhaps you forgot to add the scrollLayer as child?
[self addChild:self.scrollLayer];
In the sample code the node created from a ccbi is not referenced either. Maybe you aren't actually using it?
How can I achieve a smooth resizing of a MKCircleView on a UIMapView when adjusting an NSSlider? Apple has managed to do it in the Find my friends app when creating geofences (http://reviewznow.com/wp-content/uploads/2013/03/find-my-friends-location-alerts-01.jpg), so I guess it's possible in some way. So far I've tried the following solutions, but with a very "flickery" result:
First attempt
I added a new MKCircleView with an updated radius and immediately after removing the one that was (as suggested here MKOverlay not resizing smoothly) when the slider changed value. I also tried the other way around: first removing the overlay then adding a new one, but with the same "flickery" result.
- (void)sliderChanged:(UISlider*)sender
{
double radius = (sender.value * 100);
[self addCircleWithRadius:radius];
[mapView removeOverlays:[self.mapView.overlays firstObject]];
}
Second attempt
In the linked SO answer, he suggests that NSOperation could be used to "help you create the MKCircle objects faster" and thus making the resizing smoother using the above method of adding/removing overlays when the slides changes value. I did a implementation where I start a new thread whenever the slider changes. In each thread I remove all old overlays and add a new one with the updated scale. Perhaps he has some other kind of implementation in mind, because the way I did it I still get the same flicker when changing the slider.
- (void)sliderChanged:(UISlider*)sender
{
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self
selector:#selector(updateOverlayWithScale:)
object:[NSNumber numberWithFloat:sender.scale]];
[self.queue addOperation:operation];
}
The method that runs i each thread:
- (void)updateOverlayWithScale:(NSNumber *)scale
{
MKCircle *circle = [MKCircle circleWithCenterCoordinate:self.currentMapPin.coordinate
radius:100*[scale floatValue]];
[self.mapView performSelectorOnMainThread:#selector(removeOverlays:) withObject:self.mapView.overlays waitUntilDone:NO];
[self.mapView performSelectorOnMainThread:#selector(addOverlay:) withObject:circle waitUntilDone:NO];
}
Third attempt
I also tried implementing my own subclass of MKOverlayView that draws itself based on its scale property. Whenever the slider changes I call setNeedsDisplay and let it redraw itself, but I get the same flicker.
- (void)sliderChanged:(UISlider*)sender
{
self.currentOverlayView.scale = sender.scale
[self.currentOverlayView setNeedsDisplay];
}
And in my custom overlay view I implement drawMapRect:zoomScale:inContext:(CGContextRef)context like this
- (void)drawMapRect:(MKMapRect)mapRect
zoomScale:(MKZoomScale)zoomScale
inContext:(CGContextRef)context
{
double radius = [(MKCircle *)[self overlay] radius];
radius *= self.scale;
// ... Create a rect using the updated radius and draw a circle inside it using CGContextAddEllipseInRect ...
}
So, do you have any ideas? Thanks in advance!
A few months ago I stumbled upon this animated MKCircleView. It also has a demo on git. So I guess you should give it a go!
Check it out as you might be able to tweak it to your needs with a slider etc.
Credits go to yickhong for providing this YHAnimatedCircleView
I have a mapView (RouteMe mapView) on which there are annotations (markers).
On the mapView there is a touchesEnded function on which I usually catch all events.
Some markers have an added layer on top of them.
That layer has some animation images and as much as I know this is the only way I can show these animation images on top of the markers.
The problem is that I don't know how I can intercept a touch on a marker that has that layer on top of it. When I test the hit on the touchesEnded I recognize a CALayer class rather than a RMMArker class (obv0iously, because the layer is on top of the marker, therefore is first to intercept the event).
How can I reach the Marker once the top CALayer is tapped?
Thanks
Hackish workaround: Create an RMMapLayer instead of a CALayer. Remember to set the annotation on the sublayer to get things like callouts to work, e.g. in your RMMapLayer subclass:
RMMapLayer *sublayer = [[RMMapLayer alloc] init];
sublayer.annotation = ann;
sublayer.contents = (id)img2.CGImage;
sublayer.contentsScale = img2.scale;
sublayer.position = CGPointMake(CGRectGetWidth(self.bounds)/2, CGRectGetHeight(self.bounds)/2);
sublayer.bounds = CGRectMake(0, 0, img2.size.width, img2.size.height);
[self addSublayer:sublayer];
I don't know how many other things this has potential for breaking though, so you can follow this issue for any updates:
https://github.com/mapbox/mapbox-ios-sdk/issues/190
You can instruct the touches to passthrough. Take a look at this question: How can I click a button behind a transparent UIView?
Since the CALayer was on top of a CALAyer, all it took was to access it ancestor blockingLayer.superlayer.
By checking on the ancestor, I could have all that I needed.
This solved the problem.
I have a custom subclass of UIView, LineDrawingView. I have this as a #property in my main view controller and I add it to the view. This works fine - it creates a transparent view on top of the current view and uses UIBezierPath, touchesBegan, touchesMoved etc so you can draw all over the view by dragging your finger around.
I need to make that drawing view "L" shaped, so I can have an area in the bottom left corner where various controls are located. I can think of no way to make the drawing view "L" shaped, except maybe to add two separate rectangular drawing views, but this doesn't work because touches are interrupted when you drag your finger from one rectangle into the other. The other solution I've come up with is to add another view on top of the drawing view. This view should prevent drawing and I could also locate my controls within it so they are still usable while drawing is enabled.
I tried creating a UIView and adding it as a subview of the drawing view. I gave it a tint so I could check it was present and in the right place. I expected this to prevent drawing within the area of the new UIView, but drawing continues all over the area of the LineDrawingView. I also tried inserting the new UIView at index 2, with the LineDrawingView inserted at index 1. It still didn't affect the drawing.
self.drawView = [[LineDrawingView alloc] initWithFrame:CGRectMake(0, 50, 768, 905)];
[self.drawView setBackgroundColor:[UIColor clearColor]];
// not effective in preventing drawing!!
UIView *controlView = [[UIView alloc] initWithFrame:CGRectMake(0, 530, 310, 575)];
[controlView setUserInteractionEnabled:NO];
[controlView setBackgroundColor:[UIColor grayColor]];
[controlView setAlpha:0.3];
[self.view addSubview:drawView];
[self.drawView addSubview:controlView];
I would love to know: how can I either...
Create an "L" shaped drawing view?
OR
Cut out a section of the drawing view so users can interact with what is behind it?
OR
Impose an area on top of the drawing view where I can disable drawing and add my controls?
Here is a tutorial on creating a transparent rounded rectangle UIView, which I think you could modify in a straightforward manner to make it L shaped.
The most important thing to keep in mind that you'll have to implement your own drawRect and I believe also your own hitTest or touches (e.g. event handling like touchedBegan:withEvent:) methods.
I contacted Apple about this. It turns out there is not a way to create an "L" shaped view. The solution was to, in the drawing view, test whether touches are within the forbidden area and, if so, prevent the line from being drawn. My controls have been moved to a separate UIView which is moved to the front when drawing is enabled. This means the controls remain active the whole time and drawing cannot take place within the area of the controls.
if you want to draw an L shape drawing view you can do this by concatenating two lines only .. just like i have created an arrow .. and if you want to stop drawing while you are moving (dragging ) any object .. you just need to apply a check in your touches moved method (like is moved , )..
here is the code to draw an arrow (you can use this as a reference to draw any kind of shape) : How can I draw an arrow using Core Graphics?
code to check if you have hit the path (means your touch point is in the bezier path )
if([[self tapTargetForPath:((UIBezierPath *)[testDict objectForKey:#"path"])] containsPoint:startPoint])// if starting touch is in bezierpath
{
ishitInPath = YES;
isMoving = YES;
}
// to easily detect (or select a bezier path object)
- (UIBezierPath *)tapTargetForPath:(UIBezierPath *)path
{
if (path == nil) {
return nil;
}
CGPathRef tapTargetPath = CGPathCreateCopyByStrokingPath(path.CGPath, NULL, fmaxf(35.0f, path.lineWidth), path.lineCapStyle, path.lineJoinStyle, path.miterLimit);
if (tapTargetPath == NULL) {
return nil;
}
UIBezierPath *tapTarget = [UIBezierPath bezierPathWithCGPath:tapTargetPath];
CGPathRelease(tapTargetPath);
return tapTarget;
}