Painted painted levels: fostered fostered fostered

Tags: “iOS” “hitTest” “pointInside” author: Dac_1033 Review: QiShare team


1. Event response process

Views in iOS are superimposed layer by layer. When a view on the screen is clicked, the clicking action will be transmitted to the operating system by the hardware layer and an Event will be generated. This Event will be transmitted from the bottom to the top of the view hierarchy. Until you find a top-level view that contains the click point and can interact with the user to respond to the event. The event transmission process is illustrated on the official website:

2. Methods involved in the response chain

  • UIView hitTest method, pointInside method
// recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;   
 // default returns YES if point is in bounds
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;  

- (CGPoint)convertPoint:(CGPoint)point toView:(nullable UIView *)view;
- (CGPoint)convertPoint:(CGPoint)point fromView:(nullable UIView *)view;
- (CGRect)convertRect:(CGRect)rect toView:(nullable UIView *)view;
- (CGRect)convertRect:(CGRect)rect fromView:(nullable UIView *)view;
Copy the code
  1. Click events are passed down in a combination of hitTest and pointInside.
  2. HitTest :withEvent: Internally it first determines whether the view can respond to the touch event. If not, nil is returned, indicating that the view does not respond to the touch event. Then call pointInside:withEvent: (this method is used to determine whether the click event occurred in the current view range). If pointInside:withEvent: returns NO, hiteTest:withEvent: also returns nil directly;
  3. If pointInside:withEvent: returns YES, a hitTest:withEvent: message is sent to all the children of the current view, traversed from the topmost view all the way to the bottom view, from the end of the Subviews array. Until any subview returns a non-empty object or all subviews are iterated; The first time a subview returns a non-empty object, the hitTest:withEvent: method returns the object. If all subviews return no, the hitTest:withEvent: method returns the view itself.
  • Methods like touchesBegan, touchesMoved, And touchesEnded in UIResponder
// Generally, all responders which do custom touch handling should override all four of these methods. // Your responder will receive either touchesEnded:withEvent: or touchesCancelled:withEvent: for each // touch it is handling (those touches it received in touchesBegan:withEvent:). // *** You must handle cancelled touches to ensure correct behavior in your application. Failure to // do so is very likely to lead to incorrect behavior or crashes. - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event; - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event; - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event; - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event; - (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1); // This method is more commonly used, and will not be described here; Of course, there are more than three methods in UIResponder that respond to events, and this article uses just three of those touches as an example.Copy the code
  • The sample

In order to better understand the execution process of the above UIView and UIResponder methods in the event response process, we use the following example (sample reference article) to illustrate. View (A(B, C(D, E))) : self.view(A(B, C(D, E))

The following code overrides the parent methods we need to observe in view A, and so on in BCDE:

/* * for example: */ - (UIView *)hitTest:(CGPoint) Point withEvent:(UIEvent *)event {NSLog(@"AView ---->> hitTest:withEvent: -- -- -- "); UIView * view = [super hitTest:point withEvent:event]; NSLog(@"AView <<--- hitTest:withEvent: --- /n hitTestView:%@", view); return view; } - (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event { NSLog(@"AView --->> pointInside:withEvent: -- -- -- "); BOOL isInside = [super pointInside:point withEvent:event]; NSLog(@"AView <<--- pointInside:withEvent: --- isInside:%d", isInside); return isInside; } - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { NSLog(@"AView touchesBegan"); } - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event { NSLog(@"AView touchesMoved"); } - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event { NSLog(@"AView touchesEnded"); }Copy the code

Xcode prints the following log when it clicks on view B:

In the example, you can see the execution of the methods involved in the response chain with the following characteristics

  1. The hitTest method is not called when isUserInteractionEnabled = NO, isHidden = YES, alpha <= 0.01 in UIView;
  2. All three touches in UIResponder occur after the view that found the final response event;
  3. Second, the event chain of hit-test view is conducted twice, and the specific reason is unknown.

3. Application of hitTest method

  • Change the response hot area of UIButton

Specifically, changing the view’s response hotspots is mainly done in the pointInside method, as described in QiShare’s article on changing the hotspots. However, hitTest and pointInside are both methods in the response chain and can also return a certain view in hitTest if required.

  • View out of superView bounds can still respond to events

As shown, add a UIButton to the yellow superView. The top half of UIButton is outside the superView. Under normal circumstances, UIButton cannot respond to click events when the red box area is clicked. To make UIButton in the red box area still respond to click events, we need to rewrite the hitTest method of superView.

#import "BeyondBoundsOfView.h" @interface BeyondBoundsOfView () @property (nonatomic, strong) UIButton *button; @end @implementation BeyondBoundsOfView - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { _button = [UIButton buttonWithType:UIButtonTypeSystem]; [_button setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; [_button setTitle:@"UIButton" forState:UIControlStateNormal]; [_button setBackgroundColor:[UIColor lightGrayColor]]; _button.frame = CGRectMake(0, 0, 80, 80); [self addSubview:_button]; } return self; } - (void)layoutSubviews { [super layoutSubviews]; CGSize size = self.frame.size; _button.center = CGPointMake(size.width / 2, 0); } - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { if (! Self. IsUserInteractionEnabled | | self. IsHidden | | the self. The alpha < = 0.01) {return nil; } for (UIView *subview in self.subviews) { CGPoint convertedPoint = [subview convertPoint:point fromView:self]; UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event]; if (hitTestView) { return hitTestView; } } return nil; } @endCopy the code

The key line in the code above:

CGPoint convertedPoint = [subview convertPoint:point fromView:self]; Getting the convertedPoint is critical for us to loop through the hitTest of the child view.

Project source GitHub address


Recommended articles:

IOS about tabBar several notes algorithm small column: talk about big O notation iOS UIWebView, WKWebView injection Cookie Cookie introduction iOS icon & startup graph generator (a) algorithm small column: “D&C thought” and “quick sort” strange dance weekly