Skip to content
This repository was archived by the owner on Feb 2, 2023. It is now read-only.

Commit cd49335

Browse files
committed
[ASControlNode] Upgrades to +setEnableHitTestDebug: to intersect hitTestSlop with parents' bounds+slop, to accurately predict and visualize UIKit event delivery edge cases.
1 parent 3e2414d commit cd49335

3 files changed

Lines changed: 45 additions & 16 deletions

File tree

AsyncDisplayKit/ASControlNode.mm

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
#import "ASControlNode.h"
1010
#import "ASControlNode+Subclasses.h"
1111
#import "ASThread.h"
12+
#import "ASDisplayNodeExtras.h"
13+
#import "ASImageNode.h"
1214

1315
// UIControl allows dragging some distance outside of the control itself during
1416
// tracking. This value depends on the device idiom (25 or 70 points), so
@@ -73,7 +75,7 @@ @interface ASControlNode ()
7375

7476
@implementation ASControlNode
7577
{
76-
ASDisplayNode *_debugHighlightOverlay;
78+
ASImageNode *_debugHighlightOverlay;
7779
}
7880

7981
#pragma mark - Lifecycle
@@ -251,10 +253,10 @@ - (void)addTarget:(id)target action:(SEL)action forControlEvents:(ASControlNodeE
251253

252254
// add a highlight overlay node with area of ASControlNode + UIEdgeInsets
253255
self.clipsToBounds = NO;
254-
_debugHighlightOverlay = [[ASDisplayNode alloc] init];
256+
_debugHighlightOverlay = [[ASImageNode alloc] init];
257+
_debugHighlightOverlay.zPosition = 1000; // CALayer doesn't have -moveSublayerToFront, but this will ensure we're over the top of any siblings.
255258
_debugHighlightOverlay.layerBacked = YES;
256-
_debugHighlightOverlay.backgroundColor = [[UIColor greenColor] colorWithAlphaComponent:0.5];
257-
259+
_debugHighlightOverlay.backgroundColor = [[UIColor greenColor] colorWithAlphaComponent:0.4];
258260
[self addSubnode:_debugHighlightOverlay];
259261
}
260262
}
@@ -461,12 +463,35 @@ - (void)layout
461463
[super layout];
462464

463465
if (_debugHighlightOverlay) {
464-
UIEdgeInsets insets = [self hitTestSlop];
465-
CGRect controlNodeRect = self.bounds;
466-
_debugHighlightOverlay.frame = CGRectMake(controlNodeRect.origin.x + insets.left,
467-
controlNodeRect.origin.y + insets.top,
468-
controlNodeRect.size.width - insets.left - insets.right,
469-
controlNodeRect.size.height - insets.top - insets.bottom);
466+
467+
// Even if our parents don't have clipsToBounds set and would allow us to display the debug overlay, UIKit event delivery (hitTest:)
468+
// will not search sub-hierarchies if one of our parents does not return YES for pointInside:. In such a scenario, hitTestSlop
469+
// may not be able to expand the tap target as much as desired without also setting some hitTestSlop on the limiting parents.
470+
CGRect intersectRect = UIEdgeInsetsInsetRect(self.bounds, [self hitTestSlop]);
471+
CALayer *layer = self.layer;
472+
CALayer *intersectLayer = layer;
473+
CALayer *intersectSuperlayer = layer.superlayer;
474+
475+
// Stop climbing if we encounter a UIScrollView, as its offset bounds origin may make it seem like our events will be clipped when
476+
// scrolling will actually reveal them (because this process will not re-run due to scrolling)
477+
while (intersectSuperlayer && ![intersectSuperlayer.delegate respondsToSelector:@selector(contentOffset)]) {
478+
// Get our parent's tappable bounds. If the parent has an associated node, consider hitTestSlop, as it will extend its pointInside:.
479+
CGRect parentHitRect = intersectSuperlayer.bounds;
480+
ASDisplayNode *parentNode = ASLayerToDisplayNode(intersectSuperlayer);
481+
if (parentNode) {
482+
parentHitRect = UIEdgeInsetsInsetRect(parentHitRect, [parentNode hitTestSlop]);
483+
}
484+
485+
// Convert our current rectangle to parent coordinates, and intersect with the parent's hit rect.
486+
CGRect intersectRectInParentCoordinates = [intersectSuperlayer convertRect:intersectRect fromLayer:intersectLayer];
487+
intersectRect = CGRectIntersection(parentHitRect, intersectRectInParentCoordinates);
488+
489+
// Advance up the tree.
490+
intersectLayer = intersectSuperlayer;
491+
intersectSuperlayer = intersectLayer.superlayer;
492+
}
493+
494+
_debugHighlightOverlay.frame = [intersectLayer convertRect:intersectRect toLayer:layer];
470495
}
471496
}
472497

AsyncDisplayKit/ASDisplayNode.mm

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2383,11 +2383,13 @@ - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
23832383
return [superview gestureRecognizerShouldBegin:gestureRecognizer];
23842384
}
23852385

2386+
#if DEBUG
23862387
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
23872388
{
23882389
ASDisplayNodeAssertMainThread();
23892390
return [_view hitTest:point withEvent:event];
23902391
}
2392+
#endif
23912393

23922394
- (void)setHitTestSlop:(UIEdgeInsets)hitTestSlop
23932395
{

AsyncDisplayKit/ASImageNode.mm

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,6 @@ - (id)init
100100
_cropDisplayBounds = CGRectNull;
101101
_placeholderColor = ASDisplayNodeDefaultPlaceholderColor();
102102

103-
if ([ASImageNode shouldShowImageScalingOverlay]) {
104-
_debugLabelNode = [[ASTextNode alloc] init];
105-
_debugLabelNode.layerBacked = YES;
106-
[self addSubnode:_debugLabelNode];
107-
}
108-
109103
return self;
110104
}
111105

@@ -144,6 +138,14 @@ - (void)setImage:(UIImage *)image
144138
[self invalidateCalculatedLayout];
145139
if (image) {
146140
[self setNeedsDisplay];
141+
142+
if ([ASImageNode shouldShowImageScalingOverlay]) {
143+
ASPerformBlockOnMainThread(^{
144+
_debugLabelNode = [[ASTextNode alloc] init];
145+
_debugLabelNode.layerBacked = YES;
146+
[self addSubnode:_debugLabelNode];
147+
});
148+
}
147149
} else {
148150
self.contents = nil;
149151
}

0 commit comments

Comments
 (0)