ASControlNode.mm 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. //
  2. // ASControlNode.mm
  3. // AsyncDisplayKit
  4. //
  5. // Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
  6. // This source code is licensed under the BSD-style license found in the
  7. // LICENSE file in the root directory of this source tree. An additional grant
  8. // of patent rights can be found in the PATENTS file in the same directory.
  9. //
  10. #import "ASControlNode.h"
  11. #import "ASControlNode+Subclasses.h"
  12. #import "ASImageNode.h"
  13. #import "AsyncDisplayKit+Debug.h"
  14. #import "ASInternalHelpers.h"
  15. #import "ASControlTargetAction.h"
  16. #import "ASDisplayNode+FrameworkPrivate.h"
  17. #import "ASLayoutElementInspectorNode.h"
  18. // UIControl allows dragging some distance outside of the control itself during
  19. // tracking. This value depends on the device idiom (25 or 70 points), so
  20. // so replicate that effect with the same values here for our own controls.
  21. #define kASControlNodeExpandedInset (([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) ? -25.0f : -70.0f)
  22. // Initial capacities for dispatch tables.
  23. #define kASControlNodeEventDispatchTableInitialCapacity 4
  24. #define kASControlNodeActionDispatchTableInitialCapacity 4
  25. @interface ASControlNode ()
  26. {
  27. @private
  28. ASDN::RecursiveMutex _controlLock;
  29. // Control Attributes
  30. BOOL _enabled;
  31. BOOL _highlighted;
  32. // Tracking
  33. BOOL _tracking;
  34. BOOL _touchInside;
  35. // Target action pairs stored in an array for each event type
  36. // ASControlEvent -> [ASTargetAction0, ASTargetAction1]
  37. NSMutableDictionary<id<NSCopying>, NSMutableArray<ASControlTargetAction *> *> *_controlEventDispatchTable;
  38. }
  39. // Read-write overrides.
  40. @property (nonatomic, readwrite, assign, getter=isTracking) BOOL tracking;
  41. @property (nonatomic, readwrite, assign, getter=isTouchInside) BOOL touchInside;
  42. /**
  43. @abstract Returns a key to be used in _controlEventDispatchTable that identifies the control event.
  44. @param controlEvent A control event.
  45. @result A key for use in _controlEventDispatchTable.
  46. */
  47. id<NSCopying> _ASControlNodeEventKeyForControlEvent(ASControlNodeEvent controlEvent);
  48. /**
  49. @abstract Enumerates the ASControlNode events included mask, invoking the block for each event.
  50. @param mask An ASControlNodeEvent mask.
  51. @param block The block to be invoked for each ASControlNodeEvent included in mask.
  52. @param anEvent An even that is included in mask.
  53. */
  54. void _ASEnumerateControlEventsIncludedInMaskWithBlock(ASControlNodeEvent mask, void (^block)(ASControlNodeEvent anEvent));
  55. /**
  56. @abstract Returns the expanded bounds used to determine if a touch is considered 'inside' during tracking.
  57. @param controlNode A control node.
  58. @result The expanded bounds of the node.
  59. */
  60. CGRect _ASControlNodeGetExpandedBounds(ASControlNode *controlNode);
  61. @end
  62. @implementation ASControlNode
  63. {
  64. ASImageNode *_debugHighlightOverlay;
  65. }
  66. #pragma mark - Lifecycle
  67. - (instancetype)init
  68. {
  69. if (!(self = [super init]))
  70. return nil;
  71. _enabled = YES;
  72. // As we have no targets yet, we start off with user interaction off. When a target is added, it'll get turned back on.
  73. self.userInteractionEnabled = NO;
  74. return self;
  75. }
  76. #if TARGET_OS_TV
  77. - (void)didLoad
  78. {
  79. // On tvOS all controls, such as buttons, interact with the focus system even if they don't have a target set on them.
  80. // Here we add our own internal tap gesture to handle this behaviour.
  81. self.userInteractionEnabled = YES;
  82. UITapGestureRecognizer *tapGestureRec = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(pressDown)];
  83. tapGestureRec.allowedPressTypes = @[@(UIPressTypeSelect)];
  84. [self.view addGestureRecognizer:tapGestureRec];
  85. }
  86. #endif
  87. - (void)setUserInteractionEnabled:(BOOL)userInteractionEnabled
  88. {
  89. [super setUserInteractionEnabled:userInteractionEnabled];
  90. self.isAccessibilityElement = userInteractionEnabled;
  91. }
  92. #pragma clang diagnostic push
  93. #pragma clang diagnostic ignored "-Wobjc-missing-super-calls"
  94. #pragma mark - ASDisplayNode Overrides
  95. - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
  96. {
  97. // If we're not interested in touches, we have nothing to do.
  98. if (!self.enabled)
  99. return;
  100. ASControlNodeEvent controlEventMask = 0;
  101. // If we get more than one touch down on us, cancel.
  102. // Additionally, if we're already tracking a touch, a second touch beginning is cause for cancellation.
  103. if ([touches count] > 1 || self.tracking)
  104. {
  105. self.tracking = NO;
  106. self.touchInside = NO;
  107. [self cancelTrackingWithEvent:event];
  108. controlEventMask |= ASControlNodeEventTouchCancel;
  109. }
  110. else
  111. {
  112. // Otherwise, begin tracking.
  113. self.tracking = YES;
  114. // No need to check bounds on touchesBegan as we wouldn't get the call if it wasn't in our bounds.
  115. self.touchInside = YES;
  116. self.highlighted = YES;
  117. UITouch *theTouch = [touches anyObject];
  118. [self beginTrackingWithTouch:theTouch withEvent:event];
  119. // Send the appropriate touch-down control event depending on how many times we've been tapped.
  120. controlEventMask |= (theTouch.tapCount == 1) ? ASControlNodeEventTouchDown : ASControlNodeEventTouchDownRepeat;
  121. }
  122. [self sendActionsForControlEvents:controlEventMask withEvent:event];
  123. }
  124. - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
  125. {
  126. // If we're not interested in touches, we have nothing to do.
  127. if (!self.enabled)
  128. return;
  129. NSParameterAssert([touches count] == 1);
  130. UITouch *theTouch = [touches anyObject];
  131. CGPoint touchLocation = [theTouch locationInView:self.view];
  132. // Update our touchInside state.
  133. BOOL dragIsInsideBounds = [self pointInside:touchLocation withEvent:nil];
  134. // Update our highlighted state.
  135. CGRect expandedBounds = _ASControlNodeGetExpandedBounds(self);
  136. BOOL dragIsInsideExpandedBounds = CGRectContainsPoint(expandedBounds, touchLocation);
  137. self.touchInside = dragIsInsideExpandedBounds;
  138. self.highlighted = dragIsInsideExpandedBounds;
  139. // Note we are continuing to track the touch.
  140. [self continueTrackingWithTouch:theTouch withEvent:event];
  141. [self sendActionsForControlEvents:(dragIsInsideBounds ? ASControlNodeEventTouchDragInside : ASControlNodeEventTouchDragOutside)
  142. withEvent:event];
  143. }
  144. - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
  145. {
  146. // If we're not interested in touches, we have nothing to do.
  147. if (!self.enabled)
  148. return;
  149. // We're no longer tracking and there is no touch to be inside.
  150. self.tracking = NO;
  151. self.touchInside = NO;
  152. self.highlighted = NO;
  153. // Note that we've cancelled tracking.
  154. [self cancelTrackingWithEvent:event];
  155. // Send the cancel event.
  156. [self sendActionsForControlEvents:ASControlNodeEventTouchCancel
  157. withEvent:event];
  158. }
  159. - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
  160. {
  161. // If we're not interested in touches, we have nothing to do.
  162. if (!self.enabled)
  163. return;
  164. // On iPhone 6s, iOS 9.2 (and maybe other versions) sometimes calls -touchesEnded:withEvent:
  165. // twice on the view for one call to -touchesBegan:withEvent:. On ASControlNode, it used to
  166. // trigger an action twice unintentionally. Now, we ignore that event if we're not in a tracking
  167. // state in order to have a correct behavior.
  168. // It might be related to that issue: http://www.openradar.me/22910171
  169. if (!self.tracking)
  170. return;
  171. NSParameterAssert([touches count] == 1);
  172. UITouch *theTouch = [touches anyObject];
  173. CGPoint touchLocation = [theTouch locationInView:self.view];
  174. // Update state.
  175. self.tracking = NO;
  176. self.touchInside = NO;
  177. self.highlighted = NO;
  178. // Note that we've ended tracking.
  179. [self endTrackingWithTouch:theTouch withEvent:event];
  180. // Send the appropriate touch-up control event.
  181. CGRect expandedBounds = _ASControlNodeGetExpandedBounds(self);
  182. BOOL touchUpIsInsideExpandedBounds = CGRectContainsPoint(expandedBounds, touchLocation);
  183. [self sendActionsForControlEvents:(touchUpIsInsideExpandedBounds ? ASControlNodeEventTouchUpInside : ASControlNodeEventTouchUpOutside)
  184. withEvent:event];
  185. }
  186. #pragma clang diagnostic pop
  187. - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
  188. {
  189. // If we're interested in touches, this is a tap (the only gesture we care about) and passed -hitTest for us, then no, you may not begin. Sir.
  190. if (self.enabled && [gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]] && gestureRecognizer.view != self.view) {
  191. UITapGestureRecognizer *tapRecognizer = (UITapGestureRecognizer *)gestureRecognizer;
  192. // Allow double-tap gestures
  193. return tapRecognizer.numberOfTapsRequired != 1;
  194. }
  195. // Otherwise, go ahead. :]
  196. return YES;
  197. }
  198. #pragma mark - Action Messages
  199. - (void)addTarget:(id)target action:(SEL)action forControlEvents:(ASControlNodeEvent)controlEventMask
  200. {
  201. NSParameterAssert(action);
  202. NSParameterAssert(controlEventMask != 0);
  203. // This assertion would likely be helpful to users who aren't familiar with the implications of layer-backing.
  204. // However, it would represent an API change (in debug) as it did not used to assert.
  205. // ASDisplayNodeAssert(!self.isLayerBacked, @"ASControlNode is layer backed, will never be able to call target in target:action: pair.");
  206. ASDN::MutexLocker l(_controlLock);
  207. if (!_controlEventDispatchTable) {
  208. _controlEventDispatchTable = [[NSMutableDictionary alloc] initWithCapacity:kASControlNodeEventDispatchTableInitialCapacity]; // enough to handle common types without re-hashing the dictionary when adding entries.
  209. // only show tap-able areas for views with 1 or more addTarget:action: pairs
  210. if ([ASControlNode enableHitTestDebug] && _debugHighlightOverlay == nil) {
  211. ASPerformBlockOnMainThread(^{
  212. // add a highlight overlay node with area of ASControlNode + UIEdgeInsets
  213. self.clipsToBounds = NO;
  214. _debugHighlightOverlay = [[ASImageNode alloc] init];
  215. _debugHighlightOverlay.zPosition = 1000; // ensure we're over the top of any siblings
  216. _debugHighlightOverlay.layerBacked = YES;
  217. [self addSubnode:_debugHighlightOverlay];
  218. });
  219. }
  220. }
  221. // Create new target action pair
  222. ASControlTargetAction *targetAction = [[ASControlTargetAction alloc] init];
  223. targetAction.action = action;
  224. targetAction.target = target;
  225. // Enumerate the events in the mask, adding the target-action pair for each control event included in controlEventMask
  226. _ASEnumerateControlEventsIncludedInMaskWithBlock(controlEventMask, ^
  227. (ASControlNodeEvent controlEvent)
  228. {
  229. // Do we already have an event table for this control event?
  230. id<NSCopying> eventKey = _ASControlNodeEventKeyForControlEvent(controlEvent);
  231. NSMutableArray *eventTargetActionArray = _controlEventDispatchTable[eventKey];
  232. if (!eventTargetActionArray) {
  233. eventTargetActionArray = [[NSMutableArray alloc] init];
  234. }
  235. // Remove any prior target-action pair for this event, as UIKit does.
  236. [eventTargetActionArray removeObject:targetAction];
  237. // Register the new target-action as the last one to be sent.
  238. [eventTargetActionArray addObject:targetAction];
  239. if (eventKey) {
  240. [_controlEventDispatchTable setObject:eventTargetActionArray forKey:eventKey];
  241. }
  242. });
  243. self.userInteractionEnabled = YES;
  244. }
  245. - (NSArray *)actionsForTarget:(id)target forControlEvent:(ASControlNodeEvent)controlEvent
  246. {
  247. NSParameterAssert(target);
  248. NSParameterAssert(controlEvent != 0 && controlEvent != ASControlNodeEventAllEvents);
  249. ASDN::MutexLocker l(_controlLock);
  250. // Grab the event target action array for this event.
  251. NSMutableArray *eventTargetActionArray = _controlEventDispatchTable[_ASControlNodeEventKeyForControlEvent(controlEvent)];
  252. if (!eventTargetActionArray) {
  253. return nil;
  254. }
  255. NSMutableArray *actions = [[NSMutableArray alloc] init];
  256. // Collect all actions for this target.
  257. for (ASControlTargetAction *targetAction in eventTargetActionArray) {
  258. if ((target == nil && targetAction.createdWithNoTarget) || (target != nil && target == targetAction.target)) {
  259. [actions addObject:NSStringFromSelector(targetAction.action)];
  260. }
  261. }
  262. return actions;
  263. }
  264. - (NSSet *)allTargets
  265. {
  266. ASDN::MutexLocker l(_controlLock);
  267. NSMutableSet *targets = [[NSMutableSet alloc] init];
  268. // Look at each event...
  269. for (NSMutableArray *eventTargetActionArray in [_controlEventDispatchTable objectEnumerator]) {
  270. // and each event's targets...
  271. for (ASControlTargetAction *targetAction in eventTargetActionArray) {
  272. [targets addObject:targetAction.target];
  273. }
  274. }
  275. return targets;
  276. }
  277. - (void)removeTarget:(id)target action:(SEL)action forControlEvents:(ASControlNodeEvent)controlEventMask
  278. {
  279. NSParameterAssert(controlEventMask != 0);
  280. ASDN::MutexLocker l(_controlLock);
  281. // Enumerate the events in the mask, removing the target-action pair for each control event included in controlEventMask.
  282. _ASEnumerateControlEventsIncludedInMaskWithBlock(controlEventMask, ^
  283. (ASControlNodeEvent controlEvent)
  284. {
  285. // Grab the dispatch table for this event (if we have it).
  286. id<NSCopying> eventKey = _ASControlNodeEventKeyForControlEvent(controlEvent);
  287. NSMutableArray *eventTargetActionArray = _controlEventDispatchTable[eventKey];
  288. if (!eventTargetActionArray) {
  289. return;
  290. }
  291. NSPredicate *filterPredicate = [NSPredicate predicateWithBlock:^BOOL(ASControlTargetAction *_Nullable evaluatedObject, NSDictionary<NSString *,id> * _Nullable bindings) {
  292. if (!target || evaluatedObject.target == target) {
  293. if (!action) {
  294. return NO;
  295. } else if (evaluatedObject.action == action) {
  296. return NO;
  297. }
  298. }
  299. return YES;
  300. }];
  301. [eventTargetActionArray filterUsingPredicate:filterPredicate];
  302. if (eventTargetActionArray.count == 0) {
  303. // If there are no targets for this event anymore, remove it.
  304. [_controlEventDispatchTable removeObjectForKey:eventKey];
  305. }
  306. });
  307. }
  308. #pragma mark -
  309. - (void)sendActionsForControlEvents:(ASControlNodeEvent)controlEvents withEvent:(UIEvent *)event
  310. {
  311. NSParameterAssert(controlEvents != 0);
  312. ASDN::MutexLocker l(_controlLock);
  313. // Enumerate the events in the mask, invoking the target-action pairs for each.
  314. _ASEnumerateControlEventsIncludedInMaskWithBlock(controlEvents, ^
  315. (ASControlNodeEvent controlEvent)
  316. {
  317. // Use a copy to itereate, the action perform could call remove causing a mutation crash.
  318. NSMutableArray *eventTargetActionArray = [_controlEventDispatchTable[_ASControlNodeEventKeyForControlEvent(controlEvent)] copy];
  319. // Iterate on each target action pair
  320. for (ASControlTargetAction *targetAction in eventTargetActionArray) {
  321. SEL action = targetAction.action;
  322. id responder = targetAction.target;
  323. // NSNull means that a nil target was set, so start at self and travel the responder chain
  324. if (!responder && targetAction.createdWithNoTarget) {
  325. // if the target cannot perform the action, travel the responder chain to try to find something that does
  326. responder = [self.view targetForAction:action withSender:self];
  327. }
  328. #pragma clang diagnostic push
  329. #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
  330. [responder performSelector:action withObject:self withObject:event];
  331. #pragma clang diagnostic pop
  332. }
  333. });
  334. }
  335. #pragma mark - Convenience
  336. id<NSCopying> _ASControlNodeEventKeyForControlEvent(ASControlNodeEvent controlEvent)
  337. {
  338. return @(controlEvent);
  339. }
  340. void _ASEnumerateControlEventsIncludedInMaskWithBlock(ASControlNodeEvent mask, void (^block)(ASControlNodeEvent anEvent))
  341. {
  342. if (block == nil) {
  343. return;
  344. }
  345. // Start with our first event (touch down) and work our way up to the last event (PrimaryActionTriggered)
  346. for (ASControlNodeEvent thisEvent = ASControlNodeEventTouchDown; thisEvent <= ASControlNodeEventPrimaryActionTriggered; thisEvent <<= 1) {
  347. // If it's included in the mask, invoke the block.
  348. if ((mask & thisEvent) == thisEvent)
  349. block(thisEvent);
  350. }
  351. }
  352. CGRect _ASControlNodeGetExpandedBounds(ASControlNode *controlNode) {
  353. return CGRectInset(UIEdgeInsetsInsetRect(controlNode.view.bounds, controlNode.hitTestSlop), kASControlNodeExpandedInset, kASControlNodeExpandedInset);
  354. }
  355. #pragma mark - For Subclasses
  356. - (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)touchEvent
  357. {
  358. return YES;
  359. }
  360. - (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)touchEvent
  361. {
  362. return YES;
  363. }
  364. - (void)cancelTrackingWithEvent:(UIEvent *)touchEvent
  365. {
  366. }
  367. - (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)touchEvent
  368. {
  369. }
  370. #pragma mark - Debug
  371. - (ASImageNode *)debugHighlightOverlay
  372. {
  373. return _debugHighlightOverlay;
  374. }
  375. // methods for visualizing ASLayoutSpecs
  376. - (void)setHierarchyState:(ASHierarchyState)hierarchyState
  377. {
  378. [super setHierarchyState:hierarchyState];
  379. if (self.shouldVisualizeLayoutSpecs) {
  380. [self addTarget:self action:@selector(inspectElement) forControlEvents:ASControlNodeEventTouchUpInside];
  381. } else {
  382. [self removeTarget:self action:@selector(inspectElement) forControlEvents:ASControlNodeEventTouchUpInside];
  383. }
  384. }
  385. - (void)inspectElement
  386. {
  387. [ASLayoutElementInspectorNode sharedInstance].layoutElementToEdit = self;
  388. }
  389. @end