Browse Source

iOS: added delay gesture recognizer

This gesture recognizer will prevent GLView from processing unwanted gestures.
Emulates UIScrollView behavior
Fires delayed touches on significant movement
Sergey Minakov 5 years ago
parent
commit
16f9ff5866

+ 1 - 0
platform/iphone/SCsub

@@ -14,6 +14,7 @@ iphone_lib = [
     "in_app_store.mm",
     "icloud.mm",
     "ios.mm",
+    "gl_view_gesture_recognizer.m",
 ]
 
 env_ios = env.Clone()

+ 4 - 0
platform/iphone/gl_view.h

@@ -36,6 +36,7 @@
 #import <UIKit/UIKit.h>
 
 @protocol GLViewDelegate;
+@class GLViewGestureRecognizer;
 
 @interface GLView : UIView <UIKeyInput> {
 @private
@@ -69,6 +70,9 @@
 	BOOL delegateSetup;
 	BOOL active;
 	float screen_scale;
+
+	// Delay gesture recognizer
+	GLViewGestureRecognizer *delayGestureRecognizer;
 }
 
 @property(nonatomic, assign) id<GLViewDelegate> delegate;

+ 8 - 6
platform/iphone/gl_view.mm

@@ -29,6 +29,7 @@
 /*************************************************************************/
 
 #import "gl_view.h"
+#import "gl_view_gesture_recognizer.h"
 
 #include "core/os/keyboard.h"
 #include "core/project_settings.h"
@@ -268,6 +269,7 @@ static void clear_touches() {
 	active = FALSE;
 	if ((self = [super initWithCoder:coder])) {
 		self = [self initGLES];
+		[self initGestureRecognizer];
 	}
 	return self;
 }
@@ -320,6 +322,11 @@ static void clear_touches() {
 	return self;
 }
 
+- (void)initGestureRecognizer {
+	delayGestureRecognizer = [[GLViewGestureRecognizer alloc] init];
+	[self addGestureRecognizer:delayGestureRecognizer];
+}
+
 - (id<GLViewDelegate>)delegate {
 	return delegate;
 }
@@ -503,8 +510,6 @@ static void clear_touches() {
 		if ([touches containsObject:[tlist objectAtIndex:i]]) {
 
 			UITouch *touch = [tlist objectAtIndex:i];
-			if (touch.phase != UITouchPhaseBegan)
-				continue;
 			int tid = get_touch_id(touch);
 			ERR_FAIL_COND(tid == -1);
 			CGPoint touchPoint = [touch locationInView:self];
@@ -521,8 +526,6 @@ static void clear_touches() {
 		if ([touches containsObject:[tlist objectAtIndex:i]]) {
 
 			UITouch *touch = [tlist objectAtIndex:i];
-			if (touch.phase != UITouchPhaseMoved)
-				continue;
 			int tid = get_touch_id(touch);
 			ERR_FAIL_COND(tid == -1);
 			CGPoint touchPoint = [touch locationInView:self];
@@ -539,8 +542,6 @@ static void clear_touches() {
 		if ([touches containsObject:[tlist objectAtIndex:i]]) {
 
 			UITouch *touch = [tlist objectAtIndex:i];
-			if (touch.phase != UITouchPhaseEnded)
-				continue;
 			int tid = get_touch_id(touch);
 			ERR_FAIL_COND(tid == -1);
 			remove_touch(touch);
@@ -642,6 +643,7 @@ static void clear_touches() {
 	if (self != nil) {
 		self = [self initGLES];
 		printf("after init gles %p\n", self);
+		[self initGestureRecognizer];
 	}
 	init_touches();
 	self.multipleTouchEnabled = YES;

+ 54 - 0
platform/iphone/gl_view_gesture_recognizer.h

@@ -0,0 +1,54 @@
+/*************************************************************************/
+/*  gl_view_gesture_recognizer.h                                         */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+// GLViewGestureRecognizer allows iOS gestures to work currectly by
+// emulating UIScrollView's UIScrollViewDelayedTouchesBeganGestureRecognizer.
+// It catches all gestures incoming to UIView and delays them for 150ms
+// (the same value used by UIScrollViewDelayedTouchesBeganGestureRecognizer)
+// If touch cancelation or end message is fired it fires delayed
+// begin touch immediately as well as last touch signal
+
+#import <UIKit/UIKit.h>
+
+@interface GLViewGestureRecognizer : UIGestureRecognizer {
+@private
+
+	// Timer used to delay begin touch message.
+	// Should work as simple emulation of UIDelayedAction
+	NSTimer *delayTimer;
+
+	// Delayed touch parameters
+	NSSet *delayedTouches;
+	UIEvent *delayedEvent;
+}
+
+- (instancetype)init;
+
+@end

+ 131 - 0
platform/iphone/gl_view_gesture_recognizer.m

@@ -0,0 +1,131 @@
+/*************************************************************************/
+/*  gl_view_gesture_recognizer.m                                         */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+#import "gl_view_gesture_recognizer.h"
+
+// Using same delay interval that is used for `UIScrollView`
+const NSTimeInterval kGLGestureDelayInterval = 0.150;
+
+// Minimum distance for touches to move to fire
+// a delay timer before scheduled time.
+// Should be the low enough to not cause issues with dragging
+// but big enough to allow click to work.
+const CGFloat kGLGestureMovementDistance = 0.5;
+
+@implementation GLViewGestureRecognizer
+
+- (instancetype)init {
+	self = [super init];
+
+	self.cancelsTouchesInView = YES;
+	self.delaysTouchesBegan = YES;
+	self.delaysTouchesEnded = YES;
+
+	return self;
+}
+
+- (void)delayTouches:(NSSet *)touches andEvent:(UIEvent *)event {
+	[delayTimer fire];
+
+	delayedTouches = touches;
+	delayedEvent = event;
+
+	delayTimer = [NSTimer scheduledTimerWithTimeInterval:kGLGestureDelayInterval target:self selector:@selector(fireDelayedTouches:) userInfo:nil repeats:NO];
+}
+
+- (void)fireDelayedTouches:(id)timer {
+	[delayTimer invalidate];
+	delayTimer = nil;
+
+	if (delayedTouches) {
+		[self.view touchesBegan:delayedTouches withEvent:delayedEvent];
+	}
+
+	delayedTouches = nil;
+	delayedEvent = nil;
+}
+
+- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
+	NSSet *cleared = [self clearTouches:touches phase:UITouchPhaseBegan];
+	[self delayTouches:cleared andEvent:event];
+}
+
+- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
+	NSSet *cleared = [self clearTouches:touches phase:UITouchPhaseMoved];
+
+	if (delayTimer) {
+		// We should check if movement was significant enough to fire an event
+		// for dragging to work correctly.
+		for (UITouch *touch in cleared) {
+			CGPoint from = [touch locationInView:self.view];
+			CGPoint to = [touch previousLocationInView:self.view];
+			CGFloat xDistance = from.x - to.x;
+			CGFloat yDistance = from.y - to.y;
+
+			CGFloat distance = sqrt(xDistance * xDistance + yDistance * yDistance);
+
+			// Early exit, since one of touches has moved enough to fire a drag event.
+			if (distance > kGLGestureMovementDistance) {
+				[delayTimer fire];
+				[self.view touchesMoved:cleared withEvent:event];
+				return;
+			}
+		}
+		return;
+	}
+
+	[self.view touchesMoved:cleared withEvent:event];
+}
+
+- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
+	[delayTimer fire];
+
+	NSSet *cleared = [self clearTouches:touches phase:UITouchPhaseEnded];
+	[self.view touchesEnded:cleared withEvent:event];
+}
+
+- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
+	[delayTimer fire];
+	[self.view touchesCancelled:touches withEvent:event];
+};
+
+- (NSSet *)clearTouches:(NSSet *)touches phase:(UITouchPhase)phaseToSave {
+	NSMutableSet *cleared = [touches mutableCopy];
+
+	for (UITouch *touch in touches) {
+		if (touch.phase != phaseToSave) {
+			[cleared removeObject:touch];
+		}
+	}
+
+	return cleared;
+}
+
+@end