[ASControlNode] Upgrades to +setEnableHitTestDebug: to intersect hitTestSlop with parents' bounds+slop, to accurately predict and visualize UIKit event delivery edge cases.

This commit is contained in:
Scott Goodson
2016-03-27 22:02:13 -07:00
parent 3e2414da6f
commit cd493358cc
3 changed files with 45 additions and 16 deletions

View File

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