Show More
Commit Description:
Various UI improvements.
Commit Description:
Various UI improvements.
File last commit:
Show/Diff file:
Action:
FNA/src/Input/Touch/GestureDetector.cs
673 lines | 16.4 KiB | text/x-csharp | CSharpLexer
#region License
/* FNA - XNA4 Reimplementation for Desktop Platforms
* Copyright 2009-2020 Ethan Lee and the MonoGame Team
*
* Released under the Microsoft Public License.
* See LICENSE for details.
*/
#endregion
#region Using Statements
using System;
using System.Collections.Generic;
#endregion
namespace Microsoft.Xna.Framework.Input.Touch
{
internal static class GestureDetector
{
#region Private Static Variables
// The ID of the active finger
private static int activeFingerId = TouchPanel.NO_FINGER;
// The current position of the active finger
private static Vector2 activeFingerPosition;
/* In XNA, if the Pinch gesture was disabled mid-pinch,
* it would still dispatch a PinchComplete gesture once *all*
* fingers were off the screen. (Not just the ones involved
* in the pinch.) Kinda weird, right?
*
* This flag is used to mimic that behavior.
*/
private static bool callBelatedPinchComplete = false;
// The time when the most recent active Press/Release occurred
private static DateTime eventTimestamp;
// The IDs of all fingers currently on the screen
private static List<int> fingerIds = new List<int>();
// A flag to cancel Taps if a Double Tap has just occurred
private static bool justDoubleTapped = false;
// The position of the active finger at the last Update tick
private static Vector2 lastUpdatePosition;
// The position where the user first touched the screen
private static Vector2 pressPosition;
// The ID of the second finger (used only for Pinching)
private static int secondFingerId = TouchPanel.NO_FINGER;
// The current position of the second finger (used only for Pinching)
private static Vector2 secondFingerPosition;
// The current state of gesture detection
private static GestureState state = GestureState.NONE;
// The time of the most recent Update tick
private static DateTime updateTimestamp;
// The current velocity of the active finger
private static Vector2 velocity;
#endregion
#region Private Constants
/* How far (in pixels) the user can move their finger in a gesture
* before it counts as "moved". This prevents small, accidental
* finger movements from interfering with Hold and Tap gestures.
*/
private const int MOVE_THRESHOLD = 35;
/* How fast the finger velocity must be to register as a Flick.
* This helps prevent accidental flicks when a drag or tap was
* intended.
*/
private const int MIN_FLICK_VELOCITY = 100;
#endregion
#region Private Enums
// All possible states of Gesture detection.
private enum GestureState
{
NONE,
HOLDING,
HELD, /* Same as HOLDING, but after a Hold gesture has fired */
JUST_TAPPED,
DRAGGING_FREE,
DRAGGING_H,
DRAGGING_V,
PINCHING
};
#endregion
#region Internal Methods
internal static void OnPressed(int fingerId, Vector2 touchPosition)
{
fingerIds.Add(fingerId);
if (state == GestureState.PINCHING)
{
// None of this method applies to active pinches
return;
}
// Set the active finger if there isn't one already
if (activeFingerId == TouchPanel.NO_FINGER)
{
activeFingerId = fingerId;
activeFingerPosition = touchPosition;
}
else
{
#region Pinch Initialization
if (IsGestureEnabled(GestureType.Pinch))
{
// Initialize a Pinch
secondFingerId = fingerId;
secondFingerPosition = touchPosition;
state = GestureState.PINCHING;
}
#endregion
// No need to do anything more
return;
}
#region Double Tap Detection
if (state == GestureState.JUST_TAPPED)
{
if (IsGestureEnabled(GestureType.DoubleTap))
{
// Must tap again within 300ms of original tap's release
TimeSpan timeSinceRelease = DateTime.Now - eventTimestamp;
if (timeSinceRelease <= TimeSpan.FromMilliseconds(300))
{
// If the new tap is close to the original tap
float distance = (touchPosition - pressPosition).Length();
if (distance <= MOVE_THRESHOLD)
{
// Double Tap!
TouchPanel.EnqueueGesture(new GestureSample(
GestureType.DoubleTap,
GetGestureTimestamp(),
touchPosition,
Vector2.Zero,
Vector2.Zero,
Vector2.Zero,
fingerId,
TouchPanel.NO_FINGER
));
justDoubleTapped = true;
}
}
}
}
#endregion
state = GestureState.HOLDING;
pressPosition = touchPosition;
eventTimestamp = DateTime.Now;
}
internal static void OnReleased(int fingerId, Vector2 touchPosition)
{
fingerIds.Remove(fingerId);
// Handle release events seperately for Pinch gestures
if (state == GestureState.PINCHING)
{
OnReleased_Pinch(fingerId, touchPosition);
return;
}
// Did the user lift the active finger?
if (fingerId == activeFingerId)
{
activeFingerId = TouchPanel.NO_FINGER;
}
// We're only interested in the very last finger to leave
if (FNAPlatform.GetNumTouchFingers() > 0)
{
return;
}
#region Tap Detection
if (state == GestureState.HOLDING)
{
// Which Tap gestures are enabled?
bool tapEnabled = IsGestureEnabled(GestureType.Tap);
bool dtapEnabled = IsGestureEnabled(GestureType.DoubleTap);
if (tapEnabled || dtapEnabled)
{
// How long did the user hold the touch?
TimeSpan timeHeld = DateTime.Now - eventTimestamp;
if (timeHeld < TimeSpan.FromSeconds(1))
{
// Don't register a Tap immediately after a Double Tap
if (!justDoubleTapped)
{
if (tapEnabled)
{
// Tap!
TouchPanel.EnqueueGesture(new GestureSample(
GestureType.Tap,
GetGestureTimestamp(),
touchPosition,
Vector2.Zero,
Vector2.Zero,
Vector2.Zero,
fingerId,
TouchPanel.NO_FINGER
));
}
/* Even if Tap isn't enabled, we still
* need this for Double Tap detection.
*/
state = GestureState.JUST_TAPPED;
}
}
}
}
// Reset this flag so we can catch Taps in the future
justDoubleTapped = false;
#endregion
#region Flick Detection
if (IsGestureEnabled(GestureType.Flick))
{
// Only flick if the finger is outside the threshold and moving fast
float distanceFromPress = (touchPosition - pressPosition).Length();
if (distanceFromPress > MOVE_THRESHOLD &&
velocity.Length() >= MIN_FLICK_VELOCITY)
{
// Flick!
TouchPanel.EnqueueGesture(new GestureSample(
GestureType.Flick,
GetGestureTimestamp(),
Vector2.Zero,
Vector2.Zero,
velocity,
Vector2.Zero,
fingerId,
TouchPanel.NO_FINGER
));
}
// Reset velocity calculation variables
velocity = Vector2.Zero;
lastUpdatePosition = Vector2.Zero;
updateTimestamp = DateTime.MinValue;
}
#endregion
#region Drag Complete Detection
if (IsGestureEnabled(GestureType.DragComplete))
{
bool wasDragging = (state == GestureState.DRAGGING_H ||
state == GestureState.DRAGGING_V ||
state == GestureState.DRAGGING_FREE);
if (wasDragging)
{
// Drag Complete!
TouchPanel.EnqueueGesture(new GestureSample(
GestureType.DragComplete,
GetGestureTimestamp(),
Vector2.Zero,
Vector2.Zero,
Vector2.Zero,
Vector2.Zero,
fingerId,
TouchPanel.NO_FINGER
));
}
}
#endregion
#region Belated Pinch Complete Detection
if (callBelatedPinchComplete && IsGestureEnabled(GestureType.PinchComplete))
{
TouchPanel.EnqueueGesture(new GestureSample(
GestureType.PinchComplete,
GetGestureTimestamp(),
Vector2.Zero,
Vector2.Zero,
Vector2.Zero,
Vector2.Zero,
TouchPanel.NO_FINGER,
TouchPanel.NO_FINGER
));
}
callBelatedPinchComplete = false;
#endregion
// Reset the state if we're not anticipating a Double Tap
if (state != GestureState.JUST_TAPPED)
{
state = GestureState.NONE;
}
eventTimestamp = DateTime.Now;
}
internal static void OnMoved(int fingerId, Vector2 touchPosition, Vector2 delta)
{
// Handle move events separately for Pinch gestures
if (state == GestureState.PINCHING)
{
OnMoved_Pinch(fingerId, touchPosition, delta);
return;
}
// Replace the active finger if we lost it
if (activeFingerId == TouchPanel.NO_FINGER)
{
activeFingerId = fingerId;
}
// If this finger isn't the active finger
if (fingerId != activeFingerId)
{
// We don't care about it
return;
}
// Update the position
activeFingerPosition = touchPosition;
#region Prepare for Dragging
// Determine which drag gestures are enabled
bool hdrag = IsGestureEnabled(GestureType.HorizontalDrag);
bool vdrag = IsGestureEnabled(GestureType.VerticalDrag);
bool fdrag = IsGestureEnabled(GestureType.FreeDrag);
if (state == GestureState.HOLDING || state == GestureState.HELD)
{
// Prevent accidental drags
float distanceFromPress = (touchPosition - pressPosition).Length();
if (distanceFromPress > MOVE_THRESHOLD)
{
if (hdrag && (Math.Abs(delta.X) > Math.Abs(delta.Y)))
{
// Horizontal Drag!
state = GestureState.DRAGGING_H;
}
else if (vdrag && (Math.Abs(delta.Y) > Math.Abs(delta.X)))
{
// Vertical Drag!
state = GestureState.DRAGGING_V;
}
else if (fdrag)
{
// Free Drag!
state = GestureState.DRAGGING_FREE;
}
else
{
// No drag...
state = GestureState.NONE;
}
}
}
#endregion
#region Drag Detection
if (state == GestureState.DRAGGING_H && hdrag)
{
// Horizontal Dragging!
TouchPanel.EnqueueGesture(new GestureSample(
GestureType.HorizontalDrag,
GetGestureTimestamp(),
touchPosition,
Vector2.Zero,
new Vector2(delta.X, 0),
Vector2.Zero,
fingerId,
TouchPanel.NO_FINGER
));
}
else if (state == GestureState.DRAGGING_V && vdrag)
{
// Vertical Dragging!
TouchPanel.EnqueueGesture(new GestureSample(
GestureType.VerticalDrag,
GetGestureTimestamp(),
touchPosition,
Vector2.Zero,
new Vector2(0, delta.Y),
Vector2.Zero,
fingerId,
TouchPanel.NO_FINGER
));
}
else if (state == GestureState.DRAGGING_FREE && fdrag)
{
// Free Dragging!
TouchPanel.EnqueueGesture(new GestureSample(
GestureType.FreeDrag,
GetGestureTimestamp(),
touchPosition,
Vector2.Zero,
delta,
Vector2.Zero,
fingerId,
TouchPanel.NO_FINGER
));
}
#endregion
#region Handle Disabled Drags
/* Handle the case where the current drag type
* was disabled *while* the user was dragging.
*/
if ((state == GestureState.DRAGGING_H && !hdrag) ||
(state == GestureState.DRAGGING_V && !vdrag) ||
(state == GestureState.DRAGGING_FREE && !fdrag))
{
// Reset the state
state = GestureState.HELD;
}
#endregion
}
internal static void OnUpdate()
{
if (state == GestureState.PINCHING)
{
/* Handle the case where the Pinch gesture
* was disabled *while* the user was pinching.
*/
if (!IsGestureEnabled(GestureType.Pinch))
{
state = GestureState.HELD;
secondFingerId = TouchPanel.NO_FINGER;
// Still might need to trigger a PinchComplete
callBelatedPinchComplete = true;
}
// No pinches allowed in the rest of this method!
return;
}
// Must have an active finger to proceed
if (activeFingerId == TouchPanel.NO_FINGER)
{
return;
}
#region Flick Velocity Calculation
if (IsGestureEnabled(GestureType.Flick))
{
// We need one frame to pass so we can calculate delta time
if (updateTimestamp != DateTime.MinValue)
{
/* The calculation below is mostly taken from MonoGame.
* It accumulates velocity after running it through
* a low-pass filter to mitigate the effect of
* acceleration spikes. This works pretty well,
* but on rare occasions the velocity will still
* spike by an order of magnitude.
*
* In practice this tends to be a non-issue, but
* if you *really* need to avoid any spikes, you
* may want to consider normalizing the delta
* reported in the GestureSample and then scaling it
* to min(actualVectorLength, preferredMaxLength).
*
* -caleb
*/
float dt = (float)(DateTime.Now - updateTimestamp).TotalSeconds;
Vector2 delta = activeFingerPosition - lastUpdatePosition;
Vector2 instVelocity = delta / (0.001f + dt);
velocity += (instVelocity - velocity) * 0.45f;
}
lastUpdatePosition = activeFingerPosition;
updateTimestamp = DateTime.Now;
}
#endregion
#region Hold Detection
if (IsGestureEnabled(GestureType.Hold) && state == GestureState.HOLDING)
{
TimeSpan timeSincePress = DateTime.Now - eventTimestamp;
if (timeSincePress >= TimeSpan.FromSeconds(1))
{
// Hold!
TouchPanel.EnqueueGesture(new GestureSample(
GestureType.Hold,
GetGestureTimestamp(),
activeFingerPosition,
Vector2.Zero,
Vector2.Zero,
Vector2.Zero,
activeFingerId,
TouchPanel.NO_FINGER
));
state = GestureState.HELD;
}
}
#endregion
}
#endregion
#region Private Methods
private static TimeSpan GetGestureTimestamp()
{
/* XNA calculates gesture timestamps from
* how long the device has been turned on.
*/
return TimeSpan.FromTicks(Environment.TickCount);
}
private static bool IsGestureEnabled(GestureType gestureType)
{
return (TouchPanel.EnabledGestures & gestureType) != 0;
}
/* The *_Pinch methods are separate from the standard event methods
* because they have to deal with multiple touches. It gets really
* messy and ugly if single-touch and multi-touch detection is all
* intermingled in the same methods.
*/
private static void OnReleased_Pinch(int fingerId, Vector2 touchPosition)
{
// We don't care about fingers that aren't part of the pinch
if (fingerId != activeFingerId && fingerId != secondFingerId)
{
return;
}
if (IsGestureEnabled(GestureType.PinchComplete))
{
// Pinch Complete!
TouchPanel.EnqueueGesture(new GestureSample(
GestureType.PinchComplete,
GetGestureTimestamp(),
Vector2.Zero,
Vector2.Zero,
Vector2.Zero,
Vector2.Zero,
activeFingerId,
secondFingerId
));
}
// If we lost the active finger
if (fingerId == activeFingerId)
{
// Then the second finger becomes the active finger
activeFingerId = secondFingerId;
activeFingerPosition = secondFingerPosition;
}
// Regardless, we no longer have a second finger
secondFingerId = TouchPanel.NO_FINGER;
// Attempt to replace our fallen comrade
bool replacedSecondFinger = false;
foreach (int id in fingerIds)
{
// Find a finger that's not already spoken for
if (id != activeFingerId)
{
secondFingerId = id;
replacedSecondFinger = true;
break;
}
}
if (!replacedSecondFinger)
{
// Aaaand we're back to a single touch
state = GestureState.HELD;
}
}
private static void OnMoved_Pinch(int fingerId, Vector2 touchPosition, Vector2 delta)
{
// We only care if the finger moved is involved in the pinch
if (fingerId != activeFingerId && fingerId != secondFingerId)
{
return;
}
/* In XNA, each Pinch gesture sample contained a delta
* for both fingers. It was somehow able to detect
* simultaneous deltas at an OS level. We don't have that
* luxury, so instead, each Pinch gesture will contain the
* delta information for just _one_ of the fingers.
*
* In practice what this means is that you'll get twice as
* many Pinch gestures added to the queue (one sample for
* each finger). This doesn't matter too much, though,
* since the resulting behavior is identical to XNA.
*
* -caleb
*/
if (fingerId == activeFingerId)
{
activeFingerPosition = touchPosition;
TouchPanel.EnqueueGesture(new GestureSample(
GestureType.Pinch,
GetGestureTimestamp(),
activeFingerPosition,
secondFingerPosition,
delta,
Vector2.Zero,
activeFingerId,
secondFingerId
));
}
else
{
secondFingerPosition = touchPosition;
TouchPanel.EnqueueGesture(new GestureSample(
GestureType.Pinch,
GetGestureTimestamp(),
activeFingerPosition,
secondFingerPosition,
Vector2.Zero,
delta,
activeFingerId,
secondFingerId
));
}
}
#endregion
}
}