hitTest:WithEvent and Subviews

2020-02-28 02:53发布

问题:

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

回答1:

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];


回答2:

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.



回答3:

As far as I understand from your question, you have added bigger V2 on top of V1. so V2 will be touchable only with in the bounds of V1. So your gesture is not recognised in V2's extra area.



回答4:

You need to implement method:

- (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];
}

And then:

- (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 [super hitTest:point withEvent:event;
}

From 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 rarely need to call this method yourself, but you might override it to hide touch events from subviews.

This method ignores view objects that are hidden, that have disabled user interactions, or have an alpha level less than 0.01. This method does not take the view’s content into account when determining a hit. Thus, a view can still be returned even if the specified point is in a transparent portion of that view’s content.



回答5:

The way you are doing it is possible but difficult to do correctly.

What I recommend is to add the UITapGestureRecognizer to their common superview and then decide what view is the receiver depending on tap location, e.g.

- (void)onTap:(UITapGestureRecognizer*)tapRecognizer {
    CGPoint touchPosition = [tapRecognizer locationInView:self];

    CGRect view1Frame = self.view1.frame;
    view1Frame.width += 100;
    view1Frame.height += 100;

    if (CGRectContainsPoint(view1Frame, touchPosition)) {
        [self.view1 handleTap];
        return;
    }

    ...
}

If the views don't have a common superview, just embed each of them in a bigger transparent view and add the recognizer to this bigger view.