Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

hitTest:WithEvent and Subviews

I have 2 views , but i want to make 1 view (virtually) bigger. if I place my tapGesture on v1, the tap gesture works with a bigger hit area but if I place my tapGesture on v2 it doesn't work ( actually it doesn't recognizes the tapGesture at all, even not inside the original bounds ) even though i loop through my TestView1 hittest method and the points get contained in the frame.

#import "ViewController.h"

@interface TestView1 : UIView
@end

@implementation TestView1

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    CGFloat radius = 100.0;
    CGRect frame = CGRectMake(0, 0,
                              self.frame.size.width + radius,
                              self.frame.size.height + radius);

    if (CGRectContainsPoint(frame, point)) {
        return self;
    }
    return nil;
}

@end

@interface TestView2 : UIView
@end

@implementation TestView2

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    CGFloat radius = 100.0;
    CGRect frame = CGRectMake(0, 0,
                              self.frame.size.width + radius,
                              self.frame.size.height + radius);

    if (CGRectContainsPoint(frame, point)) {
        return self;
    }
    return nil;
}
@end

@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    TestView1 *v1 = [[TestView1 alloc] initWithFrame:CGRectMake(50.f, 50.f, 100.f, 100.f)];
    [self.view addSubview:v1];

    TestView2 *v2 = [[TestView2 alloc] initWithFrame:CGRectMake(0.f, 0.f, 100.f, 100.f)];
    v2.backgroundColor = UIColor.yellowColor;
    [v1 addSubview:v2];

    UITapGestureRecognizer *gesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(panGesture:)];
    [v2 addGestureRecognizer:gesture];
}

- (void) panGesture:(UIPanGestureRecognizer *)recognizer
{
    NSLog(@"tap");
}
@end
like image 458
Andy Jacobs Avatar asked Aug 05 '13 14:08

Andy Jacobs


2 Answers

You're not traversing the view hierarchy. From the documentation:

This method traverses the view hierarchy by sending the pointInside:withEvent: message to each subview to determine which subview should receive a touch event. If pointInside:withEvent: returns YES, then the subview’s hierarchy is traversed; otherwise, its branch of the view hierarchy is ignored.

You only need to implement pointInside:withEvent:. You shouldn't override hitTest:withEvent: because the standard implementation will call your implementation of pointInside:withEvent: and do for you all the hard work of traversing the hierarchy.

Implement like this:

@interface TestView1 : UIView
@end

@implementation TestView1

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    CGFloat radius = 100.0;
    CGRect frame = CGRectMake(0, 0,
                              self.frame.size.width + radius,
                              self.frame.size.height + radius);

    return (CGRectContainsPoint(frame, point));
}

@end

@interface TestView2 : UIView
@end

@implementation TestView2

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    CGFloat radius = 100.0;
    CGRect frame = CGRectMake(0, 0,
                              self.frame.size.width + radius,
                              self.frame.size.height + radius);

    return (CGRectContainsPoint(frame, point));
}

@end

Now, whether you need both the views is up to you. You had already succeeded in expanding touchable area of v1, and with the code above you can do it with v2 as a subview of v1.

However, TestView1 and TestView2 implementations are exactly the same (even in your original post), so why do you need to have them separate? You could make v1 and v2 both instances of the same class, e.g. TestView, and instantiate them as follows:

TestView *v1 = [[TestView alloc] initWithFrame:CGRectMake(50.f, 50.f, 100.f, 100.f)];
[self.view addSubview:v1];
v1.clipsToBounds = YES;

TestView *v2 = [[TestView alloc] initWithFrame:CGRectMake(0.f, 0.f, 100.f, 100.f)];
v2.backgroundColor = UIColor.yellowColor;
[v1 addSubview:v2];
like image 140
Sam Avatar answered Sep 22 '22 11:09

Sam


Your v2 won't receive any touch events. because when you click the area around your v1, it returns self in its - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event, which means you have declared that it is "v1", the hit-test view, who is the destination of all the touch events.
The right way to expand your v1's touchable ares is to implement - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event in your TestView1 and TestView2:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    CGFloat radius = 100.0;
    CGRect frame = CGRectMake(0, 0,
                          self.frame.size.width + radius,
                          self.frame.size.height + radius);

    if (CGRectContainsPoint(frame, point)) {
        return YES;
    }
    return [super pointInside:point withEvent:event];
}

The code above means that, when you click the area around your v1, it declares "Yes, you have touched me. And I will check who can handle it. Maybe it's me, maybe it's one of my subviews". So the hit-test continues and v1 will find its subview v2 is the top-most view thus v2 is the destination of your click event.

You may ask how could v1 know that v2 is the one. Here is the pseudo code to reveal the trick:

@implementation UIView
//...
//...

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    return CGRectContainsPoint(self.bounds, point); // Honestly tell others if the point is inside the bounds. That's the normal case.
}

// This method returns a hit-test view who or whose gesture recognizer is responsible for handling the events
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    for(UIView *aSubview in self.subviews)
    {
        // Ask each subview if the point falls in its area.
        if ([aSubview pointInside:[self convertPoint:point toView:aSubview]  point withEvent:event])
        {
            return [aSubview hitTest:[self convertPoint:point toView:aSubview] withEvent:event];
        }
    }

    // If no one can handle the event.
    return self;
}

//...
//...
@end

These code have not taken userInteractionEnable, alpha and other things into account, for simplicity.
When you call [super pointInside:point withEvent:event]; in your TestView1's - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event, it ask if the point falls in v2's area. If v2's answer is yes and because it doesn't have any subviews, so v2 will return itself in its - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event.

That's all the story.

like image 30
liuyaodong Avatar answered Sep 21 '22 11:09

liuyaodong