/* * @author Valentin Simonov / http://va.lent.in/ */ using System; using System.Collections; using System.Collections.Generic; using TouchScript.Utils; using TouchScript.Utils.Attributes; using TouchScript.Pointers; using UnityEngine; using UnityEngine.Profiling; namespace TouchScript.Gestures { /// /// Recognizes a tap. /// [AddComponentMenu("TouchScript/Gestures/Tap Gesture")] [HelpURL("http://touchscript.github.io/docs/html/T_TouchScript_Gestures_TapGesture.htm")] public class TapGesture : Gesture { #region Constants /// /// Message name when gesture is recognized /// public const string TAP_MESSAGE = "OnTap"; #endregion #region Events /// /// Occurs when gesture is recognized. /// public event EventHandler Tapped { add { tappedInvoker += value; } remove { tappedInvoker -= value; } } // Needed to overcome iOS AOT limitations private EventHandler tappedInvoker; /// /// Unity event, occurs when gesture is recognized. /// public GestureEvent OnTap = new GestureEvent(); #endregion #region Public properties /// /// Gets or sets the number of taps required for the gesture to recognize. /// /// The number of taps required for this gesture to recognize. 1 — dingle tap, 2 — double tap. public int NumberOfTapsRequired { get { return numberOfTapsRequired; } set { if (value <= 0) numberOfTapsRequired = 1; else numberOfTapsRequired = value; } } /// /// Gets or sets maximum hold time before gesture fails. /// /// Number of seconds a user should hold their fingers before gesture fails. public float TimeLimit { get { return timeLimit; } set { timeLimit = value; } } /// /// Gets or sets maximum distance for point cluster must move for the gesture to fail. /// /// Distance in cm pointers must move before gesture fails. public float DistanceLimit { get { return distanceLimit; } set { distanceLimit = value; distanceLimitInPixelsSquared = Mathf.Pow(distanceLimit * touchManager.DotsPerCentimeter, 2); } } /// /// Gets or sets the flag if pointers should be treated as a cluster. /// /// true if pointers should be treated as a cluster; otherwise, false. /// /// At the end of a gesture when pointers are lifted off due to the fact that computers are faster than humans the very last pointer's position will be gesture's after that. This flag is used to combine several pointers which from the point of a user were lifted off simultaneously and set their centroid as gesture's . /// public bool CombinePointers { get { return combinePointers; } set { combinePointers = value; } } /// /// Gets or sets time interval before gesture is recognized to combine all lifted pointers into a cluster to use its center as . /// /// Time in seconds to treat pointers lifted off during this interval as a single gesture. public float CombinePointersInterval { get { return combinePointersInterval; } set { combinePointersInterval = value; } } #endregion #region Private variables [SerializeField] private int numberOfTapsRequired = 1; [SerializeField] [NullToggle(NullFloatValue = float.PositiveInfinity)] private float timeLimit = float.PositiveInfinity; [SerializeField] [NullToggle(NullFloatValue = float.PositiveInfinity)] private float distanceLimit = float.PositiveInfinity; [SerializeField] [ToggleLeft] private bool combinePointers = false; [SerializeField] private float combinePointersInterval = .3f; private float distanceLimitInPixelsSquared; // isActive works in a tap cycle (i.e. when double/tripple tap is being recognized) // State -> Possible happens when the first pointer is detected private bool isActive = false; private int tapsDone; private Vector2 startPosition; private Vector2 totalMovement; private TimedSequence pointerSequence = new TimedSequence(); private CustomSampler gestureSampler; #endregion #region Public methods /// public override bool ShouldReceivePointer(Pointer pointer) { if (!base.ShouldReceivePointer(pointer)) return false; // Ignore redispatched pointers — they come from 2+ pointer gestures when one is left with 1 pointer. // In this state it means that the user doesn't have an intention to tap the object. return (pointer.Flags & Pointer.FLAG_RETURNED) == 0; } #endregion #region Unity methods /// protected override void Awake() { base.Awake(); gestureSampler = CustomSampler.Create("[TouchScript] Tap Gesture"); } /// protected override void OnEnable() { base.OnEnable(); distanceLimitInPixelsSquared = Mathf.Pow(distanceLimit * touchManager.DotsPerCentimeter, 2); } [ContextMenu("Basic Editor")] private void switchToBasicEditor() { basicEditor = true; } #endregion #region Gesture callbacks /// protected override void pointersPressed(IList pointers) { gestureSampler.Begin(); base.pointersPressed(pointers); if (pointersNumState == PointersNumState.PassedMaxThreshold || pointersNumState == PointersNumState.PassedMinMaxThreshold) { setState(GestureState.Failed); gestureSampler.End(); return; } if (NumPointers == pointers.Count) { // the first ever pointer if (tapsDone == 0) { startPosition = pointers[0].Position; if (timeLimit < float.PositiveInfinity) StartCoroutine("wait"); } else if (tapsDone >= numberOfTapsRequired) // Might be delayed and retapped while waiting { reset(); startPosition = pointers[0].Position; if (timeLimit < float.PositiveInfinity) StartCoroutine("wait"); } else { if (distanceLimit < float.PositiveInfinity) { if ((pointers[0].Position - startPosition).sqrMagnitude > distanceLimitInPixelsSquared) { setState(GestureState.Failed); gestureSampler.End(); return; } } } } if (pointersNumState == PointersNumState.PassedMinThreshold) { // Starting the gesture when it is already active? => we released one finger and pressed again if (isActive) setState(GestureState.Failed); else { if (State == GestureState.Idle) setState(GestureState.Possible); isActive = true; } } gestureSampler.End(); } /// protected override void pointersUpdated(IList pointers) { gestureSampler.Begin(); base.pointersUpdated(pointers); if (distanceLimit < float.PositiveInfinity) { totalMovement += pointers[0].Position - pointers[0].PreviousPosition; if (totalMovement.sqrMagnitude > distanceLimitInPixelsSquared) setState(GestureState.Failed); } gestureSampler.End(); } /// protected override void pointersReleased(IList pointers) { gestureSampler.Begin(); base.pointersReleased(pointers); if (combinePointers) { var count = pointers.Count; for (var i = 0; i < count; i++) pointerSequence.Add(pointers[i]); if (NumPointers == 0) { // Checking which points were removed in clusterExistenceTime seconds to set their centroid as cached screen position var cluster = pointerSequence.FindElementsLaterThan(Time.unscaledTime - combinePointersInterval, shouldCachePointerPosition); cachedScreenPosition = ClusterUtils.Get2DCenterPosition(cluster); cachedPreviousScreenPosition = ClusterUtils.GetPrevious2DCenterPosition(cluster); } } else { if (NumPointers == 0) { if (!isActive) { setState(GestureState.Failed); gestureSampler.End(); return; } // pointers outside of gesture target are ignored in shouldCachePointerPosition() // if all pointers are outside ScreenPosition will be invalid if (TouchManager.IsInvalidPosition(ScreenPosition)) { setState(GestureState.Failed); } else { tapsDone++; isActive = false; if (tapsDone >= numberOfTapsRequired) setState(GestureState.Recognized); } } } gestureSampler.End(); } /// protected override void onRecognized() { base.onRecognized(); StopCoroutine("wait"); if (tappedInvoker != null) tappedInvoker.InvokeHandleExceptions(this, EventArgs.Empty); if (UseSendMessage && SendMessageTarget != null) SendMessageTarget.SendMessage(TAP_MESSAGE, this, SendMessageOptions.DontRequireReceiver); if (UseUnityEvents) OnTap.Invoke(this); } /// protected override void reset() { base.reset(); isActive = false; totalMovement = Vector2.zero; StopCoroutine("wait"); tapsDone = 0; } /// protected override bool shouldCachePointerPosition(Pointer value) { // Points must be over target when released return PointerUtils.IsPointerOnTarget(value, cachedTransform); } #endregion #region private functions private IEnumerator wait() { // WaitForSeconds is affected by time scale! var targetTime = Time.unscaledTime + TimeLimit; while (targetTime > Time.unscaledTime) yield return null; if (State == GestureState.Idle || State == GestureState.Possible) setState(GestureState.Failed); } #endregion } }