mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
500 lines
17 KiB
Plaintext
500 lines
17 KiB
Plaintext
//
|
|
// ASControlNode.mm
|
|
// Texture
|
|
//
|
|
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
|
|
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
|
|
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
|
|
#import <AsyncDisplayKit/ASControlNode.h>
|
|
#import "ASControlNode+Private.h"
|
|
#import <AsyncDisplayKit/ASControlNode+Subclasses.h>
|
|
#import <AsyncDisplayKit/ASDisplayNode+Subclasses.h>
|
|
#import <AsyncDisplayKit/ASInternalHelpers.h>
|
|
#import <AsyncDisplayKit/ASControlTargetAction.h>
|
|
#import <AsyncDisplayKit/ASDisplayNode+FrameworkPrivate.h>
|
|
#import <AsyncDisplayKit/ASThread.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
|
|
// so replicate that effect with the same values here for our own controls.
|
|
#define kASControlNodeExpandedInset (([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) ? -25.0f : -70.0f)
|
|
|
|
// Initial capacities for dispatch tables.
|
|
#define kASControlNodeEventDispatchTableInitialCapacity 4
|
|
#define kASControlNodeActionDispatchTableInitialCapacity 4
|
|
|
|
@interface ASControlNode ()
|
|
{
|
|
@private
|
|
// Control Attributes
|
|
BOOL _enabled;
|
|
BOOL _highlighted;
|
|
|
|
// Tracking
|
|
BOOL _tracking;
|
|
BOOL _touchInside;
|
|
|
|
// Target action pairs stored in an array for each event type
|
|
// ASControlEvent -> [ASTargetAction0, ASTargetAction1]
|
|
NSMutableDictionary<id<NSCopying>, NSMutableArray<ASControlTargetAction *> *> *_controlEventDispatchTable;
|
|
}
|
|
|
|
// Read-write overrides.
|
|
@property (getter=isTracking) BOOL tracking;
|
|
@property (getter=isTouchInside) BOOL touchInside;
|
|
|
|
/**
|
|
@abstract Returns a key to be used in _controlEventDispatchTable that identifies the control event.
|
|
@param controlEvent A control event.
|
|
@result A key for use in _controlEventDispatchTable.
|
|
*/
|
|
id<NSCopying> _ASControlNodeEventKeyForControlEvent(ASControlNodeEvent controlEvent);
|
|
|
|
/**
|
|
@abstract Enumerates the ASControlNode events included mask, invoking the block for each event.
|
|
@param mask An ASControlNodeEvent mask.
|
|
@param block The block to be invoked for each ASControlNodeEvent included in mask.
|
|
*/
|
|
void _ASEnumerateControlEventsIncludedInMaskWithBlock(ASControlNodeEvent mask, void (^block)(ASControlNodeEvent anEvent));
|
|
|
|
/**
|
|
@abstract Returns the expanded bounds used to determine if a touch is considered 'inside' during tracking.
|
|
@param controlNode A control node.
|
|
@result The expanded bounds of the node.
|
|
*/
|
|
CGRect _ASControlNodeGetExpandedBounds(ASControlNode *controlNode);
|
|
|
|
|
|
@end
|
|
|
|
@implementation ASControlNode
|
|
{
|
|
}
|
|
|
|
#pragma mark - Lifecycle
|
|
|
|
- (instancetype)init
|
|
{
|
|
if (!(self = [super init]))
|
|
return nil;
|
|
|
|
_enabled = YES;
|
|
|
|
// As we have no targets yet, we start off with user interaction off. When a target is added, it'll get turned back on.
|
|
self.userInteractionEnabled = NO;
|
|
|
|
return self;
|
|
}
|
|
|
|
#if TARGET_OS_TV
|
|
- (void)didLoad
|
|
{
|
|
[super didLoad];
|
|
|
|
// On tvOS all controls, such as buttons, interact with the focus system even if they don't have a target set on them.
|
|
// Here we add our own internal tap gesture to handle this behaviour.
|
|
self.userInteractionEnabled = YES;
|
|
UITapGestureRecognizer *tapGestureRec = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(_pressDown)];
|
|
tapGestureRec.allowedPressTypes = @[@(UIPressTypeSelect)];
|
|
[self.view addGestureRecognizer:tapGestureRec];
|
|
}
|
|
#endif
|
|
|
|
- (void)setUserInteractionEnabled:(BOOL)userInteractionEnabled
|
|
{
|
|
[super setUserInteractionEnabled:userInteractionEnabled];
|
|
self.isAccessibilityElement = userInteractionEnabled;
|
|
}
|
|
|
|
- (void)__exitHierarchy
|
|
{
|
|
[super __exitHierarchy];
|
|
|
|
// If a control node is exit the hierarchy and is tracking we have to cancel it
|
|
if (self.tracking) {
|
|
[self _cancelTrackingWithEvent:nil];
|
|
}
|
|
}
|
|
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wobjc-missing-super-calls"
|
|
|
|
#pragma mark - ASDisplayNode Overrides
|
|
|
|
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
|
|
{
|
|
// If we're not interested in touches, we have nothing to do.
|
|
if (!self.enabled) {
|
|
return;
|
|
}
|
|
|
|
// Check if the tracking should start
|
|
UITouch *theTouch = [touches anyObject];
|
|
if (![self beginTrackingWithTouch:theTouch withEvent:event]) {
|
|
return;
|
|
}
|
|
|
|
// If we get more than one touch down on us, cancel.
|
|
// Additionally, if we're already tracking a touch, a second touch beginning is cause for cancellation.
|
|
if (touches.count > 1 || self.tracking) {
|
|
[self _cancelTrackingWithEvent:event];
|
|
} else {
|
|
// Otherwise, begin tracking.
|
|
self.tracking = YES;
|
|
|
|
// No need to check bounds on touchesBegan as we wouldn't get the call if it wasn't in our bounds.
|
|
self.touchInside = YES;
|
|
self.highlighted = YES;
|
|
|
|
// Send the appropriate touch-down control event depending on how many times we've been tapped.
|
|
ASControlNodeEvent controlEventMask = (theTouch.tapCount == 1) ? ASControlNodeEventTouchDown : ASControlNodeEventTouchDownRepeat;
|
|
[self sendActionsForControlEvents:controlEventMask withEvent:event];
|
|
}
|
|
}
|
|
|
|
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
|
|
{
|
|
// If we're not interested in touches, we have nothing to do.
|
|
if (!self.enabled) {
|
|
return;
|
|
}
|
|
|
|
NSParameterAssert(touches.count == 1);
|
|
UITouch *theTouch = [touches anyObject];
|
|
|
|
// Check if tracking should continue
|
|
if (!self.tracking || ![self continueTrackingWithTouch:theTouch withEvent:event]) {
|
|
self.tracking = NO;
|
|
return;
|
|
}
|
|
|
|
CGPoint touchLocation = [theTouch locationInView:self.view];
|
|
|
|
// Update our touchInside state.
|
|
BOOL dragIsInsideBounds = [self pointInside:touchLocation withEvent:nil];
|
|
|
|
// Update our highlighted state.
|
|
CGRect expandedBounds = _ASControlNodeGetExpandedBounds(self);
|
|
BOOL dragIsInsideExpandedBounds = CGRectContainsPoint(expandedBounds, touchLocation);
|
|
self.touchInside = dragIsInsideExpandedBounds;
|
|
self.highlighted = dragIsInsideExpandedBounds;
|
|
|
|
[self sendActionsForControlEvents:(dragIsInsideBounds ? ASControlNodeEventTouchDragInside : ASControlNodeEventTouchDragOutside)
|
|
withEvent:event];
|
|
}
|
|
|
|
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
|
|
{
|
|
// If we're not interested in touches, we have nothing to do.
|
|
if (!self.enabled) {
|
|
return;
|
|
}
|
|
|
|
// Note that we've cancelled tracking.
|
|
[self _cancelTrackingWithEvent:event];
|
|
}
|
|
|
|
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
|
|
{
|
|
// If we're not interested in touches, we have nothing to do.
|
|
if (!self.enabled) {
|
|
return;
|
|
}
|
|
|
|
// On iPhone 6s, iOS 9.2 (and maybe other versions) sometimes calls -touchesEnded:withEvent:
|
|
// twice on the view for one call to -touchesBegan:withEvent:. On ASControlNode, it used to
|
|
// trigger an action twice unintentionally. Now, we ignore that event if we're not in a tracking
|
|
// state in order to have a correct behavior.
|
|
// It might be related to that issue: http://www.openradar.me/22910171
|
|
if (!self.tracking) {
|
|
return;
|
|
}
|
|
|
|
NSParameterAssert([touches count] == 1);
|
|
UITouch *theTouch = [touches anyObject];
|
|
CGPoint touchLocation = [theTouch locationInView:self.view];
|
|
|
|
// Update state.
|
|
self.tracking = NO;
|
|
self.touchInside = NO;
|
|
self.highlighted = NO;
|
|
|
|
// Note that we've ended tracking.
|
|
[self endTrackingWithTouch:theTouch withEvent:event];
|
|
|
|
// Send the appropriate touch-up control event.
|
|
CGRect expandedBounds = _ASControlNodeGetExpandedBounds(self);
|
|
BOOL touchUpIsInsideExpandedBounds = CGRectContainsPoint(expandedBounds, touchLocation);
|
|
|
|
[self sendActionsForControlEvents:(touchUpIsInsideExpandedBounds ? ASControlNodeEventTouchUpInside : ASControlNodeEventTouchUpOutside)
|
|
withEvent:event];
|
|
}
|
|
|
|
- (void)_cancelTrackingWithEvent:(UIEvent *)event
|
|
{
|
|
// We're no longer tracking and there is no touch to be inside.
|
|
self.tracking = NO;
|
|
self.touchInside = NO;
|
|
self.highlighted = NO;
|
|
|
|
// Send the cancel event.
|
|
[self sendActionsForControlEvents:ASControlNodeEventTouchCancel withEvent:event];
|
|
}
|
|
|
|
#pragma clang diagnostic pop
|
|
|
|
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
|
|
{
|
|
ASDisplayNodeAssertMainThread();
|
|
|
|
// If not enabled we should not care about receving touches
|
|
if (! self.enabled) {
|
|
return nil;
|
|
}
|
|
|
|
return [super hitTest:point withEvent:event];
|
|
}
|
|
|
|
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
|
|
{
|
|
// 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.
|
|
if (self.enabled && [gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]] && gestureRecognizer.view != self.view) {
|
|
UITapGestureRecognizer *tapRecognizer = (UITapGestureRecognizer *)gestureRecognizer;
|
|
// Allow double-tap gestures
|
|
return tapRecognizer.numberOfTapsRequired != 1;
|
|
}
|
|
|
|
// Otherwise, go ahead. :]
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)supportsLayerBacking
|
|
{
|
|
return super.supportsLayerBacking && !self.userInteractionEnabled;
|
|
}
|
|
|
|
#pragma mark - Action Messages
|
|
|
|
- (void)addTarget:(id)target action:(SEL)action forControlEvents:(ASControlNodeEvent)controlEventMask
|
|
{
|
|
NSParameterAssert(action);
|
|
NSParameterAssert(controlEventMask != 0);
|
|
|
|
// ASControlNode cannot be layer backed if adding a target
|
|
ASDisplayNodeAssert(!self.isLayerBacked, @"ASControlNode is layer backed, will never be able to call target in target:action: pair.");
|
|
|
|
ASLockScopeSelf();
|
|
|
|
if (!_controlEventDispatchTable) {
|
|
_controlEventDispatchTable = [[NSMutableDictionary alloc] initWithCapacity:kASControlNodeEventDispatchTableInitialCapacity]; // enough to handle common types without re-hashing the dictionary when adding entries.
|
|
}
|
|
|
|
// Create new target action pair
|
|
ASControlTargetAction *targetAction = [[ASControlTargetAction alloc] init];
|
|
targetAction.action = action;
|
|
targetAction.target = target;
|
|
|
|
// Enumerate the events in the mask, adding the target-action pair for each control event included in controlEventMask
|
|
_ASEnumerateControlEventsIncludedInMaskWithBlock(controlEventMask, ^
|
|
(ASControlNodeEvent controlEvent)
|
|
{
|
|
// Do we already have an event table for this control event?
|
|
id<NSCopying> eventKey = _ASControlNodeEventKeyForControlEvent(controlEvent);
|
|
NSMutableArray *eventTargetActionArray = _controlEventDispatchTable[eventKey];
|
|
|
|
if (!eventTargetActionArray) {
|
|
eventTargetActionArray = [[NSMutableArray alloc] init];
|
|
}
|
|
|
|
// Remove any prior target-action pair for this event, as UIKit does.
|
|
[eventTargetActionArray removeObject:targetAction];
|
|
|
|
// Register the new target-action as the last one to be sent.
|
|
[eventTargetActionArray addObject:targetAction];
|
|
|
|
if (eventKey) {
|
|
[_controlEventDispatchTable setObject:eventTargetActionArray forKey:eventKey];
|
|
}
|
|
});
|
|
|
|
self.userInteractionEnabled = YES;
|
|
}
|
|
|
|
- (NSArray *)actionsForTarget:(id)target forControlEvent:(ASControlNodeEvent)controlEvent
|
|
{
|
|
NSParameterAssert(target);
|
|
NSParameterAssert(controlEvent != 0 && controlEvent != ASControlNodeEventAllEvents);
|
|
|
|
ASLockScopeSelf();
|
|
|
|
// Grab the event target action array for this event.
|
|
NSMutableArray *eventTargetActionArray = _controlEventDispatchTable[_ASControlNodeEventKeyForControlEvent(controlEvent)];
|
|
if (!eventTargetActionArray) {
|
|
return nil;
|
|
}
|
|
|
|
NSMutableArray *actions = [[NSMutableArray alloc] init];
|
|
|
|
// Collect all actions for this target.
|
|
for (ASControlTargetAction *targetAction in eventTargetActionArray) {
|
|
if ((target == nil && targetAction.createdWithNoTarget) || (target != nil && target == targetAction.target)) {
|
|
[actions addObject:NSStringFromSelector(targetAction.action)];
|
|
}
|
|
}
|
|
|
|
return actions;
|
|
}
|
|
|
|
- (NSSet *)allTargets
|
|
{
|
|
ASLockScopeSelf();
|
|
|
|
NSMutableSet *targets = [[NSMutableSet alloc] init];
|
|
|
|
// Look at each event...
|
|
for (NSMutableArray *eventTargetActionArray in [_controlEventDispatchTable objectEnumerator]) {
|
|
// and each event's targets...
|
|
for (ASControlTargetAction *targetAction in eventTargetActionArray) {
|
|
[targets addObject:targetAction.target];
|
|
}
|
|
}
|
|
|
|
return targets;
|
|
}
|
|
|
|
- (void)removeTarget:(id)target action:(SEL)action forControlEvents:(ASControlNodeEvent)controlEventMask
|
|
{
|
|
NSParameterAssert(controlEventMask != 0);
|
|
|
|
ASLockScopeSelf();
|
|
|
|
// Enumerate the events in the mask, removing the target-action pair for each control event included in controlEventMask.
|
|
_ASEnumerateControlEventsIncludedInMaskWithBlock(controlEventMask, ^
|
|
(ASControlNodeEvent controlEvent)
|
|
{
|
|
// Grab the dispatch table for this event (if we have it).
|
|
id<NSCopying> eventKey = _ASControlNodeEventKeyForControlEvent(controlEvent);
|
|
NSMutableArray *eventTargetActionArray = _controlEventDispatchTable[eventKey];
|
|
if (!eventTargetActionArray) {
|
|
return;
|
|
}
|
|
|
|
NSPredicate *filterPredicate = [NSPredicate predicateWithBlock:^BOOL(ASControlTargetAction *_Nullable evaluatedObject, NSDictionary<NSString *,id> * _Nullable bindings) {
|
|
if (!target || evaluatedObject.target == target) {
|
|
if (!action) {
|
|
return NO;
|
|
} else if (evaluatedObject.action == action) {
|
|
return NO;
|
|
}
|
|
}
|
|
|
|
return YES;
|
|
}];
|
|
[eventTargetActionArray filterUsingPredicate:filterPredicate];
|
|
|
|
if (eventTargetActionArray.count == 0) {
|
|
// If there are no targets for this event anymore, remove it.
|
|
[_controlEventDispatchTable removeObjectForKey:eventKey];
|
|
}
|
|
});
|
|
}
|
|
|
|
#pragma mark -
|
|
|
|
- (void)sendActionsForControlEvents:(ASControlNodeEvent)controlEvents withEvent:(UIEvent *)event
|
|
{
|
|
ASDisplayNodeAssertMainThread(); //We access self.view below, it's not safe to call this off of main.
|
|
NSParameterAssert(controlEvents != 0);
|
|
|
|
NSMutableArray *resolvedEventTargetActionArray = [[NSMutableArray<ASControlTargetAction *> alloc] init];
|
|
|
|
{
|
|
ASLockScopeSelf();
|
|
|
|
// Enumerate the events in the mask, invoking the target-action pairs for each.
|
|
_ASEnumerateControlEventsIncludedInMaskWithBlock(controlEvents, ^
|
|
(ASControlNodeEvent controlEvent)
|
|
{
|
|
// Iterate on each target action pair
|
|
for (ASControlTargetAction *targetAction in _controlEventDispatchTable[_ASControlNodeEventKeyForControlEvent(controlEvent)]) {
|
|
ASControlTargetAction *resolvedTargetAction = [[ASControlTargetAction alloc] init];
|
|
resolvedTargetAction.action = targetAction.action;
|
|
resolvedTargetAction.target = targetAction.target;
|
|
|
|
// NSNull means that a nil target was set, so start at self and travel the responder chain
|
|
if (!resolvedTargetAction.target && targetAction.createdWithNoTarget) {
|
|
// if the target cannot perform the action, travel the responder chain to try to find something that does
|
|
resolvedTargetAction.target = [self.view targetForAction:resolvedTargetAction.action withSender:self];
|
|
}
|
|
|
|
if (resolvedTargetAction.target) {
|
|
[resolvedEventTargetActionArray addObject:resolvedTargetAction];
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
//We don't want to hold the lock while calling out, we could potentially walk up the ownership tree causing a deadlock.
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
|
for (ASControlTargetAction *targetAction in resolvedEventTargetActionArray) {
|
|
[targetAction.target performSelector:targetAction.action withObject:self withObject:event];
|
|
}
|
|
#pragma clang diagnostic pop
|
|
}
|
|
|
|
#pragma mark - Convenience
|
|
|
|
id<NSCopying> _ASControlNodeEventKeyForControlEvent(ASControlNodeEvent controlEvent)
|
|
{
|
|
return @(controlEvent);
|
|
}
|
|
|
|
void _ASEnumerateControlEventsIncludedInMaskWithBlock(ASControlNodeEvent mask, void (^block)(ASControlNodeEvent anEvent))
|
|
{
|
|
if (block == nil) {
|
|
return;
|
|
}
|
|
// Start with our first event (touch down) and work our way up to the last event (PrimaryActionTriggered)
|
|
for (ASControlNodeEvent thisEvent = ASControlNodeEventTouchDown; thisEvent <= ASControlNodeEventPrimaryActionTriggered; thisEvent <<= 1) {
|
|
// If it's included in the mask, invoke the block.
|
|
if ((mask & thisEvent) == thisEvent)
|
|
block(thisEvent);
|
|
}
|
|
}
|
|
|
|
CGRect _ASControlNodeGetExpandedBounds(ASControlNode *controlNode) {
|
|
return CGRectInset(UIEdgeInsetsInsetRect(controlNode.view.bounds, controlNode.hitTestSlop), kASControlNodeExpandedInset, kASControlNodeExpandedInset);
|
|
}
|
|
|
|
#pragma mark - For Subclasses
|
|
|
|
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)touchEvent
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)touchEvent
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
- (void)cancelTrackingWithEvent:(UIEvent *)touchEvent
|
|
{
|
|
// Subclass hook
|
|
}
|
|
|
|
- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)touchEvent
|
|
{
|
|
// Subclass hook
|
|
}
|
|
|
|
#pragma mark - Debug
|
|
- (ASDisplayNode *)debugHighlightOverlay
|
|
{
|
|
return nil;
|
|
}
|
|
@end
|