From db9b468dd1fdfc11f4927e6e05ad8074bf890717 Mon Sep 17 00:00:00 2001 From: Valentin Shergin Date: Fri, 7 Sep 2018 23:39:05 -0700 Subject: [PATCH] Fabric: `[RCTViewComponentView betterHitTest:]` proper support for `clipToBounds` and `zIndex` Summary: @public Besides `pointerEvents` there are other two props that affect hit-testing mechanism: `zIndex` and `clipToBounds`. The default UIKit implementation does not take this into an account (it always assume that `clipToBounds` is true and `zIndex` is same). `betterHitTest` does it right. Reviewed By: sahrens Differential Revision: D9688876 fbshipit-source-id: dadfd5e5541ddd1a744fbd8c6b10949c0e266069 --- .../View/RCTViewComponentView.mm | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index 720482e1a..24482c762 100644 --- a/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -196,18 +196,52 @@ using namespace facebook::react; [self invalidateLayer]; } +- (UIView *)betterHitTest:(CGPoint)point withEvent:(UIEvent *)event +{ + // This is a classic textbook implementation of `hitTest:` with a couple of improvements: + // * It takes layers' `zIndex` property into an account; + // * It does not stop algorithm if some touch is outside the view + // which does not have `clipToBounds` enabled. + + if (!self.userInteractionEnabled || self.hidden || self.alpha < 0.01) { + return nil; + } + + BOOL isPointInside = [self pointInside:point withEvent:event]; + + if (self.clipsToBounds && !isPointInside) { + return nil; + } + + NSArray<__kindof UIView *> *sortedSubviews = + [self.subviews sortedArrayUsingComparator:^NSComparisonResult(UIView *a, UIView *b) { + // Ensure sorting is stable by treating equal `zIndex` as ascending so + // that original order is preserved. + return a.layer.zPosition > b.layer.zPosition ? NSOrderedDescending : NSOrderedAscending; + }]; + + for (UIView *subview in [sortedSubviews reverseObjectEnumerator]) { + UIView *hitView = [subview hitTest:[subview convertPoint:point fromView:self] withEvent:event]; + if (hitView) { + return hitView; + } + } + + return isPointInside ? self : nil; +} + - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { auto viewProps = *std::static_pointer_cast(_props); switch (viewProps.pointerEvents) { case PointerEventsMode::Auto: - return [super hitTest:point withEvent:event]; + return [self betterHitTest:point withEvent:event]; case PointerEventsMode::None: return nil; case PointerEventsMode::BoxOnly: return [self pointInside:point withEvent:event] ? self : nil; case PointerEventsMode::BoxNone: - UIView *view = [super hitTest:point withEvent:event]; + UIView *view = [self betterHitTest:point withEvent:event]; return view != self ? view : nil; } }