FittsLaw/Assets/GoogleVR/Scripts/EventSystem/GvrPointerScrollInput.cs
2018-10-08 23:54:11 -04:00

284 lines
11 KiB
C#

// Copyright 2017 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using UnityEngine;
using UnityEngine.EventSystems;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
/// This class is used by _GvrPointerInputModule_ to route scroll events through Unity's Event System.
/// It maintains indepedent velocities for each instance of _IScrollHandler_ that is currently being scrolled.
/// Inertia can optionally be toggled off.
[System.Serializable]
public class GvrPointerScrollInput {
public const string PROPERTY_NAME_INERTIA = "inertia";
public const string PROPERTY_NAME_DECELERATION_RATE = "decelerationRate";
private class ScrollInfo {
public bool isScrollingX = false;
public bool isScrollingY = false;
public Vector2 initScroll = Vector2.zero;
public Vector2 lastScroll = Vector2.zero;
public Vector2 scrollVelocity = Vector2.zero;
public IGvrScrollSettings scrollSettings = null;
public bool IsScrolling {
get {
return isScrollingX || isScrollingY;
}
}
}
/// Inertia means that scroll events will continue for a while after the user stops
/// touching the touchpad. It gradually slows down according to the decelerationRate.
[Tooltip("Determines if movement inertia is enabled.")]
public bool inertia = true;
/// The deceleration rate is the speed reduction per second.
/// A value of 0.5 halves the speed each second. The default is 0.05.
/// The deceleration rate is only used when inertia is enabled.
[Tooltip("The rate at which movement slows down.")]
public float decelerationRate = 0.05f;
/// Multiplier for calculating the scroll delta so that the scroll delta is
/// within the order of magnitude that the UI system expects.
public const float SCROLL_DELTA_MULTIPLIER = 1000.0f;
private const float CUTOFF_HZ = 10.0f;
private const float RC = (float) (1.0 / (2.0 * Mathf.PI * CUTOFF_HZ));
private const float SPEED_CLAMP_RATIO = 0.05f;
private const float SPEED_CLAMP = (SPEED_CLAMP_RATIO * SCROLL_DELTA_MULTIPLIER);
private const float SPEED_CLAMP_SQUARED = SPEED_CLAMP * SPEED_CLAMP;
private const float INERTIA_THRESHOLD_RATIO = 0.2f;
private const float INERTIA_THRESHOLD = (INERTIA_THRESHOLD_RATIO * SCROLL_DELTA_MULTIPLIER);
private const float INERTIA_THRESHOLD_SQUARED = INERTIA_THRESHOLD * INERTIA_THRESHOLD;
private const float SLOP_VERTICAL = 0.165f * SCROLL_DELTA_MULTIPLIER;
private const float SLOP_HORIZONTAL = 0.15f * SCROLL_DELTA_MULTIPLIER;
private Dictionary<GameObject, ScrollInfo> scrollHandlers = new Dictionary<GameObject, ScrollInfo>();
private List<GameObject> scrollingObjects = new List<GameObject>();
public void HandleScroll(GameObject currentGameObject, PointerEventData pointerData,
GvrBasePointer pointer, IGvrEventExecutor eventExecutor) {
bool touchDown = false;
bool touching = false;
bool touchUp = false;
Vector2 currentScroll = Vector2.zero;
if (pointer != null && pointer.IsAvailable) {
touchDown = pointer.TouchDown;
touching = pointer.IsTouching;
touchUp = pointer.TouchUp;
currentScroll = pointer.TouchPos * SCROLL_DELTA_MULTIPLIER;
}
GameObject currentScrollHandler = eventExecutor.GetEventHandler<IScrollHandler>(currentGameObject);
if (touchDown) {
RemoveScrollHandler(currentScrollHandler);
}
if (currentScrollHandler != null && (touchDown || touching)) {
OnTouchingScrollHandler(currentScrollHandler, pointerData, currentScroll, eventExecutor);
} else if (touchUp && currentScrollHandler != null) {
OnReleaseScrollHandler(currentScrollHandler);
}
StopScrollingIfNecessary(touching, currentScrollHandler);
UpdateInertiaScrollHandlers(touching, currentScrollHandler, pointerData, eventExecutor);
}
private void OnTouchingScrollHandler(GameObject currentScrollHandler, PointerEventData pointerData,
Vector2 currentScroll, IGvrEventExecutor eventExecutor) {
ScrollInfo scrollInfo = null;
if (!scrollHandlers.ContainsKey(currentScrollHandler)) {
scrollInfo = AddScrollHandler(currentScrollHandler, currentScroll);
} else {
scrollInfo = scrollHandlers[currentScrollHandler];
}
// Detect if we should start scrolling along the x-axis based on the horizontal slop threshold.
if (CanScrollStartX(scrollInfo, currentScroll)) {
scrollInfo.isScrollingX = true;
}
// Detect if we should start scrolling along the y-axis based on the vertical slop threshold.
if (CanScrollStartY(scrollInfo, currentScroll)) {
scrollInfo.isScrollingY = true;
}
if (scrollInfo.IsScrolling) {
Vector2 clampedScroll = currentScroll;
Vector2 clampedLastScroll = scrollInfo.lastScroll;
if (!scrollInfo.isScrollingX) {
clampedScroll.x = 0.0f;
clampedLastScroll.x = 0.0f;
}
if (!scrollInfo.isScrollingY) {
clampedScroll.y = 0.0f;
clampedLastScroll.y = 0.0f;
}
Vector2 scrollDisplacement = clampedScroll - clampedLastScroll;
UpdateVelocity(scrollInfo, scrollDisplacement);
if (!ShouldUseInertia(scrollInfo)) {
// If inertia is disabled, then we send scroll events immediately.
pointerData.scrollDelta = scrollDisplacement;
eventExecutor.ExecuteHierarchy(currentScrollHandler, pointerData, ExecuteEvents.scrollHandler);
pointerData.scrollDelta = Vector2.zero;
}
}
scrollInfo.lastScroll = currentScroll;
}
private void OnReleaseScrollHandler(GameObject currentScrollHandler) {
// When we touch up, immediately stop scrolling the currentScrollHandler if it's velocity is low.
ScrollInfo scrollInfo;
if (scrollHandlers.TryGetValue(currentScrollHandler, out scrollInfo)) {
if (!scrollInfo.IsScrolling || scrollInfo.scrollVelocity.sqrMagnitude <= INERTIA_THRESHOLD_SQUARED) {
RemoveScrollHandler(currentScrollHandler);
}
}
}
private void UpdateVelocity(ScrollInfo scrollInfo, Vector2 scrollDisplacement) {
Vector2 newVelocity = scrollDisplacement / Time.deltaTime;
float weight = Time.deltaTime / (RC + Time.deltaTime);
scrollInfo.scrollVelocity = Vector2.Lerp(scrollInfo.scrollVelocity, newVelocity, weight);
}
private void StopScrollingIfNecessary(bool touching, GameObject currentScrollHandler) {
if (scrollHandlers.Count == 0) {
return;
}
// If inertia is disabled, stop scrolling any scrollHandler that isn't currently being touched.
for (int i = scrollingObjects.Count - 1; i >= 0; i--) {
GameObject scrollHandler = scrollingObjects[i];
ScrollInfo scrollInfo = scrollHandlers[scrollHandler];
bool isScrollling = scrollInfo.IsScrolling;
bool isVelocityBelowThreshold =
isScrollling && scrollInfo.scrollVelocity.sqrMagnitude <= SPEED_CLAMP_SQUARED;
bool isCurrentlyTouching = touching && scrollHandler == currentScrollHandler;
bool shouldUseInertia = ShouldUseInertia(scrollInfo);
bool shouldStopScrolling = isVelocityBelowThreshold
|| ((!shouldUseInertia || !isScrollling) && !isCurrentlyTouching);
if (shouldStopScrolling) {
RemoveScrollHandler(scrollHandler);
}
}
}
private void UpdateInertiaScrollHandlers(bool touching, GameObject currentScrollHandler,
PointerEventData pointerData, IGvrEventExecutor eventExecutor) {
if (pointerData == null) {
return;
}
// If the currentScrollHandler is null, then the currently scrolling scrollHandlers
// must still be decelerated so the function does not return early.
for (int i = 0; i < scrollingObjects.Count; i++) {
GameObject scrollHandler = scrollingObjects[i];
ScrollInfo scrollInfo = scrollHandlers[scrollHandler];
if (!ShouldUseInertia(scrollInfo)) {
continue;
}
if (scrollInfo.IsScrolling) {
// Decelerate the scrollHandler if necessary.
if (!touching || scrollHandler != currentScrollHandler) {
float finalDecelerationRate = GetDecelerationRate(scrollInfo);
scrollInfo.scrollVelocity *= Mathf.Pow(finalDecelerationRate, Time.deltaTime);
}
// Send the scroll events.
pointerData.scrollDelta = scrollInfo.scrollVelocity * Time.deltaTime;
eventExecutor.ExecuteHierarchy(scrollHandler, pointerData, ExecuteEvents.scrollHandler);
}
}
pointerData.scrollDelta = Vector2.zero;
}
private ScrollInfo AddScrollHandler(GameObject scrollHandler, Vector2 currentScroll) {
ScrollInfo scrollInfo = new ScrollInfo();
scrollInfo.initScroll = currentScroll;
scrollInfo.lastScroll = currentScroll;
scrollInfo.scrollSettings = scrollHandler.GetComponent<IGvrScrollSettings>();
scrollHandlers[scrollHandler] = scrollInfo;
scrollingObjects.Add(scrollHandler);
return scrollInfo;
}
private void RemoveScrollHandler(GameObject scrollHandler) {
// Check if it's null via object.Equals instead of doing a direct comparison
// to avoid using Unity's equality check override for UnityEngine.Objects.
// This is so that we can remove Unity objects that have been Destroyed from the dictionary,
// but will still return early when an object is actually null.
if (object.Equals(scrollHandler, null)) {
return;
}
if (!scrollHandlers.ContainsKey(scrollHandler)) {
return;
}
scrollHandlers.Remove(scrollHandler);
scrollingObjects.Remove(scrollHandler);
}
private bool ShouldUseInertia(ScrollInfo scrollInfo) {
if (scrollInfo != null && scrollInfo.scrollSettings != null) {
return scrollInfo.scrollSettings.InertiaOverride;
}
return inertia;
}
private float GetDecelerationRate(ScrollInfo scrollInfo) {
if (scrollInfo != null && scrollInfo.scrollSettings != null) {
return scrollInfo.scrollSettings.DecelerationRateOverride;
}
return decelerationRate;
}
private static bool CanScrollStartX(ScrollInfo scrollInfo, Vector2 currentScroll) {
if (scrollInfo == null) {
return false;
}
return Mathf.Abs(currentScroll.x - scrollInfo.initScroll.x) >= SLOP_HORIZONTAL;
}
private static bool CanScrollStartY(ScrollInfo scrollInfo, Vector2 currentScroll) {
if (scrollInfo == null) {
return false;
}
return Mathf.Abs(currentScroll.y - scrollInfo.initScroll.y) >= SLOP_VERTICAL;
}
}