[tvOS] Added a default touch down animation for ASControlNode

This commit is contained in:
Aaron Schubert 2016-02-23 12:18:33 +00:00
parent 4ad6d91a10
commit d9cde1f08c
2 changed files with 99 additions and 64 deletions

View File

@ -30,6 +30,9 @@ typedef NS_OPTIONS(NSUInteger, ASControlNodeEvent)
ASControlNodeEventTouchUpOutside = 1 << 5, ASControlNodeEventTouchUpOutside = 1 << 5,
/** A system event canceling the current touches for the control node. */ /** A system event canceling the current touches for the control node. */
ASControlNodeEventTouchCancel = 1 << 6, ASControlNodeEventTouchCancel = 1 << 6,
/** A system event when the Play/Pause button on the Apple TV remote is pressed. */
ASControlNodeEventPrimaryActionTriggered = 1 << 13,
/** All events, including system events. */ /** All events, including system events. */
ASControlNodeEventAllEvents = 0xFFFFFFFF ASControlNodeEventAllEvents = 0xFFFFFFFF
}; };

View File

@ -27,11 +27,11 @@
// Control Attributes // Control Attributes
BOOL _enabled; BOOL _enabled;
BOOL _highlighted; BOOL _highlighted;
// Tracking // Tracking
BOOL _tracking; BOOL _tracking;
BOOL _touchInside; BOOL _touchInside;
// Target Messages. // Target Messages.
/* /*
The table structure is as follows: The table structure is as follows:
@ -77,26 +77,35 @@ void _ASEnumerateControlEventsIncludedInMaskWithBlock(ASControlNodeEvent mask, v
{ {
if (!(self = [super init])) if (!(self = [super init]))
return nil; return nil;
_enabled = YES; _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. // 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; self.userInteractionEnabled = NO;
#if TARGET_OS_TV
[self addTarget:self action:@selector(updateUI) forControlEvents:ASControlNodeEventAllEvents];
#endif
return self; return self;
} }
- (void)didLoad
{
#if TARGET_OS_TV
// [self addTarget:self action:@selector(updateUI) forControlEvents:ASControlNodeEventPrimaryActionTriggered];
self.userInteractionEnabled = YES;
UITapGestureRecognizer *tapGestureRec = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(pressDown)];
tapGestureRec.allowedPressTypes = @[@(UIPressTypeSelect)];
[self.view addGestureRecognizer:tapGestureRec];
#endif
}
#pragma mark - ASDisplayNode Overrides #pragma mark - ASDisplayNode Overrides
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{ {
// If we're not interested in touches, we have nothing to do. // If we're not interested in touches, we have nothing to do.
if (!self.enabled) if (!self.enabled)
return; return;
ASControlNodeEvent controlEventMask = 0; ASControlNodeEvent controlEventMask = 0;
// If we get more than one touch down on us, cancel. // 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. // Additionally, if we're already tracking a touch, a second touch beginning is cause for cancellation.
if ([touches count] > 1 || self.tracking) if ([touches count] > 1 || self.tracking)
@ -110,18 +119,18 @@ void _ASEnumerateControlEventsIncludedInMaskWithBlock(ASControlNodeEvent mask, v
{ {
// Otherwise, begin tracking. // Otherwise, begin tracking.
self.tracking = YES; self.tracking = YES;
// No need to check bounds on touchesBegan as we wouldn't get the call if it wasn't in our bounds. // 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.touchInside = YES;
self.highlighted = YES; self.highlighted = YES;
UITouch *theTouch = [touches anyObject]; UITouch *theTouch = [touches anyObject];
[self beginTrackingWithTouch:theTouch withEvent:event]; [self beginTrackingWithTouch:theTouch withEvent:event];
// Send the appropriate touch-down control event depending on how many times we've been tapped. // Send the appropriate touch-down control event depending on how many times we've been tapped.
controlEventMask |= (theTouch.tapCount == 1) ? ASControlNodeEventTouchDown : ASControlNodeEventTouchDownRepeat; controlEventMask |= (theTouch.tapCount == 1) ? ASControlNodeEventTouchDown : ASControlNodeEventTouchDownRepeat;
} }
[self sendActionsForControlEvents:controlEventMask withEvent:event]; [self sendActionsForControlEvents:controlEventMask withEvent:event];
} }
@ -130,41 +139,42 @@ void _ASEnumerateControlEventsIncludedInMaskWithBlock(ASControlNodeEvent mask, v
// If we're not interested in touches, we have nothing to do. // If we're not interested in touches, we have nothing to do.
if (!self.enabled) if (!self.enabled)
return; return;
NSParameterAssert([touches count] == 1); NSParameterAssert([touches count] == 1);
UITouch *theTouch = [touches anyObject]; UITouch *theTouch = [touches anyObject];
CGPoint touchLocation = [theTouch locationInView:self.view]; CGPoint touchLocation = [theTouch locationInView:self.view];
// Update our touchInside state. // Update our touchInside state.
BOOL dragIsInsideBounds = [self pointInside:touchLocation withEvent:nil]; BOOL dragIsInsideBounds = [self pointInside:touchLocation withEvent:nil];
// Update our highlighted state. // Update our highlighted state.
CGRect expandedBounds = CGRectInset(self.view.bounds, kASControlNodeExpandedInset, kASControlNodeExpandedInset); CGRect expandedBounds = CGRectInset(self.view.bounds, kASControlNodeExpandedInset, kASControlNodeExpandedInset);
BOOL dragIsInsideExpandedBounds = CGRectContainsPoint(expandedBounds, touchLocation); BOOL dragIsInsideExpandedBounds = CGRectContainsPoint(expandedBounds, touchLocation);
self.touchInside = dragIsInsideExpandedBounds; self.touchInside = dragIsInsideExpandedBounds;
self.highlighted = dragIsInsideExpandedBounds; self.highlighted = dragIsInsideExpandedBounds;
// Note we are continuing to track the touch. // Note we are continuing to track the touch.
[self continueTrackingWithTouch:theTouch withEvent:event]; [self continueTrackingWithTouch:theTouch withEvent:event];
[self sendActionsForControlEvents:(dragIsInsideBounds ? ASControlNodeEventTouchDragInside : ASControlNodeEventTouchDragOutside) [self sendActionsForControlEvents:(dragIsInsideBounds ? ASControlNodeEventTouchDragInside : ASControlNodeEventTouchDragOutside)
withEvent:event]; withEvent:event];
} }
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{ {
NSLog(@"Touches Cancelled");
// If we're not interested in touches, we have nothing to do. // If we're not interested in touches, we have nothing to do.
if (!self.enabled) if (!self.enabled)
return; return;
// We're no longer tracking and there is no touch to be inside. // We're no longer tracking and there is no touch to be inside.
self.tracking = NO; self.tracking = NO;
self.touchInside = NO; self.touchInside = NO;
self.highlighted = NO; self.highlighted = NO;
// Note that we've cancelled tracking. // Note that we've cancelled tracking.
[self cancelTrackingWithEvent:event]; [self cancelTrackingWithEvent:event];
// Send the cancel event. // Send the cancel event.
[self sendActionsForControlEvents:ASControlNodeEventTouchCancel [self sendActionsForControlEvents:ASControlNodeEventTouchCancel
withEvent:event]; withEvent:event];
@ -175,7 +185,7 @@ void _ASEnumerateControlEventsIncludedInMaskWithBlock(ASControlNodeEvent mask, v
// If we're not interested in touches, we have nothing to do. // If we're not interested in touches, we have nothing to do.
if (!self.enabled) if (!self.enabled)
return; return;
// On iPhone 6s, iOS 9.2 (and maybe other versions) sometimes calls -touchesEnded:withEvent: // 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 // 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 // trigger an action twice unintentionally. Now, we ignore that event if we're not in a tracking
@ -183,23 +193,23 @@ void _ASEnumerateControlEventsIncludedInMaskWithBlock(ASControlNodeEvent mask, v
// It might be related to that issue: http://www.openradar.me/22910171 // It might be related to that issue: http://www.openradar.me/22910171
if (!self.tracking) if (!self.tracking)
return; return;
NSParameterAssert([touches count] == 1); NSParameterAssert([touches count] == 1);
UITouch *theTouch = [touches anyObject]; UITouch *theTouch = [touches anyObject];
CGPoint touchLocation = [theTouch locationInView:self.view]; CGPoint touchLocation = [theTouch locationInView:self.view];
// Update state. // Update state.
self.tracking = NO; self.tracking = NO;
self.touchInside = NO; self.touchInside = NO;
self.highlighted = NO; self.highlighted = NO;
// Note that we've ended tracking. // Note that we've ended tracking.
[self endTrackingWithTouch:theTouch withEvent:event]; [self endTrackingWithTouch:theTouch withEvent:event];
// Send the appropriate touch-up control event. // Send the appropriate touch-up control event.
CGRect expandedBounds = CGRectInset(self.view.bounds, kASControlNodeExpandedInset, kASControlNodeExpandedInset); CGRect expandedBounds = CGRectInset(self.view.bounds, kASControlNodeExpandedInset, kASControlNodeExpandedInset);
BOOL touchUpIsInsideExpandedBounds = CGRectContainsPoint(expandedBounds, touchLocation); BOOL touchUpIsInsideExpandedBounds = CGRectContainsPoint(expandedBounds, touchLocation);
[self sendActionsForControlEvents:(touchUpIsInsideExpandedBounds ? ASControlNodeEventTouchUpInside : ASControlNodeEventTouchUpOutside) [self sendActionsForControlEvents:(touchUpIsInsideExpandedBounds ? ASControlNodeEventTouchUpInside : ASControlNodeEventTouchUpOutside)
withEvent:event]; withEvent:event];
} }
@ -212,7 +222,7 @@ void _ASEnumerateControlEventsIncludedInMaskWithBlock(ASControlNodeEvent mask, v
// Allow double-tap gestures // Allow double-tap gestures
return tapRecognizer.numberOfTapsRequired != 1; return tapRecognizer.numberOfTapsRequired != 1;
} }
// Otherwise, go ahead. :] // Otherwise, go ahead. :]
return YES; return YES;
} }
@ -228,11 +238,11 @@ void _ASEnumerateControlEventsIncludedInMaskWithBlock(ASControlNodeEvent mask, v
// Convert nil to [NSNull null] so that it can be used as a key for NSMapTable. // Convert nil to [NSNull null] so that it can be used as a key for NSMapTable.
if (!target) if (!target)
target = [NSNull null]; target = [NSNull null];
if (!_controlEventDispatchTable) { if (!_controlEventDispatchTable) {
_controlEventDispatchTable = [[NSMutableDictionary alloc] initWithCapacity:kASControlNodeEventDispatchTableInitialCapacity]; // enough to handle common types without re-hashing the dictionary when adding entries. _controlEventDispatchTable = [[NSMutableDictionary alloc] initWithCapacity:kASControlNodeEventDispatchTableInitialCapacity]; // enough to handle common types without re-hashing the dictionary when adding entries.
} }
// Enumerate the events in the mask, adding the target-action pair for each control event included in controlEventMask // Enumerate the events in the mask, adding the target-action pair for each control event included in controlEventMask
_ASEnumerateControlEventsIncludedInMaskWithBlock(controlEventMask, ^ _ASEnumerateControlEventsIncludedInMaskWithBlock(controlEventMask, ^
(ASControlNodeEvent controlEvent) (ASControlNodeEvent controlEvent)
@ -269,14 +279,14 @@ void _ASEnumerateControlEventsIncludedInMaskWithBlock(ASControlNodeEvent mask, v
{ {
NSParameterAssert(target); NSParameterAssert(target);
NSParameterAssert(controlEvent != 0 && controlEvent != ASControlNodeEventAllEvents); NSParameterAssert(controlEvent != 0 && controlEvent != ASControlNodeEventAllEvents);
ASDN::MutexLocker l(_controlLock); ASDN::MutexLocker l(_controlLock);
// Grab the event dispatch table for this event. // Grab the event dispatch table for this event.
NSMapTable *eventDispatchTable = [_controlEventDispatchTable objectForKey:_ASControlNodeEventKeyForControlEvent(controlEvent)]; NSMapTable *eventDispatchTable = [_controlEventDispatchTable objectForKey:_ASControlNodeEventKeyForControlEvent(controlEvent)];
if (!eventDispatchTable) if (!eventDispatchTable)
return nil; return nil;
// Return the actions for this target. // Return the actions for this target.
return [eventDispatchTable objectForKey:target]; return [eventDispatchTable objectForKey:target];
} }
@ -286,7 +296,7 @@ void _ASEnumerateControlEventsIncludedInMaskWithBlock(ASControlNodeEvent mask, v
ASDN::MutexLocker l(_controlLock); ASDN::MutexLocker l(_controlLock);
NSMutableSet *targets = [[NSMutableSet alloc] init]; NSMutableSet *targets = [[NSMutableSet alloc] init];
// Look at each event... // Look at each event...
for (NSMapTable *eventDispatchTable in [_controlEventDispatchTable allValues]) for (NSMapTable *eventDispatchTable in [_controlEventDispatchTable allValues])
{ {
@ -294,7 +304,7 @@ void _ASEnumerateControlEventsIncludedInMaskWithBlock(ASControlNodeEvent mask, v
for (id target in eventDispatchTable) for (id target in eventDispatchTable)
[targets addObject:target]; [targets addObject:target];
} }
return targets; return targets;
} }
@ -303,7 +313,7 @@ void _ASEnumerateControlEventsIncludedInMaskWithBlock(ASControlNodeEvent mask, v
NSParameterAssert(controlEventMask != 0); NSParameterAssert(controlEventMask != 0);
ASDN::MutexLocker l(_controlLock); ASDN::MutexLocker l(_controlLock);
// Enumerate the events in the mask, removing the target-action pair for each control event included in controlEventMask. // Enumerate the events in the mask, removing the target-action pair for each control event included in controlEventMask.
_ASEnumerateControlEventsIncludedInMaskWithBlock(controlEventMask, ^ _ASEnumerateControlEventsIncludedInMaskWithBlock(controlEventMask, ^
(ASControlNodeEvent controlEvent) (ASControlNodeEvent controlEvent)
@ -357,7 +367,7 @@ void _ASEnumerateControlEventsIncludedInMaskWithBlock(ASControlNodeEvent mask, v
NSParameterAssert(controlEvents != 0); NSParameterAssert(controlEvents != 0);
ASDN::MutexLocker l(_controlLock); ASDN::MutexLocker l(_controlLock);
// Enumerate the events in the mask, invoking the target-action pairs for each. // Enumerate the events in the mask, invoking the target-action pairs for each.
_ASEnumerateControlEventsIncludedInMaskWithBlock(controlEvents, ^ _ASEnumerateControlEventsIncludedInMaskWithBlock(controlEvents, ^
(ASControlNodeEvent controlEvent) (ASControlNodeEvent controlEvent)
@ -429,9 +439,17 @@ void _ASEnumerateControlEventsIncludedInMaskWithBlock(ASControlNodeEvent mask, v
} }
#if TARGET_OS_TV #if TARGET_OS_TV
#pragma mark - tvOS #pragma mark - tvOS
- (void)updateUI - (void)pressDown
{ {
NSLog(@"Update UI"); [UIView animateWithDuration:0.1 delay:0 options:UIViewAnimationCurveLinear animations:^{
[self setPressedState];
} completion:^(BOOL finished) {
if (finished) {
[UIView animateWithDuration:0.1 delay:0 options:UIViewAnimationCurveLinear animations:^{
[self setFocusedState];
} completion:nil];
}
}];
} }
- (BOOL)canBecomeFocused - (BOOL)canBecomeFocused
@ -446,33 +464,47 @@ void _ASEnumerateControlEventsIncludedInMaskWithBlock(ASControlNodeEvent mask, v
- (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator - (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator
{ {
NSLog(@"Focused"); if (context.nextFocusedView && context.nextFocusedView == self.view) {
if (context.nextFocusedView && context.nextFocusedView == self.view) { //Focused
//Focused [coordinator addCoordinatedAnimations:^{
[coordinator addCoordinatedAnimations:^{ [self setFocusedState];
self.layer.shadowOffset = CGSizeMake(2, 10); } completion:nil];
self.layer.shadowColor = [UIColor blackColor].CGColor; } else{
self.layer.shadowRadius = 12.0; //Not focused
self.layer.shadowOpacity = 0.45; [coordinator addCoordinatedAnimations:^{
self.layer.shadowPath = [UIBezierPath bezierPathWithRect:self.layer.bounds].CGPath; [self setDefaultState];
self.view.transform = CGAffineTransformScale(CGAffineTransformIdentity, 1.1, 1.1); } completion:nil];
} completion:^{ }
}
}];
} else{
//Not focused
[coordinator addCoordinatedAnimations:^{
self.layer.shadowOffset = CGSizeZero;
self.layer.shadowColor = [UIColor blackColor].CGColor;
self.layer.shadowRadius = 0;
self.layer.shadowOpacity = 0;
self.layer.shadowPath = [UIBezierPath bezierPathWithRect:self.layer.bounds].CGPath;
self.view.transform = CGAffineTransformScale(CGAffineTransformIdentity, 1, 1);
} completion:^{
}];
} - (void)setFocusedState
{
self.layer.shadowOffset = CGSizeMake(2, 10);
self.layer.shadowColor = [UIColor blackColor].CGColor;
self.layer.shadowRadius = 12.0;
self.layer.shadowOpacity = 0.45;
self.layer.shadowPath = [UIBezierPath bezierPathWithRect:self.layer.bounds].CGPath;
self.view.transform = CGAffineTransformScale(CGAffineTransformIdentity, 1.1, 1.1);
}
- (void)setPressedState
{
self.layer.shadowOffset = CGSizeMake(2, 2);
self.layer.shadowColor = [UIColor blackColor].CGColor;
self.layer.shadowRadius = 12.0;
self.layer.shadowOpacity = 0.45;
self.layer.shadowPath = [UIBezierPath bezierPathWithRect:self.layer.bounds].CGPath;
self.view.transform = CGAffineTransformScale(CGAffineTransformIdentity, 1, 1);
}
- (void)setDefaultState
{
self.layer.shadowOffset = CGSizeZero;
self.layer.shadowColor = [UIColor blackColor].CGColor;
self.layer.shadowRadius = 0;
self.layer.shadowOpacity = 0;
self.layer.shadowPath = [UIBezierPath bezierPathWithRect:self.layer.bounds].CGPath;
self.view.transform = CGAffineTransformScale(CGAffineTransformIdentity, 1, 1);
} }
#endif #endif
@end @end