Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Touch Manager Gesture Cancellation #727

1 change: 1 addition & 0 deletions SmartDeviceLink-iOS.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ ss.public_header_files = [
'SmartDeviceLink/SDLPutFileResponse.h',
'SmartDeviceLink/SDLReadDID.h',
'SmartDeviceLink/SDLReadDIDResponse.h',
'SmartDeviceLink/SDLRectangle.h',
'SmartDeviceLink/SDLRegisterAppInterface.h',
'SmartDeviceLink/SDLRegisterAppInterfaceResponse.h',
'SmartDeviceLink/SDLRequestType.h',
Expand Down
1 change: 1 addition & 0 deletions SmartDeviceLink.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ ss.public_header_files = [
'SmartDeviceLink/SDLPutFile.h',
'SmartDeviceLink/SDLPutFileResponse.h',
'SmartDeviceLink/SDLReadDID.h',
'SmartDeviceLink/SDLRectangle.h',
'SmartDeviceLink/SDLReadDIDResponse.h',
'SmartDeviceLink/SDLRegisterAppInterface.h',
'SmartDeviceLink/SDLRegisterAppInterfaceResponse.h',
Expand Down
180 changes: 124 additions & 56 deletions SmartDeviceLink/SDLTouchManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,21 @@ typedef NS_ENUM(NSUInteger, SDLPerformingTouchType) {
};

/*!
* @abstract
* @abstract
* Touch Manager will ignore touches that represent more than 2 fingers on the screen.
*/
static NSUInteger const MaximumNumberOfTouches = 2;

@interface SDLTouchManager ()

/*!
* @abstract
* @abstract
* First Touch received from onOnTouchEvent.
*/
@property (nonatomic, strong, nullable) SDLTouch *previousTouch;

/*!
* @abstract
* @abstract
* Cached previous single tap used for double tap detection.
*/
@property (nonatomic, strong, nullable) SDLTouch *singleTapTouch;
Expand Down Expand Up @@ -91,7 +91,7 @@ - (instancetype)init {
_touchEnabled = YES;

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sdl_onTouchEvent:) name:SDLDidReceiveTouchEventNotification object:nil];

return self;
}

Expand All @@ -102,23 +102,28 @@ - (void)cancelPendingTouches {

#pragma mark - SDLDidReceiveTouchEventNotification

/**
* Handles detecting the type and state of the gesture and notifies the appropriate delegate callbacks.

* @param notification A SDLOnTouchEvent notification.
*/
- (void)sdl_onTouchEvent:(SDLRPCNotificationNotification *)notification {
if (!self.isTouchEnabled
|| (!self.touchEventHandler && !self.touchEventDelegate)
|| ![notification.notification isKindOfClass:SDLOnTouchEvent.class]) {
return;
}

SDLOnTouchEvent* onTouchEvent = (SDLOnTouchEvent*)notification.notification;

SDLTouchType touchType = onTouchEvent.type;
SDLTouchEvent *touchEvent = onTouchEvent.event.firstObject;
SDLTouch *touch = [[SDLTouch alloc] initWithTouchEvent:touchEvent];

if (self.touchEventHandler) {
self.touchEventHandler(touch, touchType);
}

if (!self.touchEventDelegate || (touch.identifier > MaximumNumberOfTouches)) {
return;
}
Expand All @@ -129,51 +134,56 @@ - (void)sdl_onTouchEvent:(SDLRPCNotificationNotification *)notification {
[self sdl_handleTouchMoved:touch];
} else if ([onTouchEvent.type isEqualToEnum:SDLTouchTypeEnd]) {
[self sdl_handleTouchEnded:touch];
} else if ([onTouchEvent.type isEqualToEnum:SDLTouchTypeCancel]) {
[self sdl_handleTouchCanceled:touch];
}
}

#pragma mark - Private
/**
* Handles a BEGIN touch event sent by Core
*
* @param touch Gesture information
*/
- (void)sdl_handleTouchBegan:(SDLTouch *)touch {
if (!touch.isFirstFinger && !self.isTouchEnabled) {
return; // no-op
}

_performingTouchType = SDLPerformingTouchTypeSingleTouch;

switch (touch.identifier) {
case SDLTouchIdentifierFirstFinger:
case SDLTouchIdentifierFirstFinger: {
self.previousTouch = touch;
break;
case SDLTouchIdentifierSecondFinger:
} break;
case SDLTouchIdentifierSecondFinger: {
_performingTouchType = SDLPerformingTouchTypeMultiTouch;
self.currentPinchGesture = [[SDLPinchGesture alloc] initWithFirstTouch:self.previousTouch
secondTouch:touch];
self.currentPinchGesture = [[SDLPinchGesture alloc] initWithFirstTouch:self.previousTouch secondTouch:touch];
self.previousPinchDistance = self.currentPinchGesture.distance;
if ([self.touchEventDelegate respondsToSelector:@selector(touchManager:pinchDidStartAtCenterPoint:)]) {
[self.touchEventDelegate touchManager:self
pinchDidStartAtCenterPoint:self.currentPinchGesture.center];
[self.touchEventDelegate touchManager:self pinchDidStartAtCenterPoint:self.currentPinchGesture.center];
}
break;
} break;
}
}

/**
* Handles a MOVE touch event sent by Core
*
* @param touch Gesture information
*/
- (void)sdl_handleTouchMoved:(SDLTouch *)touch {
if ((touch.timeStamp - self.previousTouch.timeStamp) <= (self.movementTimeThreshold * NSEC_PER_USEC) || !self.isTouchEnabled) {
if ((touch.timeStamp - self.previousTouch.timeStamp) <= (self.movementTimeThreshold * NSEC_PER_USEC)) {
return; // no-op
}

switch (self.performingTouchType) {
case SDLPerformingTouchTypeMultiTouch:
case SDLPerformingTouchTypeMultiTouch: {
switch (touch.identifier) {
case SDLTouchIdentifierFirstFinger:
case SDLTouchIdentifierFirstFinger: {
self.currentPinchGesture.firstTouch = touch;
break;
case SDLTouchIdentifierSecondFinger:
} break;
case SDLTouchIdentifierSecondFinger: {
self.currentPinchGesture.secondTouch = touch;
break;
} break;
}


if ([self.touchEventDelegate respondsToSelector:@selector(touchManager:didReceivePinchAtCenterPoint:withScale:)]) {
CGFloat scale = self.currentPinchGesture.distance / self.previousPinchDistance;
[self.touchEventDelegate touchManager:self
Expand All @@ -182,63 +192,57 @@ - (void)sdl_handleTouchMoved:(SDLTouch *)touch {
}

self.previousPinchDistance = self.currentPinchGesture.distance;
break;
case SDLPerformingTouchTypeSingleTouch:
} break;
case SDLPerformingTouchTypeSingleTouch: {
_performingTouchType = SDLPerformingTouchTypePanningTouch;
if ([self.touchEventDelegate respondsToSelector:@selector(touchManager:panningDidStartAtPoint:)]) {
[self.touchEventDelegate touchManager:self
panningDidStartAtPoint:touch.location];
}
break;
case SDLPerformingTouchTypePanningTouch:
} break;
case SDLPerformingTouchTypePanningTouch: {
if ([self.touchEventDelegate respondsToSelector:@selector(touchManager:didReceivePanningFromPoint:toPoint:)]) {
[self.touchEventDelegate touchManager:self
didReceivePanningFromPoint:self.previousTouch.location
toPoint:touch.location];
}
break;
case SDLPerformingTouchTypeNone:
break;
} break;
case SDLPerformingTouchTypeNone: break;
}

self.previousTouch = touch;
}

/**
* Handles a END touch type notification sent by Core
*
* @param touch Gesture information
*/
- (void)sdl_handleTouchEnded:(SDLTouch *)touch {
if (!self.isTouchEnabled) {
return; // no-op
}

switch (self.performingTouchType) {
case SDLPerformingTouchTypeMultiTouch:
switch (touch.identifier) {
case SDLTouchIdentifierFirstFinger:
self.currentPinchGesture.firstTouch = touch;
break;
case SDLTouchIdentifierSecondFinger:
self.currentPinchGesture.secondTouch = touch;
break;
}

case SDLPerformingTouchTypeMultiTouch: {
[self sdl_setMultiTouchFingerTouchForTouch:touch];
if (self.currentPinchGesture.isValid) {
if ([self.touchEventDelegate respondsToSelector:@selector(touchManager:pinchDidEndAtCenterPoint:)]) {
[self.touchEventDelegate touchManager:self
pinchDidEndAtCenterPoint:self.currentPinchGesture.center];
}
self.currentPinchGesture = nil;
}
break;
case SDLPerformingTouchTypePanningTouch:
} break;
case SDLPerformingTouchTypePanningTouch: {
if ([self.touchEventDelegate respondsToSelector:@selector(touchManager:panningDidEndAtPoint:)]) {
[self.touchEventDelegate touchManager:self
panningDidEndAtPoint:touch.location];
}
break;
case SDLPerformingTouchTypeSingleTouch:
if (self.singleTapTimer == nil) { // Initial Tap
} break;
case SDLPerformingTouchTypeSingleTouch: {
if (self.singleTapTimer == nil) {
// Initial Tap
self.singleTapTouch = touch;
[self sdl_initializeSingleTapTimerAtPoint:self.singleTapTouch.location];
} else { // Double Tap
} else {
// Double Tap
[self sdl_cancelSingleTapTimer];

NSUInteger timeStampDelta = touch.timeStamp - self.singleTapTouch.timeStamp;
Expand All @@ -256,17 +260,78 @@ - (void)sdl_handleTouchEnded:(SDLTouch *)touch {

self.singleTapTouch = nil;
}
break;
case SDLPerformingTouchTypeNone:
break;
} break;
case SDLPerformingTouchTypeNone: break;
}

self.previousTouch = nil;
_performingTouchType = SDLPerformingTouchTypeNone;
}

/**
* Handles a CANCEL touch event sent by CORE. A CANCEL touch event is sent when a gesture is interrupted during a video stream. This can happen when a system dialog box appears on the screen, such as when the user is alerted about an incoming phone call.
*
* Pinch and pan gesture subscribers are notified if the gesture is canceled. Tap gestures are simply canceled without notification.
*
* @param touch Gesture information
*/
- (void)sdl_handleTouchCanceled:(SDLTouch *)touch {
if (self.singleTapTimer != nil) {
// Cancel any ongoing single tap timer
[self sdl_cancelSingleTapTimer];
self.singleTapTouch = nil;
}

switch (self.performingTouchType) {
case SDLPerformingTouchTypeMultiTouch: {
[self sdl_setMultiTouchFingerTouchForTouch:touch];
if (self.currentPinchGesture.isValid) {
if ([self.touchEventDelegate respondsToSelector:@selector(touchManager:pinchCanceledAtCenterPoint:)]) {
[self.touchEventDelegate touchManager:self
pinchCanceledAtCenterPoint:self.currentPinchGesture.center];
}
self.currentPinchGesture = nil;
}
} break;
case SDLPerformingTouchTypePanningTouch: {
if ([self.touchEventDelegate respondsToSelector:@selector(touchManager:panningCanceledAtPoint:)]) {
[self.touchEventDelegate touchManager:self
panningCanceledAtPoint:touch.location];
}
} break;
case SDLPerformingTouchTypeSingleTouch: // fallthrough
case SDLPerformingTouchTypeNone: break;
}

self.previousTouch = nil;
_performingTouchType = SDLPerformingTouchTypeNone;
}

/**
* Saves the pinch touch gesture to the correct finger
*
* @param touch Gesture information
*/
- (void)sdl_setMultiTouchFingerTouchForTouch:(SDLTouch *)touch {
switch (touch.identifier) {
case SDLTouchIdentifierFirstFinger: {
self.currentPinchGesture.firstTouch = touch;
} break;
case SDLTouchIdentifierSecondFinger: {
self.currentPinchGesture.secondTouch = touch;
} break;
}
}

/**
* Creates a timer used to detect the type of tap gesture (single or double tap)
*
* @param point Screen coordinates of the tap gesture
*/
- (void)sdl_initializeSingleTapTimerAtPoint:(CGPoint)point {
__weak typeof(self) weakSelf = self;
self.singleTapTimer = dispatch_create_timer(self.tapTimeThreshold, NO, ^{
// If timer was not canceled by a second tap then only one tap detected
typeof(weakSelf) strongSelf = weakSelf;
strongSelf.singleTapTouch = nil;
[strongSelf sdl_cancelSingleTapTimer];
Expand All @@ -277,6 +342,9 @@ - (void)sdl_initializeSingleTapTimerAtPoint:(CGPoint)point {
});
}

/**
* Cancels a tap gesture timer
*/
- (void)sdl_cancelSingleTapTimer {
if (self.singleTapTimer == NULL) {
return;
Expand Down
20 changes: 20 additions & 0 deletions SmartDeviceLink/SDLTouchManagerDelegate.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (void)touchManager:(SDLTouchManager *)manager panningDidEndAtPoint:(CGPoint)point;

/**
* @abstract
* Panning canceled.
* @param manager
* Current initalized SDLTouchManager issuing the callback.
* @param point
* Location of the panning's end point in the head unit's coordinate system.
*/
- (void)touchManager:(SDLTouchManager *)manager panningCanceledAtPoint:(CGPoint)point;

/**
* @abstract
* Pinch did start.
Expand Down Expand Up @@ -101,6 +111,16 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (void)touchManager:(SDLTouchManager *)manager pinchDidEndAtCenterPoint:(CGPoint)point;

/**
* @abstract
* Pinch canceled.
* @param manager
* Current initalized SDLTouchManager issuing the callback.
* @param point
* Center point of the pinch in the head unit's coordinate system.
*/
- (void)touchManager:(SDLTouchManager *)manager pinchCanceledAtCenterPoint:(CGPoint)point;

@end

NS_ASSUME_NONNULL_END
Loading