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
156 changes: 121 additions & 35 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,10 +134,17 @@ - (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
Expand All @@ -141,39 +153,42 @@ - (void)sdl_handleTouchBegan:(SDLTouch *)touch {
_performingTouchType = SDLPerformingTouchTypeSingleTouch;

switch (touch.identifier) {
case SDLTouchIdentifierFirstFinger:
case SDLTouchIdentifierFirstFinger:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This indentation for the whole switch is incorrect, should look like:

switch (touch.identifier) {
    case SDLTouchIdentifierFirstFinger: {
        self.previousTouch = touch;
    } break;
    case SDLTouchIdentifierSecondFinger: {
        _performingTouchType = SDLPerformingTouchTypeMultiTouch;
        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];
        }
    } break;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

self.previousTouch = touch;
break;
case SDLTouchIdentifierSecondFinger:

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

/**
* 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) {
return; // no-op
}

switch (self.performingTouchType) {
case SDLPerformingTouchTypeMultiTouch:
case SDLPerformingTouchTypeMultiTouch:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also indented incorrectly:

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

        if ([self.touchEventDelegate respondsToSelector:@selector(touchManager:didReceivePinchAtCenterPoint:withScale:)]) {
            CGFloat scale = self.currentPinchGesture.distance / self.previousPinchDistance;
            [self.touchEventDelegate touchManager:self
                     didReceivePinchAtCenterPoint:self.currentPinchGesture.center
                                        withScale:scale];
        }

        self.previousPinchDistance = self.currentPinchGesture.distance;
    } break;
    case SDLPerformingTouchTypeSingleTouch: {
        _performingTouchType = SDLPerformingTouchTypePanningTouch;
        if ([self.touchEventDelegate respondsToSelector:@selector(touchManager:panningDidStartAtPoint:)]) {
            [self.touchEventDelegate touchManager:self
                           panningDidStartAtPoint:touch.location];
        }
    } 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;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

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


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

self.previousPinchDistance = self.currentPinchGesture.distance;
break;
case SDLPerformingTouchTypeSingleTouch:

case SDLPerformingTouchTypeSingleTouch:
_performingTouchType = SDLPerformingTouchTypePanningTouch;
if ([self.touchEventDelegate respondsToSelector:@selector(touchManager:panningDidStartAtPoint:)]) {
[self.touchEventDelegate touchManager:self
panningDidStartAtPoint:touch.location];
}
break;
case SDLPerformingTouchTypePanningTouch:

case SDLPerformingTouchTypePanningTouch:
if ([self.touchEventDelegate respondsToSelector:@selector(touchManager:didReceivePanningFromPoint:toPoint:)]) {
[self.touchEventDelegate touchManager:self
didReceivePanningFromPoint:self.previousTouch.location
toPoint:touch.location];
}
break;
case SDLPerformingTouchTypeNone:

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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also indented incorrectly:

switch (self.performingTouchType) {
    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: {
        if ([self.touchEventDelegate respondsToSelector:@selector(touchManager:panningDidEndAtPoint:)]) {
            [self.touchEventDelegate touchManager:self panningDidEndAtPoint:touch.location];
        }
    } break;
    case SDLPerformingTouchTypeSingleTouch: {
        if (self.singleTapTimer == nil) {
            // Initial Tap
            self.singleTapTouch = touch;
            [self sdl_initializeSingleTapTimerAtPoint:self.singleTapTouch.location];
        } else {
            // Double Tap
            [self sdl_cancelSingleTapTimer];

            NSUInteger timeStampDelta = touch.timeStamp - self.singleTapTouch.timeStamp;
            CGFloat xDelta = fabs(touch.location.x - self.singleTapTouch.location.x);
            CGFloat yDelta = fabs(touch.location.y - self.singleTapTouch.location.y);

            if (timeStampDelta <= self.tapTimeThreshold * NSEC_PER_USEC && xDelta <= self.tapDistanceThreshold && yDelta <= self.tapDistanceThreshold) {
                CGPoint centerPoint = CGPointCenterOfPoints(touch.location,
                                                            self.singleTapTouch.location);
                if ([self.touchEventDelegate respondsToSelector:@selector(touchManager:didReceiveDoubleTapAtPoint:)]) {
                    [self.touchEventDelegate touchManager:self
                               didReceiveDoubleTapAtPoint:centerPoint];
                }
            }

            self.singleTapTouch = nil;
        }
    } break;

    case SDLPerformingTouchTypeNone: break;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

[self sdl_setMultiTouchFingerTouchForTouch:touch];
if (self.currentPinchGesture.isValid) {
if ([self.touchEventDelegate respondsToSelector:@selector(touchManager:pinchDidEndAtCenterPoint:)]) {
[self.touchEventDelegate touchManager:self
Expand All @@ -228,17 +243,21 @@ - (void)sdl_handleTouchEnded:(SDLTouch *)touch {
self.currentPinchGesture = nil;
}
break;
case SDLPerformingTouchTypePanningTouch:

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

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 @@ -257,16 +276,80 @@ - (void)sdl_handleTouchEnded:(SDLTouch *)touch {
self.singleTapTouch = nil;
}
break;
case SDLPerformingTouchTypeNone:

case SDLPerformingTouchTypeNone:
break;
}
self.previousTouch = nil;
_performingTouchType = SDLPerformingTouchTypeNone;
}

/**
* Handles a CANCEL touch event sent by CORE. The 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.isTouchEnabled) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This already exists in the calling method and should not be repeated here or in the other sub-handlers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Removed from all sub-handlers

return; // no-op
}

if (self.singleTapTimer != nil) {
// Cancel any ongoing single tap timer
[self sdl_cancelSingleTapTimer];
self.singleTapTouch = nil;
}

switch (self.performingTouchType) {
case SDLPerformingTouchTypeMultiTouch:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also indented incorrectly:

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

[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 SDLPerformingTouchTypeNone:
case SDLPerformingTouchTypeSingleTouch:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverse this and the above case, they should be in the same order as other sub-handlers (this is done in the code provided in the above comment)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

break;
}

self.previousTouch = nil;
_performingTouchType = SDLPerformingTouchTypeNone;
}

- (void)sdl_setMultiTouchFingerTouchForTouch:(SDLTouch *)touch {
switch (touch.identifier) {
case SDLTouchIdentifierFirstFinger:
self.currentPinchGesture.firstTouch = touch;
break;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add braces as in above cases

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

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 +360,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