From cd493358cc802d878101a618676bea23de07e7fc Mon Sep 17 00:00:00 2001 From: Scott Goodson Date: Sun, 27 Mar 2016 22:02:13 -0700 Subject: [PATCH] [ASControlNode] Upgrades to +setEnableHitTestDebug: to intersect hitTestSlop with parents' bounds+slop, to accurately predict and visualize UIKit event delivery edge cases. --- AsyncDisplayKit/ASControlNode.mm | 45 +++++++++++++++++++++++++------- AsyncDisplayKit/ASDisplayNode.mm | 2 ++ AsyncDisplayKit/ASImageNode.mm | 14 +++++----- 3 files changed, 45 insertions(+), 16 deletions(-) diff --git a/AsyncDisplayKit/ASControlNode.mm b/AsyncDisplayKit/ASControlNode.mm index 41b41b091d..247752d523 100644 --- a/AsyncDisplayKit/ASControlNode.mm +++ b/AsyncDisplayKit/ASControlNode.mm @@ -9,6 +9,8 @@ #import "ASControlNode.h" #import "ASControlNode+Subclasses.h" #import "ASThread.h" +#import "ASDisplayNodeExtras.h" +#import "ASImageNode.h" // UIControl allows dragging some distance outside of the control itself during // tracking. This value depends on the device idiom (25 or 70 points), so @@ -73,7 +75,7 @@ static BOOL _enableHitTestDebug = NO; @implementation ASControlNode { - ASDisplayNode *_debugHighlightOverlay; + ASImageNode *_debugHighlightOverlay; } #pragma mark - Lifecycle @@ -251,10 +253,10 @@ static BOOL _enableHitTestDebug = NO; // add a highlight overlay node with area of ASControlNode + UIEdgeInsets self.clipsToBounds = NO; - _debugHighlightOverlay = [[ASDisplayNode alloc] init]; + _debugHighlightOverlay = [[ASImageNode alloc] init]; + _debugHighlightOverlay.zPosition = 1000; // CALayer doesn't have -moveSublayerToFront, but this will ensure we're over the top of any siblings. _debugHighlightOverlay.layerBacked = YES; - _debugHighlightOverlay.backgroundColor = [[UIColor greenColor] colorWithAlphaComponent:0.5]; - + _debugHighlightOverlay.backgroundColor = [[UIColor greenColor] colorWithAlphaComponent:0.4]; [self addSubnode:_debugHighlightOverlay]; } } @@ -461,12 +463,35 @@ void _ASEnumerateControlEventsIncludedInMaskWithBlock(ASControlNodeEvent mask, v [super layout]; if (_debugHighlightOverlay) { - UIEdgeInsets insets = [self hitTestSlop]; - CGRect controlNodeRect = self.bounds; - _debugHighlightOverlay.frame = CGRectMake(controlNodeRect.origin.x + insets.left, - controlNodeRect.origin.y + insets.top, - controlNodeRect.size.width - insets.left - insets.right, - controlNodeRect.size.height - insets.top - insets.bottom); + + // Even if our parents don't have clipsToBounds set and would allow us to display the debug overlay, UIKit event delivery (hitTest:) + // will not search sub-hierarchies if one of our parents does not return YES for pointInside:. In such a scenario, hitTestSlop + // may not be able to expand the tap target as much as desired without also setting some hitTestSlop on the limiting parents. + CGRect intersectRect = UIEdgeInsetsInsetRect(self.bounds, [self hitTestSlop]); + CALayer *layer = self.layer; + CALayer *intersectLayer = layer; + CALayer *intersectSuperlayer = layer.superlayer; + + // Stop climbing if we encounter a UIScrollView, as its offset bounds origin may make it seem like our events will be clipped when + // scrolling will actually reveal them (because this process will not re-run due to scrolling) + while (intersectSuperlayer && ![intersectSuperlayer.delegate respondsToSelector:@selector(contentOffset)]) { + // Get our parent's tappable bounds. If the parent has an associated node, consider hitTestSlop, as it will extend its pointInside:. + CGRect parentHitRect = intersectSuperlayer.bounds; + ASDisplayNode *parentNode = ASLayerToDisplayNode(intersectSuperlayer); + if (parentNode) { + parentHitRect = UIEdgeInsetsInsetRect(parentHitRect, [parentNode hitTestSlop]); + } + + // Convert our current rectangle to parent coordinates, and intersect with the parent's hit rect. + CGRect intersectRectInParentCoordinates = [intersectSuperlayer convertRect:intersectRect fromLayer:intersectLayer]; + intersectRect = CGRectIntersection(parentHitRect, intersectRectInParentCoordinates); + + // Advance up the tree. + intersectLayer = intersectSuperlayer; + intersectSuperlayer = intersectLayer.superlayer; + } + + _debugHighlightOverlay.frame = [intersectLayer convertRect:intersectRect toLayer:layer]; } } diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index 17797a62af..ae7fff8f64 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -2383,11 +2383,13 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) return [superview gestureRecognizerShouldBegin:gestureRecognizer]; } +#if DEBUG - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { ASDisplayNodeAssertMainThread(); return [_view hitTest:point withEvent:event]; } +#endif - (void)setHitTestSlop:(UIEdgeInsets)hitTestSlop { diff --git a/AsyncDisplayKit/ASImageNode.mm b/AsyncDisplayKit/ASImageNode.mm index 7c27ed561a..3ab12e565a 100644 --- a/AsyncDisplayKit/ASImageNode.mm +++ b/AsyncDisplayKit/ASImageNode.mm @@ -100,12 +100,6 @@ _cropDisplayBounds = CGRectNull; _placeholderColor = ASDisplayNodeDefaultPlaceholderColor(); - if ([ASImageNode shouldShowImageScalingOverlay]) { - _debugLabelNode = [[ASTextNode alloc] init]; - _debugLabelNode.layerBacked = YES; - [self addSubnode:_debugLabelNode]; - } - return self; } @@ -144,6 +138,14 @@ [self invalidateCalculatedLayout]; if (image) { [self setNeedsDisplay]; + + if ([ASImageNode shouldShowImageScalingOverlay]) { + ASPerformBlockOnMainThread(^{ + _debugLabelNode = [[ASTextNode alloc] init]; + _debugLabelNode.layerBacked = YES; + [self addSubnode:_debugLabelNode]; + }); + } } else { self.contents = nil; }