This commit is contained in:
2025-08-18 09:22:24 +08:00
commit cef5623ab0
1333 changed files with 305844 additions and 0 deletions
@@ -0,0 +1,105 @@
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Events;
using frame8.Logic.Misc.Other.Extensions;
using frame8.Logic.Misc.Visual.UI;
using frame8.Logic.Misc.Visual.UI.MonoBehaviours;
using Com.ForbiddenByte.OSA.Core;
namespace Com.ForbiddenByte.OSA.Util
{
/// <summary>
/// Important note: if used with ScrollbarFixer8 (which is true in the most cases,
/// make sure <see cref="ScrollbarFixer8.minSize"/> is not too small
/// </summary>
public class DiscreteScrollbar : MonoBehaviour
{
public RectTransform slotPrefab;
public RectTransform slotsParent;
public UnityIntEvent OnSlotSelected;
public Func<int> getItemsCountFunc;
Scrollbar _Scrollbar;
RectTransform[] slots = new RectTransform[0];
RectTransform _ScrollbarPanelRT;
IScrollRectProxy _ScrollRectProxy;
int _OneIfVert_ZeroIfHor;
const int MAX_COUNT = 100;
bool _UpdatePending;
void Awake()
{
// Get in parent, but ignore self
_ScrollRectProxy = transform.parent.GetComponentInParent<IScrollRectProxy>();
if (_ScrollRectProxy == null)
throw new OSAException(GetType().Name + ": No IScrollRectProxy component found in parent");
_Scrollbar = GetComponent<Scrollbar>();
_ScrollbarPanelRT = _Scrollbar.transform as RectTransform;
_OneIfVert_ZeroIfHor = _ScrollRectProxy.IsHorizontal ? 0 : 1;
}
void OnEnable() { _UpdatePending = false; }
public void OnScrollbarSizeChanged()
{
StartCoroutine(UpdateSize());
}
IEnumerator UpdateSize()
{
while (_UpdatePending) // wait for prev request to complete
yield return null;
_UpdatePending = true;
yield return null;
if (getItemsCountFunc == null)
throw new OSAException(GetType().Name + "getItemsCountFunc==null. Please specify a count provider");
_UpdatePending = true;
int count = getItemsCountFunc();
if (count > MAX_COUNT)
throw new OSAException(GetType().Name + ": count is " + count + ". Bigger than MAX_COUNT=" + MAX_COUNT + ". Are you sure you want to use a discrete scrollbar?");
Rebuild(count);
_UpdatePending = false;
}
public void Rebuild(int numSlots)
{
slotPrefab.gameObject.SetActive(true);
// Clear prev
if (slots != null)
foreach (var slot in slots)
Destroy(slot.gameObject);
// Add new
slots = new RectTransform[numSlots];
float sizesCumu = 0;
float slotSize = _ScrollbarPanelRT.rect.size[_OneIfVert_ZeroIfHor] / numSlots; // not using the handle's size because of rounding errors with higher <numSlots>
RectTransform.Edge edgeToInsetFrom = _OneIfVert_ZeroIfHor == 1 ? RectTransform.Edge.Top : RectTransform.Edge.Left;
for (int i = 0; i < numSlots; i++)
{
var slot = (Instantiate(slotPrefab.gameObject) as GameObject).GetComponent<RectTransform>();
slots[i] = slot;
slot.SetParent(slotsParent, false);
slot.SetInsetAndSizeFromParentEdgeWithCurrentAnchors(edgeToInsetFrom, sizesCumu, slotSize);
sizesCumu += slotSize;
int copyOfI = i;
slot.GetComponentInChildren<Button>().onClick.AddListener(() => { if (OnSlotSelected != null) OnSlotSelected.Invoke(copyOfI); });
}
slotPrefab.gameObject.SetActive(false);
}
[Serializable]
public class UnityIntEvent : UnityEvent<int> { }
}
}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b105aab89694f2040a2b9b7d81c9aa23
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
@@ -0,0 +1,188 @@
using UnityEngine;
using System.Collections;
using UnityEngine.UI;
using System;
using frame8.Logic.Misc.Other.Extensions;
using UnityEngine.Events;
using UnityEngine.Serialization;
namespace Com.ForbiddenByte.OSA.Util
{
/// <summary>
/// Utility to expand an item when it's clicked, dispatching the size change request via <see cref="ISizeChangesHandler"/> for increased flexibility
/// Known issue when used with OSA: when during collapsing the item goes outside viewport, the animation stales, since the views are recycled.
/// This can be solved by having a separate resizing utility script that's not attached to a recycling-prone object.
/// </summary>
[Obsolete("This script will soon be deprecated. Please use the ExpandCollapseAnimationState class instead, which handles several edge cases better.")]
public class ExpandCollapseOnClick : MonoBehaviour
{
/// <summary>
/// The button to whose onClock to subscribe. If not specified, will try to GetComponent&lt;Button&gt; from the GO containing this script
/// </summary>
[Tooltip("will be taken from this object, if not specified")]
public Button button = null;
/// <summary>When expanding, the initial size will be <see cref="nonExpandedSize"/> and the target size will be <see cref="nonExpandedSize"/> x <see cref="expandFactor"/>; opposite is true when collapsing</summary>
[NonSerialized] // must be set through code
public float expandFactor = 2f;
/// <summary>The duration of the expand(or collapse) animation</summary>
public float animDuration = .2f;
public NonExpandedSizeSource nonExpandedSizeSource = NonExpandedSizeSource.WAIT_FOR_EXTERNAL;
[Tooltip("Used in conjunction with NonExpandedSizeSource.PREDEFINED")]
public float nonExpandedSizePredefined = 0f;
public bool useUnscaledTime = true;
/// <summary>This is the size from which the item will start expanding</summary>
[HideInInspector]
public float nonExpandedSize = -1f;
/// <summary>This keeps track of the 'expanded' state. If true, on click the animation will set <see cref="nonExpandedSize"/> as the target size; else, <see cref="nonExpandedSize"/> x <see cref="expandFactor"/> </summary>
[HideInInspector]
public bool expanded = false;
[Tooltip("Returns a value between 0 and 1, 1 meaning end of the progress")]
[FormerlySerializedAs("onExpandAmounChanged")] // correcting typo from pre-4.0 versions
public UnityFloatEvent onExpandAmountChanged = null;
[Tooltip("Returns a value between nonExpandedSize and nonExpandedSize * expandFactor")]
public UnityFloatEvent onExpandSizeChanged = null;
[Obsolete("Use onExpandAmountChanged, instead", true)]
[HideInInspector] // just to be sure unity's serialization system won't behave buggy
public UnityFloatEvent onExpandAmounChanged { get { return onExpandAmountChanged; } }
float Time { get { return useUnscaledTime ? UnityEngine.Time.unscaledTime : UnityEngine.Time.time; } }
float startSize;
float endSize;
float animStart;
//float animEnd;
bool animating = false;
RectTransform rectTransform;
public ISizeChangesHandler sizeChangesHandler;
public enum NonExpandedSizeSource
{
WAIT_FOR_EXTERNAL,
SELF_HEIGHT,
SELF_WIDTH,
PREDEFINED
}
void Awake()
{
rectTransform = transform as RectTransform;
if (button == null)
button = GetComponent<Button>();
if (button)
button.onClick.AddListener(OnClicked);
}
void Start()
{
if (nonExpandedSizeSource == NonExpandedSizeSource.SELF_HEIGHT)
nonExpandedSize = rectTransform.rect.height;
else if (nonExpandedSizeSource == NonExpandedSizeSource.SELF_WIDTH)
nonExpandedSize = rectTransform.rect.width;
else if (nonExpandedSizeSource == NonExpandedSizeSource.PREDEFINED)
nonExpandedSize = nonExpandedSizePredefined;
}
public void OnClicked()
{
if (animating)
return;
if (nonExpandedSize < 0f)
return;
animating = true;
animStart = Time;
//animEnd = animStart + animDuration;
if (expanded) // shrinking
{
startSize = nonExpandedSize * expandFactor;
endSize = nonExpandedSize;
}
else // expanding
{
startSize = nonExpandedSize;
endSize = nonExpandedSize * expandFactor;
}
}
void Update()
{
if (animating)
{
float elapsedTime = Time - animStart;
float t01 = elapsedTime / animDuration;
if (t01 >= 1f) // done
{
t01 = 1f; // fill/clamp animation
animating = false;
}
else
t01 = Mathf.Sqrt(t01); // fast-in, slow-out effect
float size = Mathf.Lerp(startSize, endSize, t01);
if (sizeChangesHandler == null)
{
//// debug
//rectTransform.SetInsetAndSizeFromParentEdgeWithCurrentAnchors(RectTransform.Edge.Top, rectTransform.GetInsetFromParentTopEdge(rectTransform.parent as RectTransform), size);
if (t01 == 0f) // done
expanded = !expanded;
}
else
{
bool accepted = sizeChangesHandler.HandleSizeChangeRequest(rectTransform, size);
// Interruption
if (!accepted)
animating = false;
if (!animating) // done; even if it wasn't accepted, wether we should or shouldn't change the "expanded" state depends on the user's requirements. We chose to change it
{
expanded = !expanded;
sizeChangesHandler.OnExpandedStateChanged(rectTransform, expanded);
}
}
if (onExpandAmountChanged != null)
onExpandAmountChanged.Invoke(t01);
if (onExpandSizeChanged != null)
onExpandSizeChanged.Invoke(size);
}
}
/// <summary>Interface to implement by the class that'll handle the size changes when the animation runs</summary>
public interface ISizeChangesHandler
{
/// <summary>Called each frame during animation</summary>
/// <param name="rt">The animated RectTransform</param>
/// <param name="newSize">The requested size</param>
/// <returns>If it was accepted</returns>
bool HandleSizeChangeRequest(RectTransform rt, float newSize);
/// <summary>Called when the animation ends and the item successfully expanded (<paramref name="expanded"/> is true) or collapsed (else)</summary>
/// <param name="rt">The animated RectTransform</param>
/// <param name="expanded">true if the item expanded. false, if collapsed</param>
void OnExpandedStateChanged(RectTransform rt, bool expanded);
}
[Serializable]
public class UnityFloatEvent : UnityEvent<float> { }
}
}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e022249f686fcbf4d8a3568253759b35
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 74ebe3a40395cd1489c719b031733c78
folderAsset: yes
timeCreated: 1532021437
licenseType: Store
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 34b6cee54f21dde4a80098c1be41ee29
folderAsset: yes
timeCreated: 1552388739
licenseType: Store
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,76 @@
using System.Collections.Generic;
namespace Com.ForbiddenByte.OSA.Util.IO.Pools
{
/// <summary>
/// First in, First out caching. Use it when it's very unlikely or impossible to scroll back to older items.
/// </summary>
/// <remarks>In most cases, this is not actually the best way of doing caching. Consider <see cref="LRUCachingPool"/> instead, as it's more versatile.</remarks>
public class FIFOCachingPool : IPool
{
public delegate void ObjectDestroyer(object key, object value);
public int Capacity { get; private set; }
public int CurrentCount { get { return _Keys.Count; } }
readonly Queue<object> _Keys;
readonly Dictionary<object, object> _Cache;
readonly ObjectDestroyer _ObjectDestroyer;
/// <summary>First in, First out caching</summary>
/// <param name="capacity"></param>
/// <param name="objectDestroyer">
/// When an object is kicked out of the cache, this will be used to process its destruction,
/// in case special code needs to be executed. This is also called for each value when the cache is cleared usinc <see cref="Clear"/>
/// </param>
public FIFOCachingPool(int capacity, ObjectDestroyer objectDestroyer = null)
{
Capacity = capacity;
_Keys = new Queue<object>(capacity);
_Cache = new Dictionary<object, object>(capacity);
_ObjectDestroyer = objectDestroyer;
}
public object Get(object key)
{
object value;
if (_Cache.TryGetValue(key, out value))
{
return value;
}
return null;
}
public void Put(object key, object value)
{
if (CurrentCount == Capacity)
{
object keyToDiscard = _Keys.Dequeue();
object oldValue = _Cache[keyToDiscard];
_Cache.Remove(keyToDiscard);
if (_ObjectDestroyer != null)
_ObjectDestroyer(keyToDiscard, oldValue);
}
_Keys.Enqueue(key);
_Cache[key] = value;
}
public void Clear()
{
if (_ObjectDestroyer != null)
{
foreach (var kv in _Cache)
{
if (kv.Value != null)
_ObjectDestroyer(kv.Key, kv.Value);
}
}
_Cache.Clear();
}
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 9e512af370b74dc43958a0abf0df1946
timeCreated: 1552388740
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,18 @@
using UnityEngine;
using System.Collections;
using UnityEngine.UI;
using Com.ForbiddenByte.OSA.Util;
using System;
using System.Collections.Generic;
namespace Com.ForbiddenByte.OSA.Util.IO.Pools
{
public interface IPool
{
int Capacity { get; }
object Get(object key);
void Put(object key, object value);
void Clear();
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 00dd717d999bf284e97208b360a592a3
timeCreated: 1552388739
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,89 @@
using System.Collections.Generic;
namespace Com.ForbiddenByte.OSA.Util.IO.Pools
{
/// <summary>
/// Clears the least accessed items in favor of the most recently accessed ones.
/// </summary>
/// <remarks>This is more versatile than <see cref="FIFOCachingPool"/>.</remarks>
public class LRUCachingPool : IPool
{
public delegate void ObjectDestroyer(object key, object value);
public int Capacity { get; private set; }
public int CurrentCount { get { return _Cache.Count; } }
readonly Dictionary<object, LinkedListNode<CacheItem>> _Cache;
readonly LinkedList<CacheItem> _LruOrderList;
readonly ObjectDestroyer _ObjectDestroyer;
public LRUCachingPool(int capacity, ObjectDestroyer objectDestroyer = null)
{
Capacity = capacity;
_Cache = new Dictionary<object, LinkedListNode<CacheItem>>(capacity);
_LruOrderList = new LinkedList<CacheItem>();
_ObjectDestroyer = objectDestroyer;
}
public object Get(object key)
{
if (_Cache.TryGetValue(key, out var node))
{
// Move accessed node to the end to show that it was recently used
_LruOrderList.Remove(node); // remove current node
_LruOrderList.AddLast(node); // add it back to the end
return node.Value.Value;
}
return null;
}
public void Put(object key, object value)
{
// If the key already exists, remove it first
if (_Cache.TryGetValue(key, out var existingNode))
{
_LruOrderList.Remove(existingNode);
_Cache.Remove(key);
}
if (_Cache.Count == Capacity)
{
// Remove the least recently used item
LinkedListNode<CacheItem> oldestNode = _LruOrderList.First;
_LruOrderList.RemoveFirst();
_Cache.Remove(oldestNode.Value.Key);
if (_ObjectDestroyer != null)
_ObjectDestroyer(oldestNode.Value.Key, oldestNode.Value.Value);
}
var newNode = new LinkedListNode<CacheItem>(new CacheItem { Key = key, Value = value });
_LruOrderList.AddLast(newNode);
_Cache[key] = newNode;
}
public void Clear()
{
if (_ObjectDestroyer != null)
{
foreach (var cached in _LruOrderList)
{
if (cached.Value != null)
_ObjectDestroyer(cached.Key, cached.Value);
}
}
_Cache.Clear();
_LruOrderList.Clear();
}
class CacheItem
{
public object Key { get; set; }
public object Value { get; set; }
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 63c0c36a14e9f3d40927c554363757d7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,154 @@
using UnityEngine;
using System.Collections;
using UnityEngine.UI;
using Com.ForbiddenByte.OSA.Util;
using System;
using Com.ForbiddenByte.OSA.Util.IO.Pools;
namespace Com.ForbiddenByte.OSA.Util.IO
{
/// <summary>Utility behavior to be attached to a GameObject containing a RawImage for loading remote images using <see cref="SimpleImageDownloader"/>, optionally displaying a specific image during loading and/or on error</summary>
[RequireComponent(typeof(RawImage))]
public class RemoteImageBehaviour : MonoBehaviour
{
public delegate void LoadCompleteDelegate(bool fromCache, bool success);
[Tooltip("If not assigned, will try to find it in this game object")]
[SerializeField] RawImage _RawImage = null;
#pragma warning disable 0649
[SerializeField] Texture2D _LoadingTexture = null;
[SerializeField] Texture2D _ErrorTexture = null;
#pragma warning restore 0649
string _CurrentRequestedURL;
bool _DestroyPending;
Texture2D _Texture;
IPool _Pool;
public void InitializeWithPool(IPool pool)
{
_Pool = pool;
}
void Awake()
{
if (!_RawImage)
_RawImage = GetComponent<RawImage>();
}
/// <summary>Starts the loading, setting the current image to <see cref="_LoadingTexture"/>, if available. If the image is already in cache, and <paramref name="loadCachedIfAvailable"/>==true, will load that instead</summary>
public void Load(string imageURL, bool loadCachedIfAvailable = true, LoadCompleteDelegate onCompleted = null, Action onCanceled = null)
{
bool currentRequestedURLAlreadyLoaded = _CurrentRequestedURL == imageURL;
_CurrentRequestedURL = imageURL;
if (loadCachedIfAvailable)
{
bool foundCached = false;
// Don't re-request if the url is the same. This is useful if there's no pool provided
if (currentRequestedURLAlreadyLoaded)
foundCached = _Texture != null;
else if (_Pool != null)
{
Texture2D cachedInPool = _Pool.Get(imageURL) as Texture2D;
if (cachedInPool)
{
_Texture = cachedInPool;
foundCached = true;
_CurrentRequestedURL = imageURL;
}
}
if (foundCached)
{
_RawImage.texture = _Texture;
if (onCompleted != null)
onCompleted(true, true);
return;
}
}
_RawImage.texture = _LoadingTexture;
var request = new SimpleImageDownloader.Request()
{
url = imageURL,
onDone = result =>
{
if (!_DestroyPending && imageURL == _CurrentRequestedURL) // this will be false if a new request was done during downloading, case in which the result will be ignored
{
// Commented: not reusing textures to load data into them anymore, since in most cases we'll use a pool
//result.LoadTextureInto(_Texture);
if (_Pool == null)
{
// Non-pooled textures should be destroyed
if (_Texture)
DisposeTexture(_Texture);
_Texture = result.CreateTextureFromReceivedData();
}
else
{
var textureAlreadyStoredMeanwhile = _Pool.Get(imageURL);
bool someoneStoredTheImageSooner = textureAlreadyStoredMeanwhile != null;
if (someoneStoredTheImageSooner)
{
// Happens when the same URL is requested multiple times for the first time, and of course only the first
// downloaded image should be kept. In this case, someone else already have downloaded and cached the image, so we just discard the one we downloaded
_Texture = textureAlreadyStoredMeanwhile as Texture2D;
}
else
{
// First time downloaded => cache
_Texture = result.CreateTextureFromReceivedData();
_Pool.Put(imageURL, _Texture);
}
}
_RawImage.texture = _Texture;
if (onCompleted != null)
onCompleted(false, true);
}
else if (onCanceled != null)
onCanceled();
},
onError = () =>
{
if (!_DestroyPending && imageURL == _CurrentRequestedURL) // this will be false if a new request was done during downloading, case in which the result will be ignored
{
_RawImage.texture = _ErrorTexture;
if (onCompleted != null)
onCompleted(false, false);
}
else if (onCanceled != null)
onCanceled();
}
};
SimpleImageDownloader.Instance.Enqueue(request);
}
void OnDestroy()
{
_DestroyPending = true;
// Non-pooled textures should be destroyed
if (_Pool == null && _Texture)
{
DisposeTexture(_Texture);
}
}
void DisposeTexture(Texture2D texture)
{
try
{
Destroy(texture);
}
catch { }
}
}
}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c3acd27727b08d44fb5740f21dedfd14
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
@@ -0,0 +1,121 @@
// WWW class usage
#pragma warning disable 0618
using UnityEngine;
using System.Collections;
using UnityEngine.UI;
using System;
using System.Collections.Generic;
using UnityEngine.Networking;
namespace Com.ForbiddenByte.OSA.Util.IO
{
/// <summary>
/// <para>A utility singleton class for downloading images using a LIFO queue for the requests. <see cref="MaxConcurrentRequests"/> can be used to limit the number of concurrent requests. </para>
/// <para>Default is <see cref="DEFAULT_MAX_CONCURRENT_REQUESTS"/>. Each request is executed immediately if there's room for it. When the queue is full, the downloder starts checking each second if a slot is freed, after which re-enters the loop.</para>
/// </summary>
public class SimpleImageDownloader : MonoBehaviour
{
public static SimpleImageDownloader Instance
{
get
{
if (_Instance == null)
_Instance = new GameObject(typeof(SimpleImageDownloader).Name).AddComponent<SimpleImageDownloader>();
return _Instance;
}
}
static SimpleImageDownloader _Instance;
public int MaxConcurrentRequests { get; set; }
const int DEFAULT_MAX_CONCURRENT_REQUESTS = 20;
List<Request> _QueuedRequests = new List<Request>();
List<Request> _ExecutingRequests = new List<Request>();
WaitForSeconds _Wait1Sec = new WaitForSeconds(1f);
IEnumerator Start()
{
if (MaxConcurrentRequests == 0)
MaxConcurrentRequests = DEFAULT_MAX_CONCURRENT_REQUESTS;
while (true)
{
while (_ExecutingRequests.Count >= MaxConcurrentRequests)
{
yield return _Wait1Sec;
}
int lastIndex = _QueuedRequests.Count - 1;
if (lastIndex >= 0)
{
var lastRequest = _QueuedRequests[lastIndex];
_QueuedRequests.RemoveAt(lastIndex);
StartCoroutine(DownloadCoroutine(lastRequest));
}
yield return null;
}
}
void OnDestroy()
{
_Instance = null;
}
public void Enqueue(Request request)
{ _QueuedRequests.Add(request); }
IEnumerator DownloadCoroutine(Request request)
{
_ExecutingRequests.Add(request);
var www = UnityWebRequestTexture.GetTexture(request.url);
yield return www.SendWebRequest();
if (string.IsNullOrEmpty(www.error))
{
if (request.onDone != null)
{
var result = new Result(www);
request.onDone(result);
}
}
else
{
if (request.onError != null)
request.onError();
}
www.Dispose();
_ExecutingRequests.Remove(request);
}
public class Request
{
public string url;
public Action<Result> onDone;
public Action onError;
}
public class Result
{
UnityEngine.Networking.UnityWebRequest _UsedRequest;
public Result(UnityWebRequest www)
{ _UsedRequest = www; }
public Texture2D CreateTextureFromReceivedData()
{ return DownloadHandlerTexture.GetContent(_UsedRequest); }
}
}
}
#pragma warning restore 0618
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4864fb691f2f269449b08caea7dc1284
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 12cb88043734d2340820fbc6e121ad74
folderAsset: yes
timeCreated: 1530790913
licenseType: Store
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,422 @@
//#define DEBUG_EVENTS
using UnityEngine;
using System.Collections;
using UnityEngine.UI;
using System;
using frame8.Logic.Misc.Other.Extensions;
using UnityEngine.EventSystems;
using System.Collections.Generic;
namespace Com.ForbiddenByte.OSA.Util.ItemDragging
{
/// <summary>
/// </summary>
[RequireComponent(typeof(Graphic))]
public class DraggableItem : MonoBehaviour, IPointerDownHandler, IInitializePotentialDragHandler, IBeginDragHandler, IDragHandler, IEndDragHandler, IPointerUpHandler//, ICancelHandler
{
public float longClickTime = .7f;
public IDragDropListener dragDropListener;
public StateEnum State { get { return _State; } }
public RectTransform RT { get { return _RT; } }
public OrphanedItemBundle OrphanedBundle { get { return _OrphanedBundle; } }
public Vector2 CurrentOnDragEventWorldPosition { get { return _CurrentOnDragEventWorldPosition; } }
public Vector2 DistancePointerToDraggedInCanvasSpace { get { return _DistancePointerToDraggedInCanvasSpace; } }
public Camera CurrentPressEventCamera { get { return _CurrentPressEventCamera; } }
IInitializePotentialDragHandler _ParentToDelegateDragEventsTo;
Vector2 _CurrentOnDragEventWorldPosition;
Vector2 _DistancePointerToDraggedInCanvasSpace;
Camera _CurrentPressEventCamera;
RectTransform _RT;
Canvas _Canvas;
GraphicRaycaster _GraphicRaycaster;
RectTransform _CanvasRT;
Vector2 _CurrentPressEventWorldPosition;
float _PressedTime;
StateEnum _State;
OrphanedItemBundle _OrphanedBundle;
//int _PointerID;
EventSystem _EventSystem;
EventSystem GetOrFindEventSystem()
{
if (_EventSystem == null)
_EventSystem = FindObjectOfType<EventSystem>();
return _EventSystem;
}
void Start()
{
_RT = transform as RectTransform;
}
void Update()
{
if (_State == StateEnum.PRESSING__WAITING_FOR_LONG_CLICK)
{
if (Time.unscaledTime - _PressedTime >= longClickTime)
OnLongClick();
}
}
void OnLongClick()
{
EnterState_AfterLongClickDragAccepted_WaitingToBeginDrag();
var evSystem = GetOrFindEventSystem();
if (!evSystem)
{
EnterState_WaitingForPress();
return;
}
var canvas = GetComponentInParent<Canvas>();
var raycaster = canvas.GetComponentInParent<GraphicRaycaster>();
var raycastResults = new List<RaycastResult>();
var pev = new PointerEventData(evSystem);
pev.position = _CurrentPressEventWorldPosition;
raycaster.Raycast(pev, raycastResults);
bool foundThis = false;
foreach (var res in raycastResults)
{
if (res.gameObject == gameObject)
{
foundThis = true;
break;
}
}
// Happens if the object is moved externally while the pointer remains still
if (!foundThis)
{
EnterState_AfterLongClickDragDeclined_WaitingToBeginDrag();
return;
}
_Canvas = canvas;
_CanvasRT = _Canvas.transform as RectTransform;
_GraphicRaycaster = raycaster;
var pos = RT.position;
RT.SetParent(_CanvasRT, false);
RT.position = pos; // preserving the pos
SetVisualMode(VisualMode.OVER_OWNER_OR_OUTSIDE);
if (dragDropListener != null && !dragDropListener.OnPrepareToDragItem(this))
{
EnterState_AfterLongClickDragDeclined_WaitingToBeginDrag();
}
}
public void CancelDragSilently()
{
EnterState_WaitingForPress();
}
void SetVisualMode(VisualMode mode)
{
int intMode = (int)mode;
var euler = RT.localEulerAngles;
euler.x = 10f * intMode;
euler.z = 4f * intMode;
RT.localEulerAngles = euler;
}
void EnterState_WaitingForPress()
{
_ParentToDelegateDragEventsTo = null;
_DistancePointerToDraggedInCanvasSpace = Vector2.zero;
_CurrentPressEventCamera = null;
_Canvas = null;
_CanvasRT = null;
_GraphicRaycaster = null;
SetVisualMode(VisualMode.NONE);
_State = StateEnum.WAITING_FOR_PRESS;
}
void EnterState_AfterLongClickDragAccepted_WaitingToBeginDrag()
{
_State = StateEnum.AFTER_LONG_CLICK_DRAG_ACCEPTED__WAITING_TO_BEGIN_DRAG;
}
void EnterState_AfterLongClickDragDeclined_WaitingToBeginDrag()
{
_State = StateEnum.AFTER_LONG_CLICK_DRAG_DECLINED__WAITING_TO_BEGIN_DRAG;
}
void EnterState_BusyDelegatingDragEventToParent()
{
_State = StateEnum.BUSY_DELEGATING_DRAG_TO_PARENT;
}
#region Callbacks from Unity UI event handlers
void IPointerDownHandler.OnPointerDown(PointerEventData eventData)
{
#if DEBUG_EVENTS
Debug.Log("OnPointerDown: " + _State);
#endif
if (eventData.button != PointerEventData.InputButton.Left)
return;
if (_State != StateEnum.WAITING_FOR_PRESS)
return;
//_PointerID = eventData.pointerId;
_CurrentPressEventWorldPosition = eventData.position;
_State = StateEnum.PRESSING__WAITING_FOR_LONG_CLICK;
_PressedTime = Time.unscaledTime;
}
void IPointerUpHandler.OnPointerUp(PointerEventData eventData)
{
#if DEBUG_EVENTS
Debug.Log("OnPointerUp: " + _State);
#endif
if (eventData.button != PointerEventData.InputButton.Left)
return;
if (_State == StateEnum.PRESSING__WAITING_FOR_LONG_CLICK)
{
EnterState_WaitingForPress();
}
else if (_State == StateEnum.DRAGGING || _State == StateEnum.AFTER_LONG_CLICK_DRAG_ACCEPTED__WAITING_TO_BEGIN_DRAG)
{
var raycaster = _GraphicRaycaster;
EnterState_WaitingForPress();
DropAndCheckForOrphaned(eventData, raycaster);
}
}
void IInitializePotentialDragHandler.OnInitializePotentialDrag(PointerEventData eventData)
{
#if DEBUG_EVENTS
Debug.Log("OnInitializePotentialDrag: " + _State);
#endif
_ParentToDelegateDragEventsTo = null;
if (!RT.parent)
return;
_ParentToDelegateDragEventsTo = RT.parent.GetComponentInParent(typeof(IInitializePotentialDragHandler)) as IInitializePotentialDragHandler;
if (_ParentToDelegateDragEventsTo != null)
_ParentToDelegateDragEventsTo.OnInitializePotentialDrag(eventData);
}
void IBeginDragHandler.OnBeginDrag(PointerEventData eventData)
{
#if DEBUG_EVENTS
Debug.Log("OnBeginDrag: " + _State);
#endif
if (eventData.button != PointerEventData.InputButton.Left
|| _State != StateEnum.AFTER_LONG_CLICK_DRAG_ACCEPTED__WAITING_TO_BEGIN_DRAG)
{
if (
// A child was pressed, which forwarded the event to us
_State == StateEnum.WAITING_FOR_PRESS
// Long-click canceled
|| _State == StateEnum.PRESSING__WAITING_FOR_LONG_CLICK
// The OnPrepareToDragItem returned false (the listener declined the drag when the long click happened) or the item could not be dragged due to other reasons
|| _State == StateEnum.AFTER_LONG_CLICK_DRAG_DECLINED__WAITING_TO_BEGIN_DRAG
)
{
var casted = _ParentToDelegateDragEventsTo as IBeginDragHandler;
if (casted != null)
{
EnterState_BusyDelegatingDragEventToParent(); // keep sending the current started drag event
casted.OnBeginDrag(eventData);
}
else
EnterState_WaitingForPress();
}
return;
}
_CurrentPressEventCamera = eventData.pressEventCamera;
Vector2 draggedVHPosScreen = frame8.Logic.Misc.Other.UIUtils8.Instance.WorldToScreenPointForCanvas(_Canvas, eventData.pressEventCamera, RT.position);
_DistancePointerToDraggedInCanvasSpace = draggedVHPosScreen - eventData.position;
_State = StateEnum.DRAGGING;
if (dragDropListener == null)
{
if (_OrphanedBundle == null)
Debug.Log("OnBeginDrag: dragDropListener is null, but the item is not orphaned (_OrphanedBundle is null)");
return;
}
dragDropListener.OnBeginDragItem(eventData);
}
void IDragHandler.OnDrag(PointerEventData eventData)
{
//Debug.Log("OnDrag" + eventData.button);
if (eventData.button != PointerEventData.InputButton.Left
|| _State != StateEnum.DRAGGING)
{
if (_State != StateEnum.BUSY_DELEGATING_DRAG_TO_PARENT)
return;
var casted = _ParentToDelegateDragEventsTo as IDragHandler;
if (casted != null)
casted.OnDrag(eventData);
return;
}
_CurrentOnDragEventWorldPosition = eventData.position;
Vector3 worldPoint;
RectTransformUtility.ScreenPointToWorldPointInRectangle(
_CanvasRT,
CurrentOnDragEventWorldPosition + DistancePointerToDraggedInCanvasSpace,
eventData.pressEventCamera,
out worldPoint
);
RT.position = worldPoint;
if (dragDropListener == null && _OrphanedBundle == null)
{
Debug.Log("OnBeginDrag: dragDropListener is null, but the item is not orphaned (_OrphanedBundle is null)");
return;
}
var results = RaycastForDragDropListeners(_GraphicRaycaster, eventData);
if (results.Count > 0)
{
// Just a visual feedback that another listener may accept this item
if (dragDropListener == null || !results.Contains(dragDropListener))
SetVisualMode(VisualMode.OVER_POTENTIAL_NEW_OWNER);
else
SetVisualMode(VisualMode.OVER_OWNER_OR_OUTSIDE);
}
else
SetVisualMode(VisualMode.OVER_OWNER_OR_OUTSIDE);
if (dragDropListener != null)
dragDropListener.OnDraggedItem(eventData);
}
void IEndDragHandler.OnEndDrag(PointerEventData eventData)
{
#if DEBUG_EVENTS
Debug.Log("OnEndDrag: " + _State);
#endif
if (eventData.button != PointerEventData.InputButton.Left
|| _State != StateEnum.DRAGGING)
{
if (_State != StateEnum.BUSY_DELEGATING_DRAG_TO_PARENT)
return;
var casted = _ParentToDelegateDragEventsTo as IEndDragHandler;
EnterState_WaitingForPress(); // prevent setting _ParentToDelegateDragEventsTo to null
if (casted != null)
casted.OnEndDrag(eventData);
return;
}
var raycaster = _GraphicRaycaster;
EnterState_WaitingForPress();
DropAndCheckForOrphaned(eventData, raycaster);
}
#endregion
void DropAndCheckForOrphaned(PointerEventData eventData, GraphicRaycaster raycaster)
{
if (dragDropListener == null && _OrphanedBundle == null)
Destroy(gameObject);
var wasOrphanedBeforeDrag = dragDropListener == null;
if (wasOrphanedBeforeDrag || (_OrphanedBundle = dragDropListener.OnDroppedItem(eventData)) != null)
{
if (dragDropListener != null)
throw new InvalidOperationException("When orphaned, dragDropListener should be set to null");
// Find a listener among the raycasted ones, other that the current listener (since this is the listener that has orphaned the item anyway)
var results = RaycastForDragDropListeners(raycaster, eventData);
bool accepted = false;
foreach (var listener in results)
{
if (!wasOrphanedBeforeDrag && _OrphanedBundle.previousOwner != null && listener == _OrphanedBundle.previousOwner)
continue;
accepted = listener.OnDroppedExternalItem(eventData, this);
if (!accepted)
continue;
}
if (accepted)
{
if (dragDropListener == null)
throw new InvalidOperationException("When adopting an orphaned item, dragDropListener should be set to the new owner");
_OrphanedBundle = null;
}
else
{
// Just wait for another press event
}
}
}
List<IDragDropListener> RaycastForDragDropListeners(GraphicRaycaster raycaster, PointerEventData eventData)
{
List<IDragDropListener> listeners = new List<IDragDropListener>();
List<RaycastResult> results = new List<RaycastResult>();
raycaster.Raycast(eventData, results);
// Find a listener among the raycasted ones
IDragDropListener listener;
foreach (var res in results)
{
listener = res.gameObject.GetComponent(typeof(IDragDropListener)) as IDragDropListener;
if (listener == null)
continue;
listeners.Add(listener);
}
return listeners;
}
public enum StateEnum
{
WAITING_FOR_PRESS,
BUSY_DELEGATING_DRAG_TO_PARENT,
PRESSING__WAITING_FOR_LONG_CLICK,
AFTER_LONG_CLICK_DRAG_DECLINED__WAITING_TO_BEGIN_DRAG,
AFTER_LONG_CLICK_DRAG_ACCEPTED__WAITING_TO_BEGIN_DRAG,
DRAGGING,
}
enum VisualMode
{
NONE = 0,
OVER_OWNER_OR_OUTSIDE = 1,
OVER_POTENTIAL_NEW_OWNER = -1
}
public class OrphanedItemBundle
{
public IDragDropListener previousOwner;
public object views;
public object model;
}
/// <summary>Interface to implement by the class that'll handle the drag events</summary>
public interface IDragDropListener
{
/// <summary> Returns if the item drag was accepted </summary>
bool OnPrepareToDragItem(DraggableItem item);
void OnBeginDragItem(PointerEventData eventData);
void OnDraggedItem(PointerEventData eventData);
/// <summary> Returns null if the object was accepted. Otherwise, an <see cref="OrphanedItemBundle"/> </summary>
OrphanedItemBundle OnDroppedItem(PointerEventData eventData);
/// <summary> Returns if the item was accepted </summary>
bool OnDroppedExternalItem(PointerEventData eventData, DraggableItem orphanedItemWithBundle);
}
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: e05b68d5a78fb4546949b44114cf847a
timeCreated: 1530790917
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,82 @@
using UnityEngine;
using System.Collections;
using UnityEngine.UI;
using System;
using frame8.Logic.Misc.Other.Extensions;
using UnityEngine.EventSystems;
namespace Com.ForbiddenByte.OSA.Util
{
/// <summary>
/// Utility to delegate the "long click" event to <see cref="longClickListener"/>
/// It requires a graphic component (can be an image with zero alpha) that can be clicked in order to receive OnPointerDown, OnPointerUp etc.
/// No other UI elements should be on top of this one in order to receive pointer callbacks
/// </summary>
[RequireComponent(typeof(Graphic))]
public class LongClickableItem : MonoBehaviour, IPointerDownHandler, IPointerUpHandler, ICancelHandler
{
public float longClickTime = .7f;
public IItemLongClickListener longClickListener;
public StateEnum State { get { return _State; } }
float _PressedTime;
StateEnum _State;
//int _PointerID;
public enum StateEnum
{
NOT_PRESSING,
PRESSING_WAITING_FOR_LONG_CLICK,
PRESSING_AFTER_LONG_CLICK
}
void Update()
{
if (_State == StateEnum.PRESSING_WAITING_FOR_LONG_CLICK)
{
if (Time.unscaledTime - _PressedTime >= longClickTime)
{
_State = StateEnum.PRESSING_AFTER_LONG_CLICK;
if (longClickListener != null)
longClickListener.OnItemLongClicked(this);
}
}
}
#region Callbacks from Unity UI event handlers
public void OnPointerDown(PointerEventData eventData)
{
//Debug.Log("OnPointerDown" + eventData.button);
if (eventData.button != PointerEventData.InputButton.Left)
return;
//_PointerID = eventData.pointerId;
_State = StateEnum.PRESSING_WAITING_FOR_LONG_CLICK;
_PressedTime = Time.unscaledTime;
}
public void OnPointerUp(PointerEventData eventData)
{
//Debug.Log("OnPointerUp" + eventData.button);
if (eventData.button != PointerEventData.InputButton.Left)
return;
_State = StateEnum.NOT_PRESSING;
}
public void OnCancel(BaseEventData eventData)
{
//Debug.Log("OnCancel");
_State = StateEnum.NOT_PRESSING;
}
#endregion
/// <summary>Interface to implement by the class that'll handle the long click events</summary>
public interface IItemLongClickListener
{
void OnItemLongClicked(LongClickableItem longClickedItem);
}
}
}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 591f287cf6e51b8479d1544830586dd5
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 4bc415364553c314a9f2baa241dad1dc
folderAsset: yes
timeCreated: 1563293174
licenseType: Store
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,60 @@
// Pre-2019.1 Unity versions have performance problems with the Shadow script,
// while 2019.{1,2,3,4} sometimes have problems with displaying an Image+Shadow
#if UNITY_2019_1_OR_NEWER && !(UNITY_2019_1_0 || UNITY_2019_1_1 || UNITY_2019_1_2 || UNITY_2019_1_3 || UNITY_2019_1_4)
#define ALLOW_SHADOWS
#endif
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
namespace Com.ForbiddenByte.OSA.Util.Optimization
{
/// <summary>
/// Simple: If no Unity 2019, no shadow (the shadow will be destroyed).
/// Seems like a lot of Shadow components used cause significant FPS drops
/// </summary>
[ExecuteInEditMode]
public class ShadowRemover : MonoBehaviour
{
// If not on a compatible unity version, destroy the Shadow script and this script
#if ALLOW_SHADOWS
#else
[SerializeField]
[HideInInspector]
Shadow _Shadow;
void Awake()
{
if (!_Shadow)
_Shadow = GetComponent<Shadow>();
if (!Application.isPlaying)
return;
// In play mode, destroy it
if (_Shadow)
{
Destroy(_Shadow);
_Shadow = null;
}
Destroy(this);
}
#if UNITY_EDITOR
void Update()
{
// No checks during play mode
if (Application.isPlaying)
return;
if (!_Shadow)
_Shadow = GetComponent<Shadow>();
}
#endif
#endif
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 1a97ab825bf3ba44d81da6a6ce5c66fa
timeCreated: 1563293088
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,349 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace Com.ForbiddenByte.OSA.Util
{
[AddComponentMenu("Layout/Packed Grid Layout Group", 153)]
/// <summary>
/// Layout class to arrange children elements in a circular grid format. Circular in the . Items will try to occupy as much space as possible.
/// </summary>
public class PackedGridLayoutGroup : LayoutGroup
{
[SerializeField] protected float m_ForcedSpacing = 0f;
[Tooltip("The specified axis will have the 'Preferred' size set based on children")]
[SerializeField] protected AxisOrNone m_childrenControlSize = AxisOrNone.Vertical;
[Tooltip("If true, the layout will start with bigger children. The starting position is defined by the 'Child Alignment' property")]
[SerializeField] protected bool m_biggerChildrenFirst = true;
[Tooltip("Set to as many as possible, if the FPS allows")]
[Range(1, (int)Packer2DBox.NodeChoosingStrategy.COUNT_)]
[SerializeField] protected int m_numPasses = (int)Packer2DBox.NodeChoosingStrategy.COUNT_;
/// <summary>
/// The spacing to use between layout elements in the grid on both axes.
/// The spacing is created by shrinking the childrens' sizes rather than actually adding spaces.
/// If you want true spacing, consider modifying the children themselves to also include some padding inside them
/// </summary>
public float ForcedSpacing { get { return m_ForcedSpacing; } set { SetProperty(ref m_ForcedSpacing, value); } }
/// <summary>
/// The specified axis will have the 'Preferred' size set based on children
/// </summary>
public AxisOrNone ChildrenControlSize { get { return m_childrenControlSize; } set { SetProperty(ref m_childrenControlSize, value); } }
/// <summary>
/// If true, the layout will start with bigger children. The starting position is defined by the 'Child Alignment' property
/// </summary>
public bool BiggerChildrenFirst { get { return m_biggerChildrenFirst; } set { SetProperty(ref m_biggerChildrenFirst, value); } }
/// <summary>
/// <para>This refers to the number of different strategies to use when packing children. Set to as many as possible, if the FPS allows.
/// See <see cref="Packer2DBox.NodeChoosingStrategy"/>.</para>
/// <para> At the moment (15 Mar 2019), more than 1 pass is executing only if the boxes don't all fit in the available
/// space, as the first strategy (<see cref="Packer2DBox.NodeChoosingStrategy.MAX_VOLUME"/>) seems to always perform the best</para>
/// </summary>
public int NumPasses { get { return m_numPasses; } set { SetProperty(ref m_numPasses, value); } }
protected PackedGridLayoutGroup()
{ }
#if UNITY_EDITOR
protected override void OnValidate()
{
base.OnValidate();
}
#endif
public override void CalculateLayoutInputHorizontal()
{
base.CalculateLayoutInputHorizontal();
float minWidthToSet;
float preferredWidthToSet;
if (m_childrenControlSize == AxisOrNone.Horizontal)
{
float width, _;
GetChildSetups(out width, out _);
minWidthToSet = preferredWidthToSet = width + padding.horizontal;
}
else
{
minWidthToSet = minWidth;
preferredWidthToSet = preferredWidth;
}
SetLayoutInputForAxis(minWidthToSet, preferredWidthToSet, -1, 0);
}
public override void CalculateLayoutInputVertical()
{
float minHeightToSet;
float preferredHeightToSet;
if (m_childrenControlSize == AxisOrNone.Vertical)
{
float _, height;
GetChildSetups(out _, out height);
minHeightToSet = preferredHeightToSet = height + padding.vertical;
}
else
{
minHeightToSet = minHeight;
preferredHeightToSet = preferredHeight;
}
SetLayoutInputForAxis(minHeightToSet, preferredHeightToSet, -1, 1);
}
public override void SetLayoutHorizontal()
{
LayoutChildren(true, false);
}
public override void SetLayoutVertical()
{
LayoutChildren(false, true);
}
void GetInsetAndEdges(float chWidth, float chHeight, out RectTransform.Edge xInsetEdge, out RectTransform.Edge yInsetEdge, out float xAddInset, out float yAddInset)
{
var gridRect = rectTransform.rect;
xInsetEdge = RectTransform.Edge.Left;
yInsetEdge = RectTransform.Edge.Top;
xAddInset = 0f;
yAddInset = 0f;
if (m_childrenControlSize != AxisOrNone.Horizontal)
{
switch (childAlignment)
{
case TextAnchor.UpperLeft:
case TextAnchor.LowerLeft:
case TextAnchor.MiddleLeft:
xInsetEdge = RectTransform.Edge.Left;
xAddInset = padding.left;
break;
case TextAnchor.UpperRight:
case TextAnchor.LowerRight:
case TextAnchor.MiddleRight:
xInsetEdge = RectTransform.Edge.Right;
xAddInset = padding.right;
break;
case TextAnchor.UpperCenter:
case TextAnchor.MiddleCenter:
case TextAnchor.LowerCenter:
xAddInset = Mathf.Max((gridRect.width - chWidth), padding.horizontal) / 2f;
break;
}
}
if (m_childrenControlSize != AxisOrNone.Vertical)
{
switch (childAlignment)
{
case TextAnchor.UpperLeft:
case TextAnchor.UpperCenter:
case TextAnchor.UpperRight:
yInsetEdge = RectTransform.Edge.Top;
yAddInset = padding.top;
break;
case TextAnchor.LowerLeft:
case TextAnchor.LowerCenter:
case TextAnchor.LowerRight:
yInsetEdge = RectTransform.Edge.Bottom;
yAddInset = padding.bottom;
break;
case TextAnchor.MiddleLeft:
case TextAnchor.MiddleCenter:
case TextAnchor.MiddleRight:
yAddInset = Mathf.Max((gridRect.height - chHeight), padding.vertical) / 2f;
break;
}
}
}
void LayoutChildren(bool hor, bool vert)
{
float chWidth, chHeight;
var setups = GetChildSetups(out chWidth, out chHeight);
RectTransform.Edge xInsetEdge, yInsetEdge;
float xAddInset, yAddInset;
GetInsetAndEdges(chWidth, chHeight, out xInsetEdge, out yInsetEdge, out xAddInset, out yAddInset);
foreach (var child in setups)
{
var c = child as ChildSetup;
if (c.box.position == null)
continue;
if (hor)
c.child.SetInsetAndSizeFromParentEdge(xInsetEdge, (float)c.box.position.x + xAddInset, (float)c.box.width - ForcedSpacing);
if (vert)
c.child.SetInsetAndSizeFromParentEdge(yInsetEdge, (float)c.box.position.y + yAddInset, (float)c.box.height - ForcedSpacing);
}
}
List<ChildSetup> GetChildSetups(out float width, out float height)
{
var list = new List<ChildSetup>(rectChildren.Count);
foreach (var child in rectChildren)
{
float chWidth = LayoutUtility.GetPreferredSize(child, 0);
float chHeight = LayoutUtility.GetPreferredSize(child, 1);
list.Add(new ChildSetup(chWidth, chHeight, child));
}
if (BiggerChildrenFirst)
{
// Biggest boxes first with maxside, then secondarily by volume
// More info: https://codeincomplete.com/posts/bin-packing/
list.Sort((a, b) =>
{
var aMax = System.Math.Max(a.box.width, a.box.height);
var bMax = System.Math.Max(b.box.width, b.box.height);
if (aMax != bMax)
return (int)(bMax - aMax);
return (int)(b.box.volume - a.box.volume);
});
}
float availableWidth, availableHeight;
if (m_childrenControlSize == AxisOrNone.Horizontal)
{
availableWidth = float.MaxValue;
availableHeight = rectTransform.rect.height - padding.vertical;
}
else if (m_childrenControlSize == AxisOrNone.Vertical)
{
availableWidth = rectTransform.rect.width - padding.horizontal;
availableHeight = float.MaxValue;
}
else
{
availableHeight = rectTransform.rect.height - padding.vertical;
availableWidth = rectTransform.rect.width - padding.horizontal;
}
// Spacing usually creates mode big empty spaces where no item can fit
float spacingToUse = 0f;
var packer = new Packer2DBox(availableWidth, availableHeight, spacingToUse);
int maxStrategiesToUse = m_numPasses;
List<Packer2DBox.Box>[] boxesPerStrategy = new List<Packer2DBox.Box>[maxStrategiesToUse];
int[] nullPositionsPerStrategy = new int[maxStrategiesToUse];
double[] totalWidthsPerStrategy = new double[maxStrategiesToUse];
double[] totalHeightsPerStrategy = new double[maxStrategiesToUse];
int iBest = -1;
bool copyBoxesForFinalResult = maxStrategiesToUse > 1;
for (int i = 0; i < maxStrategiesToUse; i++)
{
var boxesThisPass = i == 0 ? list.ConvertAll(c => c.box) : list.ConvertAll(c => { c.ReinitBox(); return c.box; });
double totalWidthThisPass;
double totalHeightThisPass;
packer.Pack(boxesThisPass, false, (Packer2DBox.NodeChoosingStrategy)i, out totalWidthThisPass, out totalHeightThisPass);
int thisPassNullPositions = 0;
for (int j = 0; j < boxesThisPass.Count; j++)
{
var b = boxesThisPass[j];
if (b.position == null)
++thisPassNullPositions;
}
boxesPerStrategy[i] = boxesThisPass;
nullPositionsPerStrategy[i] = thisPassNullPositions;
totalWidthsPerStrategy[i] = totalWidthThisPass;
totalHeightsPerStrategy[i] = totalHeightThisPass;
if (iBest == -1)
{
iBest = i;
if (thisPassNullPositions == 0)
{
// Boxes won't be overridden by next strategies, so no need to copy them
copyBoxesForFinalResult = false;
// First pass is Packer2DBox.NodeChoosingStrategy.MAX_VOLUME and all boxes were fit => there's no better strategy
break;
}
continue;
}
if (thisPassNullPositions < nullPositionsPerStrategy[iBest])
{
iBest = i;
continue;
}
if (thisPassNullPositions > nullPositionsPerStrategy[iBest])
continue;
if (totalWidthThisPass * totalHeightThisPass < totalWidthsPerStrategy[i] * totalHeightsPerStrategy[i])
{
iBest = i;
continue;
}
}
if (copyBoxesForFinalResult)
{
var bestBoxes = boxesPerStrategy[iBest];
for (int i = 0; i < list.Count; i++)
list[i].box = bestBoxes[i];
}
////Testing whether first strategy is always better when no nulls are found
//if (nullPositionsPerStrategy[0] == 0 && iBest != 0)
// throw new System.Exception(nullPositionsPerStrategy[0] + ", " + (Packer2DBox.NodeChoosingStrategy)iBest + ", " + nullPositionsPerStrategy[iBest]);
//Debug.Log("Strategy used: " + (Packer2DBox.NodeChoosingStrategy)iBest);
width = (float)totalWidthsPerStrategy[iBest];
height = (float)totalHeightsPerStrategy[iBest];
return list;
}
public enum AxisOrNone
{
Horizontal = RectTransform.Axis.Horizontal,
Vertical = RectTransform.Axis.Vertical,
None
}
class ChildSetup
{
public RectTransform child;
public Packer2DBox.Box box;
public ChildSetup(double width, double height, RectTransform child)
{
this.child = child;
box = new Packer2DBox.Box(width, height);
}
public void ReinitBox() { box = new Packer2DBox.Box(box.width, box.height); }
}
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: e9bdf01a11e37bd46b06dce794365dba
timeCreated: 1552575321
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,293 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Com.ForbiddenByte.OSA.Util
{
/*
License type: MIT
Quoted from github: "A short and simple permissive license with conditions
only requiring preservation of copyright and license notices. Licensed works, modifications,
and larger works may be distributed under different terms and without source code"
*/
/*
Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016 Jake Gordon and contributors
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.
*/
/// <summary>
/// Heavily modified version of https://github.com/cariquitanmac/2D-Bin-Pack-Binary-Search,
/// which is a C# implementation of Jakes Gordon Binary Tree Algorithm for 2D Bin Packing https://github.com/jakesgordon/bin-packing/
/// <para>All rights go to the original author.</para>
/// </summary>
public class Packer2DBox
{
List<Box> _Boxes;
Node _RootNode;
double _Spacing;
bool _AlternatingStrategyBiggerRightNode;
bool _AlternatingOtherStrategyBiggerRightNode;
double _ContainerWidth;
double _ContainerHeight;
NodeChoosingStrategy _ChoosingStrategy;
public Packer2DBox(double containerWidth, double containerHeight, double spacing)
{
this._ContainerWidth = containerWidth;
this._ContainerHeight = containerHeight;
this._Spacing = spacing;
}
public void Pack(List<Box> boxes, bool sort, NodeChoosingStrategy choosingStrategy, out double totalWidth, out double totalHeight)
{
_ChoosingStrategy = choosingStrategy;
_AlternatingStrategyBiggerRightNode = _ChoosingStrategy == NodeChoosingStrategy.ALTERNATING_START_WITH_RIGHT;
_RootNode = new Node(0d, 0d) { height = _ContainerHeight, width = _ContainerWidth };
_Boxes = boxes;
if (sort)
{
// Biggest boxes first with maxside, then secondarily by volume
// More info: https://codeincomplete.com/posts/bin-packing/
_Boxes.Sort((a, b) =>
{
var aMax = System.Math.Max(a.width, a.height);
var bMax = System.Math.Max(b.width, b.height);
if (aMax != bMax)
return (int)(bMax - aMax);
return (int)(b.volume - a.volume);
});
//_Boxes = _Boxes.Sort((a, b) =>
//{
// var aMax = Math.Max(a.width, a.height);
// var bMax = Math.Max(b.width, b.height);
// if (aMax != bMax)
// return (int)(bMax - aMax);
// return (int)(b.volume - a.volume);
//});
//_Boxes = _Boxes.OrderByDescending(x => Math.Max(x.width, x.height)).ToList();
////_Boxes = _Boxes.OrderByDescending(x => x.volume).ToList();
}
totalWidth = 0f;
totalHeight = 0f;
foreach (var box in _Boxes)
{
Node node = null;
FindNode(_RootNode, box.width, box.height, ref node);
if (node != null)
{
// Split rectangles
box.position = SplitNode(node, box.width, box.height);
double width = box.position.x + box.width;
if (width > totalWidth)
totalWidth = width;
double height = box.position.y + box.height;
if (height > totalHeight)
totalHeight = height;
}
}
}
void FindNode(Node rootNode, double boxWidth, double boxHeight, ref Node node)
{
if (rootNode.isOccupied)
{
FindNode(rootNode.rightNode, boxWidth, boxHeight, ref node);
FindNode(rootNode.bottomNode, boxWidth, boxHeight, ref node);
}
else if (boxWidth <= rootNode.width && boxHeight <= rootNode.height)
{
if (node == null || rootNode.distFromOrigin < node.distFromOrigin)
node = rootNode;
}
}
Node SplitNode(Node node, double boxWidth, double boxHeight)
{
node.isOccupied = true;
double rightNodeFullWidth = node.width - (boxWidth + _Spacing);
double rightNodeFullHeight = node.height;
double bottomNodeFullWidth = node.width;
double bottomNodeFullHeight = node.height - (boxHeight + _Spacing);
bool biggerRightNode;
var localStrategy = _ChoosingStrategy;
if (localStrategy == NodeChoosingStrategy.MAX_VOLUME)
{
double rightVolume = rightNodeFullWidth * rightNodeFullHeight;
double bottomVolume = bottomNodeFullWidth * bottomNodeFullHeight;
if (rightVolume == bottomVolume)
{
// In case of equality, alternate between what we chose the last time
biggerRightNode = _AlternatingOtherStrategyBiggerRightNode;
_AlternatingOtherStrategyBiggerRightNode = !_AlternatingOtherStrategyBiggerRightNode;
}
else
{
biggerRightNode = rightVolume > bottomVolume;
}
}
else if (localStrategy == NodeChoosingStrategy.MAX_SIDE)
{
double rightMaxSide = Math.Max(rightNodeFullWidth, rightNodeFullHeight);
double bottomMaxSide = Math.Max(bottomNodeFullWidth, bottomNodeFullHeight);
if (rightMaxSide == bottomMaxSide)
{
// In case of equality, alternate between what we chose the last time
biggerRightNode = _AlternatingOtherStrategyBiggerRightNode;
_AlternatingOtherStrategyBiggerRightNode = !_AlternatingOtherStrategyBiggerRightNode;
}
else
{
biggerRightNode = rightMaxSide > bottomMaxSide;
}
}
else if (localStrategy == NodeChoosingStrategy.MAX_SIDE)
{
double rightMaxSide = Math.Max(rightNodeFullWidth, rightNodeFullHeight);
double bottomMaxSide = Math.Max(bottomNodeFullWidth, bottomNodeFullHeight);
if (rightMaxSide == bottomMaxSide)
{
// In case of equality, alternate between what we chose the last time
biggerRightNode = _AlternatingOtherStrategyBiggerRightNode;
_AlternatingOtherStrategyBiggerRightNode = !_AlternatingOtherStrategyBiggerRightNode;
}
else
{
biggerRightNode = rightMaxSide > bottomMaxSide;
}
}
else if (localStrategy == NodeChoosingStrategy.MAX_SUM)
{
double rightSum = rightNodeFullWidth + rightNodeFullHeight;
double bottomSum = bottomNodeFullWidth + bottomNodeFullHeight;
if (rightSum == bottomSum)
{
// In case of equality, alternate between what we chose the last time
biggerRightNode = _AlternatingOtherStrategyBiggerRightNode;
_AlternatingOtherStrategyBiggerRightNode = !_AlternatingOtherStrategyBiggerRightNode;
}
else
{
biggerRightNode = rightSum > bottomSum;
}
}
else
{
if (_ChoosingStrategy == NodeChoosingStrategy.RIGHT)
biggerRightNode = true;
else if (_ChoosingStrategy == NodeChoosingStrategy.BOTTOM)
biggerRightNode = false;
else
{
// Alternating
biggerRightNode = _AlternatingStrategyBiggerRightNode;
_AlternatingStrategyBiggerRightNode = !_AlternatingStrategyBiggerRightNode;
}
}
node.rightNode = new Node(node.x + (boxWidth + _Spacing), node.y)
{
depth = node.depth + 1,
width = rightNodeFullWidth,
height = biggerRightNode ? node.height : boxHeight
};
node.bottomNode = new Node(node.x, node.y + (boxHeight + _Spacing))
{
depth = node.depth + 1,
width = biggerRightNode ? boxWidth : node.width,
height = bottomNodeFullHeight
};
return node;
}
public class Node
{
public int depth;
public Node rightNode;
public Node bottomNode;
public double x;
public double y;
public double width;
public double height;
readonly public double distFromOrigin;
public bool isOccupied;
public Node(double x, double y)
{
this.x = x;
this.y = y;
distFromOrigin = Math.Sqrt(x * x + y * y);
}
}
public class Box
{
public double height;
public double width;
public double volume;
public Node position;
public Box(double width, double height)
{
this.width = width;
this.height = height;
volume = width * height;
}
}
/// <summary>Note expanding choices, in order of success rate</summary>
public enum NodeChoosingStrategy
{
MAX_VOLUME,
MAX_SUM,
MAX_SIDE,
ALTERNATING_START_WITH_BOTTOM,
RIGHT,
BOTTOM,
ALTERNATING_START_WITH_RIGHT, // same as BOTTOM, in 99% of cases
COUNT_
}
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 363daeb6cdf9ec14185bcc2f8f2f6fc6
timeCreated: 1552581283
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,5 @@
fileFormatVersion: 2
guid: 1fb02192dc3a1c2409414afb729b192b
folderAsset: yes
DefaultImporter:
userData:
@@ -0,0 +1,385 @@
using UnityEngine;
using UnityEngine.UI;
using System;
using UnityEngine.EventSystems;
using UnityEngine.Events;
using frame8.Logic.Misc.Visual.UI;
using frame8.Logic.Misc.Other.Extensions;
namespace Com.ForbiddenByte.OSA.Util.PullToRefresh
{
/// <summary>
/// Attach it to your ScrollView where the pull to refresh functionality is needed.
/// Browse the PullToRefreshExample scene to see how the gizmo should be set up. An image is better than 1k words.
/// </summary>
public class PullToRefreshBehaviour : MonoBehaviour, IScrollRectProxy, IBeginDragHandler, IDragHandler, IEndDragHandler
{
#region Serialized fields
/// <summary>The normalized distance relative to screen size. Always between 0 and 1</summary>
[SerializeField] [Range(.1f, 1f)] [Tooltip("The normalized distance relative to screen size. Always between 0 and 1")]
float _PullAmountNormalized = .25f;
/// <summary>The reference of the gizmo to use. If null, will try to GetComponentInChildren&lt;PullToRefreshGizmo&gt;()</summary>
[SerializeField] [Tooltip("If null, will try to GetComponentInChildren()")]
PullToRefreshGizmo _RefreshGizmo = null;
//[SerializeField]
//RectTransform.Axis _Axis;
/// <summary></summary>
[SerializeField]
bool _AllowPullFromEnd = false;
/// <summary>If false, you'll need to call HideGizmo() manually after pull. Subscribe to PullToRefreshBehaviour.OnRefresh event to know when a refresh event occurred. This method is used when the gizmo should do an animation while the refresh is executing (for ex., when some data is downloading)</summary>
[SerializeField] [Tooltip("If false, you'll need to call HideGizmo() manually after pull. Subscribe to PullToRefreshBehaviour.OnRefresh event to know when a refresh event occurred")]
bool _AutoHideRefreshGizmo = true;
#pragma warning disable 0649
/// <summary>Quick way of playing a sound effect when the pull power reaches 1f</summary>
[SerializeField]
AudioClip _SoundOnPreRefresh = null;
/// <summary>Quick way of playing a sound effect when the refresh occurred</summary>
[SerializeField]
AudioClip _SoundOnRefresh = null;
#endregion
#pragma warning restore 0649
#region Unity events
[Tooltip("Unity event fired when the pull was released")]
/// <summary>Unity event (editable in inspector) fired when the refresh occurred</summary>
public UnityEvent OnRefresh = null;
[Tooltip("Same as OnRefresh, but also gives you the refresh sign.\n" +
"1 = top, -1 = bottom")]
/// <summary>Same as <see cref="OnRefresh"/>, but also gives you the refresh sign. 1 = top, -1 = bottom</summary>
public UnityEventFloat OnRefreshWithSign = null;
[Tooltip("Unity event (editable in inspector) fired when each frame the click/finger is dragged after it has touched the ScrollView.\n" +
"Negative values indicate pulling from end")]
/// <summary>
/// Unity event (editable in inspector) fired when each frame the click/finger is dragged after it has touched the ScrollView.
/// Negative values indicate pulling from end
/// </summary>
public UnityEventFloat OnPullProgress = null;
#endregion
/// <summary>
/// Will be retrieved from the scrollrect. If not found, it can be assigned anytime before the first Update.
/// If not assigned, a default proxy will be used. The purpose of this is to allow custom implementations of ScrollRect to be used
/// </summary>
public IScrollRectProxy externalScrollRectProxy;
#region IScrollRectProxy properties implementation
public bool IsInitialized { get { return _ScrollRect != null; } }
public Vector2 Velocity { get; set; }
public bool IsHorizontal { get { return _ScrollRect.horizontal; } }
public bool IsVertical { get { return _ScrollRect.vertical; } }
public RectTransform Content { get { return _ScrollRect.content; } }
public RectTransform Viewport { get { return _ScrollRect.viewport; } }
double IScrollRectProxy.ContentInsetFromViewportStart { get { return Content.GetInsetFromParentEdge(Viewport, ScrollRectProxy.GetStartEdge()); } }
double IScrollRectProxy.ContentInsetFromViewportEnd { get { return Content.GetInsetFromParentEdge(Viewport, ScrollRectProxy.GetEndEdge()); } }
#endregion
IScrollRectProxy ScrollRectProxy { get { return externalScrollRectProxy == null ? this : externalScrollRectProxy; } }
ScrollRect _ScrollRect;
float _ResolvedAVGScreenSize;
bool _PlayedPreSoundForCurrentDrag;
//bool _IgnoreCurrentDrag;
RectTransform _RT;
int _CurrentDragSign;
StateEnum _State;
/// <summary>Not used in this default interface implementation</summary>
#pragma warning disable 0067
public event Action<double> ScrollPositionChanged = delegate { };
#pragma warning restore 0067
void Awake()
{
_RT = transform as RectTransform;
_ResolvedAVGScreenSize = (Screen.width + Screen.height) / 2f;
_ScrollRect = GetComponent<ScrollRect>();
_RefreshGizmo = GetComponentInChildren<PullToRefreshGizmo>(); // self or children
if (_ScrollRect)
{
// May be null
externalScrollRectProxy = _ScrollRect.GetComponent(typeof(IScrollRectProxy)) as IScrollRectProxy;
}
else
{
externalScrollRectProxy = GetComponentInParent(typeof(IScrollRectProxy)) as IScrollRectProxy;
if (externalScrollRectProxy == null)
{
if (enabled)
{
Debug.Log(GetType().Name + ": no scrollRect provided and found no " + typeof(IScrollRectProxy).Name + " component among ancestors. Disabling...");
enabled = false;
}
return;
}
}
}
#region IScrollRectProxy methods implementation (used if external proxy is not manually assigned)
public void SetNormalizedPosition(double normalizedPosition) { }
public double GetNormalizedPosition()
{
if (_ScrollRect.horizontal)
return _ScrollRect.horizontalNormalizedPosition;
return _ScrollRect.verticalNormalizedPosition;
}
public double GetContentSize() { return _RT.rect.size[_ScrollRect.horizontal ? 0 : 1]; }
public double GetViewportSize() { return Viewport.rect.size[_ScrollRect.horizontal ? 0 : 1]; }
public void StopMovement() { _ScrollRect.StopMovement(); }
#endregion
#region UI callbacks
public void OnBeginDrag(PointerEventData eventData)
{
if (eventData.button != PointerEventData.InputButton.Left)
return;
if (!isActiveAndEnabled)
return;
if (_State != StateEnum.NONE)
return;
if (_RefreshGizmo.IsShown)
return;
if (!ScrollRectProxy.IsInitialized)
return;
double dragAmountNorm, _;
GetDragAmountNormalized(eventData, out dragAmountNorm, out _);
if (!_AllowPullFromEnd && dragAmountNorm < 0d)
return;
int curDragSign = Math.Sign(dragAmountNorm);
_CurrentDragSign = curDragSign;
_PlayedPreSoundForCurrentDrag = false;
_State = StateEnum.DRAGGING_WAITING_FOR_PULL;
}
public void OnDrag(PointerEventData eventData)
{
if (eventData.button != PointerEventData.InputButton.Left)
return;
if (!isActiveAndEnabled)
return;
if (!ScrollRectProxy.IsInitialized)
return;
switch (_State)
{
case StateEnum.DRAGGING_WAITING_FOR_PULL:
if (!IsContentBiggerThanViewport() || IsScrollRectAtTarget(_CurrentDragSign))
{
_State = StateEnum.PULLING_WAITING_FOR_RELEASE;
goto case StateEnum.PULLING_WAITING_FOR_RELEASE;
}
return;
case StateEnum.PULLING_WAITING_FOR_RELEASE:
if (IsContentBiggerThanViewport() && !IsScrollRectAtTarget(_CurrentDragSign))
{
HideGizmoInternal();
_State = StateEnum.DRAGGING_WAITING_FOR_PULL;
return;
}
double dragAmountNorm, deltaNorm;
GetDragAmountNormalized(eventData, out dragAmountNorm, out deltaNorm);
if (Math.Sign(dragAmountNorm) != _CurrentDragSign)
{
return;
}
//if (!_AllowPullFromEnd && dragAmountNorm < 0d)
//{
// HideGizmoInternal();
// return;
//}
double pullPower = dragAmountNorm;
ShowGizmoIfNeeded();
if (_RefreshGizmo)
_RefreshGizmo.OnPull(pullPower);
if (OnPullProgress != null)
OnPullProgress.Invoke((float)pullPower);
if (Math.Abs(pullPower) >= 1d && !_PlayedPreSoundForCurrentDrag)
{
_PlayedPreSoundForCurrentDrag = true;
if (_SoundOnPreRefresh)
AudioSource.PlayClipAtPoint(_SoundOnPreRefresh, Camera.main.transform.position);
}
return;
}
//Debug.Log("eventData.pressPosition=" + eventData.pressPosition + "\n eventData.position=" + eventData.position + "\neventData.scrollDelta="+ eventData.scrollDelta);
}
public void OnEndDrag(PointerEventData eventData)
{
if (eventData.button != PointerEventData.InputButton.Left)
return;
if (!ScrollRectProxy.IsInitialized)
return;
if (_State != StateEnum.PULLING_WAITING_FOR_RELEASE)
{
if (_State == StateEnum.DRAGGING_WAITING_FOR_PULL)
{
HideGizmoInternal();
_State = StateEnum.NONE;
}
return;
}
bool proceedRefresh = false;
if (isActiveAndEnabled)
{
proceedRefresh = true;
if (IsContentBiggerThanViewport())
proceedRefresh = IsScrollRectAtTarget(_CurrentDragSign);
if (proceedRefresh)
{
double dragAmount, _;
GetDragAmountNormalized(eventData, out dragAmount, out _);
if (Math.Sign(dragAmount) != _CurrentDragSign)
proceedRefresh = false;
else if (Math.Abs(dragAmount) < 1d)
proceedRefresh = false;
}
}
if (proceedRefresh)
{
if (OnRefresh != null)
OnRefresh.Invoke();
if (OnRefreshWithSign != null)
OnRefreshWithSign.Invoke(_CurrentDragSign);
if (_RefreshGizmo)
_RefreshGizmo.OnRefreshed(_AutoHideRefreshGizmo);
if (_SoundOnRefresh)
AudioSource.PlayClipAtPoint(_SoundOnRefresh, Camera.main.transform.position);
}
else
{
if (_RefreshGizmo)
_RefreshGizmo.OnRefreshCancelled();
_State = StateEnum.NONE;
}
if (_RefreshGizmo && _RefreshGizmo.IsShown)
{
if (_AutoHideRefreshGizmo)
{
HideGizmoInternal();
_State = StateEnum.NONE;
}
else
{
_State = StateEnum.AFTER_RELEASE_WAITING_FOR_GIZMO_TO_HIDE;
}
}
}
#endregion
// sign: 1=start(top or left), -1=end(bottom or right);
bool IsScrollRectAtTarget(int targetDragSign)
{
double normPos = ScrollRectProxy.GetNormalizedPosition();
if (ScrollRectProxy.IsHorizontal)
normPos = 1d - normPos;
if (targetDragSign == 1 && normPos >= 1d)
return true;
if (targetDragSign == -1 && normPos <= 0d)
return true;
return false;
}
bool IsContentBiggerThanViewport() { return ScrollRectProxy.GetContentSize() > _RT.rect.size[ScrollRectProxy.IsHorizontal ? 0 : 1]; }
public void ShowGizmoIfNeeded()
{
if (_RefreshGizmo && !_RefreshGizmo.IsShown)
_RefreshGizmo.IsShown = true;
}
public void HideGizmo()
{
HideGizmoInternal();
if (_State == StateEnum.AFTER_RELEASE_WAITING_FOR_GIZMO_TO_HIDE)
_State = StateEnum.NONE;
}
void HideGizmoInternal()
{
if (_RefreshGizmo)
_RefreshGizmo.IsShown = false;
}
void GetDragAmountNormalized(PointerEventData eventData, out double total, out double delta)
{
total = 0d;
delta = 0d;
float pos;
float maxPullAmount = _PullAmountNormalized * _ResolvedAVGScreenSize;
if (ScrollRectProxy.IsVertical)
{
pos = eventData.position.y;
float worldDragVec = pos - eventData.pressPosition.y;
total = -worldDragVec;
delta = -eventData.delta.y;
}
else
{
pos = eventData.position.x;
float worldDragVec = pos - eventData.pressPosition.x;
total = worldDragVec;
delta = eventData.delta.x;
}
total /= maxPullAmount;
delta /= maxPullAmount;
}
enum StateEnum
{
NONE,
DRAGGING_WAITING_FOR_PULL,
PULLING_WAITING_FOR_RELEASE,
AFTER_RELEASE_WAITING_FOR_GIZMO_TO_HIDE
}
[Serializable]
public class UnityEventFloat : UnityEvent<float> { }
}
}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8a7fcb71e0128534fb984a6449319591
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
@@ -0,0 +1,50 @@
using UnityEngine;
namespace Com.ForbiddenByte.OSA.Util.PullToRefresh
{
/// <summary> Base class for gizmos that can be used with <see cref="PullToRefreshBehaviour"/> (see it for more details). Attach it to your ScrollView </summary>
public class PullToRefreshGizmo : MonoBehaviour
{
/// <summary>Property that can be overriden by the inheritors. The default implementation is to set whether the game object is active or not</summary>
public virtual bool IsShown
{
get { return _IsShown; }
set
{
_IsShown = value;
gameObject.SetActive(_IsShown);
}
}
bool _IsShown;
public virtual void Awake()
{}
/// <summary>
/// <para>Called for each OnDrag event on the ScrollView. In other words, it's called continuously during moving the mouse/finger after the click</para>
/// </summary>
/// <param name="power">0d = didn't drag at all; .5d = dragged half-way from start; 1d = dragged from start exactly at the minimum needed point in
/// order for a refresh event to occur; values will exceed 1f after this minimum drag amount is exceeded
/// (which can be used to visualize the fact that after the click/finger is released, the refresh will occur).
/// Negative values indicate a pull from the end instead of start, and the same rules apply
/// </param>
public virtual void OnPull(double power)
{}
/// <summary> Called when the refresh did occur (dragged with at least 1f power and released)</summary>
/// <param name="autoHide">A hint for the gizmo to know whether it should hide itself or something will hide it externally by setting <see cref="IsShown"/>=false </param>
public virtual void OnRefreshed(bool autoHide)
{
if (autoHide)
IsShown = false;
}
/// <summary> Called when the click/finger was released before the pullPower reached 1f</summary>
public virtual void OnRefreshCancelled()
{ IsShown = false; }
}
}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e3a396e6841ce6343adc9ea23c888ab8
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
@@ -0,0 +1,159 @@
using UnityEngine;
using System;
using UnityEngine.Serialization;
namespace Com.ForbiddenByte.OSA.Util.PullToRefresh
{
/// <summary>
/// <para> Implementation of <see cref="PullToRefreshGizmo"/> that uses a rotating image to show the pull progress. </para>
/// <para>The image is rotated by the amount of distance traveled by the click/finger.</para>
/// <para>When enough pulling distance is covered the gizmo enters the "ready to refresh" state,</para>
/// <para>the rotation amount applied is damped by <see cref="_ExcessPullRotationDamping"/> (i.e. a value of 1f won't apply any furter rotation, </para>
/// <para>while a value of 0f will apply the same amount of rotation per distance traveled by the click/finger as before the "ready to refresh" state).</para>
/// <para>When <see cref="OnRefreshed(bool)"/> is called with true, the gizmo will disappear; if it'll be called with false, </para>
/// <para>it'll start auto-rotating with a speed of <see cref="_AutoRotationDegreesPerSec"/> degrees per second, until <see cref="IsShown"/> is set to false.</para>
/// <para>This last use-case is very common for when the refresh event actually takes time (i.e. retrieving items from a server).</para>
/// </summary>
public class PullToRefreshRotateGizmo : PullToRefreshGizmo
{
#pragma warning disable 0649
[SerializeField]
[FormerlySerializedAs("_StartingPoint")]
RectTransform _PullFromStartInitial = null;
[SerializeField]
[FormerlySerializedAs("_EndingPoint")]
RectTransform _PullFromStartTarget = null;
[SerializeField]
RectTransform _PullFromEndInitial = null;
[SerializeField]
RectTransform _PullFromEndTarget = null;
#pragma warning restore 0649
//[Tooltip("When pulling is done from the end, this gizmo will also appear at the end, not at the start. " +
// "\nThe gizmo's position in this case is inferred using the parent's size and _StartingPoint & _EndingPoint ")]
//[SerializeField]
//bool _AllowAppearingFromEnd = true;
[SerializeField]
[Range(0f, 1f)]
float _ExcessPullRotationDamping = .95f;
[SerializeField]
float _AutoRotationDegreesPerSec = 200;
[Tooltip("Will also interpolate its own scale between the Initial's and Target's scale")]
[SerializeField]
bool _ScaleWithTarget = true;
[Tooltip("If true, it won't be affected by Time.timeScale")]
[SerializeField]
bool _UseUnscaledTime = true;
bool _WaitingForManualHide;
/// <summary>Calls base implementation + resets the rotation to default each time is assigned, regardless if true or false</summary>
public override bool IsShown
{
get { return base.IsShown; }
set
{
base.IsShown = value;
// Reset to default rotation
transform.localRotation = Quaternion.Euler(_InitialLocalRotation);
if (!value)
_WaitingForManualHide = false;
}
}
Vector3 _InitialLocalRotation, _InitialLocalScale;
Transform _TR;
public override void Awake()
{
base.Awake();
_TR = transform;
_InitialLocalRotation = _TR.localRotation.eulerAngles;
_InitialLocalScale = _TR.localScale;
}
void Update()
{
if (_WaitingForManualHide)
{
SetLocalZRotation((_TR.localEulerAngles.z - (_UseUnscaledTime ? Time.unscaledDeltaTime : Time.deltaTime) * _AutoRotationDegreesPerSec) % 360);
}
}
public override void OnPull(double power)
{
base.OnPull(power);
double powerAbs = Math.Abs(power);
int powerSign = Math.Sign(power);
float powerAbsClamped01 = Mathf.Clamp01((float)powerAbs);
float excess = Mathf.Max(0f, (float)powerAbs - 1f);
float dampedExcess = excess * (1f - _ExcessPullRotationDamping);
SetLocalZRotation((_InitialLocalRotation.z - 360 * (powerAbsClamped01 + dampedExcess)) % 360);
//_TR.position = LerpUnclamped(_StartingPoint.position, _EndingPoint.position, power <= 1f ? (power - (1f - power/2)*(1f-power/2)) : (1 - 1/(1 + excess) ));
Vector3 start, end;
Vector3 scaleStart, scaleEnd;
if (powerSign < 0 && _PullFromEndInitial && _PullFromEndTarget)
{
start = _PullFromEndInitial.position;
end = _PullFromEndTarget.position;
scaleStart = _PullFromEndInitial.localScale;
scaleEnd = _PullFromEndTarget.localScale;
}
else
{
start = _PullFromStartInitial.position;
end = _PullFromStartTarget.position;
scaleStart = _PullFromStartInitial.localScale;
scaleEnd = _PullFromStartTarget.localScale;
}
var t01Unclamped = 2 - 2 / (1 + powerAbsClamped01);
_TR.position = LerpUnclamped(start, end, t01Unclamped);
if (_ScaleWithTarget)
_TR.localScale = LerpUnclamped(scaleStart, scaleEnd, t01Unclamped);
else
_TR.localScale = _InitialLocalScale;
}
public override void OnRefreshCancelled()
{
base.OnRefreshCancelled();
_WaitingForManualHide = false;
}
public override void OnRefreshed(bool autoHide)
{
base.OnRefreshed(autoHide);
_WaitingForManualHide = !autoHide;
}
Vector3 LerpUnclamped(Vector3 from, Vector3 to, float t) { return (1f - t) * from + t * to ; }
void SetLocalZRotation(float zRotation)
{
var rotE = _TR.localRotation.eulerAngles;
rotE.z = zRotation;
_TR.localRotation = Quaternion.Euler(rotE);
}
}
}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 26e83b34c01bd224294affa1aee5a14d
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
@@ -0,0 +1,270 @@
using UnityEngine;
using System;
using frame8.Logic.Misc.Other.Extensions;
using UnityEngine.EventSystems;
using frame8.Logic.Misc.Other;
using UnityEngine.Serialization;
namespace Com.ForbiddenByte.OSA.Util
{
public class RectTransformEdgeDragger : MonoBehaviour, IDragHandler, IPointerDownHandler, IPointerUpHandler
{
public event Action TargetDragged;
[FormerlySerializedAs("draggedRectTransform")]
[SerializeField]
RectTransform _DraggedRectTransform = null;
[FormerlySerializedAs("draggedEdge")]
[SerializeField]
RectTransform.Edge _DraggedEdge = RectTransform.Edge.Left;
[SerializeField]
RectTransform _StartPoint = null;
[SerializeField]
RectTransform _EndPoint = null;
[SerializeField]
[Tooltip("Set to false if the dragger will be automatically dragged by the exact same amount, as a result of being a direct child of the dragged recttransform")]
bool _DragSelf = true;
[SerializeField]
float _DraggedRectTransformMinSize = 1f;
[SerializeField]
float _DraggedRectTransformMaxSize = 0f;
public RectTransform DraggedRectTransform { get { return _DraggedRectTransform; } }
public float DragNormalizedAmount { get { return GetNormPosOnDraggingSegment(GetVEndpointStartToMe()); } }
float DragAreaSize { get { return Vector3.Distance(_StartPoint.localPosition, _EndPoint.localPosition); } }
RectTransform _RT;
RectTransform _MyParent;
Vector2 _StartDragPosInMySpace;
//Vector2 _MyInitialLocalPos;
float _MyInitialInset;
float _DraggedRTInitialInset;
//float _DraggedRTStartInset;
//float _DraggedRTStartSize;
float _DraggedRTInitialSize;
Canvas _Canvas;
bool _Dragging;
void Awake()
{
_RT = (transform as RectTransform);
_MyParent = _RT.parent as RectTransform;
if (_StartPoint.parent != _MyParent || _EndPoint.parent != _MyParent)
throw new UnityException("_StartPoint and _EndPoint should have the same parent as the dragger");
// Get the root canvas
var c = _Canvas = transform.parent.GetComponentInParent<Canvas>();
while (c && c.transform.parent)
{
_Canvas = c;
c = c.transform.parent.GetComponentInParent<Canvas>();
}
}
void Start()
{
//_MyInitialLocalPos = _RT.localPosition;
Reinitialize();
//SetNormalizedPosition(0);
}
public void Reinitialize()
{
_MyInitialInset = GetMyCurrentInsetFromDraggedEdge();
_DraggedRTInitialInset = GetDraggedRTCurrentInsetFromDraggedEdge();
_DraggedRTInitialSize = GetRTSize(_DraggedRectTransform);
}
void IPointerDownHandler.OnPointerDown(PointerEventData ped)
{
var localPos = UIUtils8.Instance.WorldToCanvasLocalPosition(_Canvas, _RT.parent as RectTransform, Camera.main, _RT.position);
_Dragging = localPos != null;
if (!_Dragging)
return;
_StartDragPosInMySpace = localPos.Value;
if (!_DragSelf)
{
Reinitialize();
}
//_DraggedRTStartInset = GetDraggedRTCurrentInsetFromDraggedEdge();
//_DraggedRTStartSize = GetRTSize(_DraggedRectTransform);
}
void IDragHandler.OnDrag(PointerEventData ped)
{
if (!_Dragging)
return;
var cam = ped.pressEventCamera;
Vector2 posInMySpace;
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(_MyParent, ped.position, cam, out posInMySpace))
return;
Vector2 pressPosInMySpace;
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(_MyParent, ped.pressPosition, cam, out pressPosInMySpace))
return;
var dragVectorInMySpace = posInMySpace - pressPosInMySpace;
var parentOfDragged = _DraggedRectTransform.parent as RectTransform;
Vector2 posInDraggedRTSpace;
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(parentOfDragged, ped.position, cam, out posInDraggedRTSpace))
return;
Vector2 pressPosInDraggedRTSpace;
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(parentOfDragged, ped.pressPosition, cam, out pressPosInDraggedRTSpace))
return;
//var dragVectorInDraggedRTSpace = posInDraggedRTSpace - pressPosInDraggedRTSpace;
var rtNewPos = _StartDragPosInMySpace;
var rtNewPosUnclamped = _StartDragPosInMySpace;
rtNewPosUnclamped += dragVectorInMySpace;
//float amount;
//float rectMoveAmount;
float _DraggedRTInsetDelta;
if (_DraggedEdge == RectTransform.Edge.Left || _DraggedEdge == RectTransform.Edge.Right)
{
rtNewPos.x += dragVectorInMySpace.x;
_DraggedRTInsetDelta = dragVectorInMySpace.x * (_DraggedEdge == RectTransform.Edge.Left ? 1f : -1f);
}
else
{
rtNewPos.y += dragVectorInMySpace.y;
_DraggedRTInsetDelta = dragVectorInMySpace.y * (_DraggedEdge == RectTransform.Edge.Bottom ? 1f : -1f);
}
float normPos = GetNormPosOnDraggingSegment(GetVEndPointStartTo(rtNewPosUnclamped));
if (_DragSelf)
{
SetNormalizedPosition(normPos, true);
}
else
{
//Debug.Log(normPos);
//// TODO see why normPos reports at boundary when it actually isn't
//if (normPos == 0f)
//{
// if (_DraggedRTInsetDelta > 0)
// return;
//}
//else if (normPos == 1f)
//{
// if (_DraggedRTInsetDelta < 0)
// return;
//}
float newInset = _DraggedRTInitialInset + _DraggedRTInsetDelta;
float newSize = _DraggedRTInitialSize - _DraggedRTInsetDelta;
if (newSize < _DraggedRectTransformMinSize)
{
float excess = _DraggedRectTransformMinSize - newSize;
newInset -= excess;
newSize += excess;
}
if (_DraggedRectTransformMaxSize != 0f && newSize > _DraggedRectTransformMaxSize)
{
float excess = newSize - _DraggedRectTransformMaxSize;
newInset += excess;
newSize -= excess;
}
SetDraggedRTInsetAndSize(newInset, newSize);
}
if (TargetDragged != null)
TargetDragged();
}
void IPointerUpHandler.OnPointerUp(PointerEventData eventData)
{
if (!_Dragging)
return;
// TODO test if this is still needed
//Reinitialize();
}
Vector2 GetVEndpointStartToMe() { return GetVEndPointStartTo(_RT.localPosition); }
Vector2 GetVEndPointStartTo(Vector2 localPoint)
{
Vector2 endV2 = _EndPoint.localPosition;
return localPoint - endV2;
}
/// <summary>
/// Segment start is the end point (it was at the bottom on the moment of the implementation and it was easier to visualize this way)
/// </summary>
float GetNormPosOnDraggingSegment(Vector2 vSegmentStartToPoint)
{
Vector2 endV2 = _EndPoint.localPosition;
Vector2 startV2 = _StartPoint.localPosition;
// O = end point, A = my pos, B = start point
var oa = vSegmentStartToPoint;
var ob = startV2 - endV2;
return GetNormPosOnSegment(ob, oa);
}
float GetNormPosOnSegment(Vector2 segmentVector, Vector2 vSegmentStartToPoint)
{
var oa = vSegmentStartToPoint;
var ob = segmentVector;
var obNorm = ob / ob.magnitude;
var oaInOBSpace = oa / ob.magnitude; // i.e. considering ob as unit vector
//float dot = Vector2.Dot(oaNorm, obNorm);
float dot = Vector2.Dot(obNorm, oaInOBSpace);
float normPos = 1f - Mathf.Clamp01(dot);
return normPos;
}
public void SetNormalizedPosition(float normalizedPos, bool updateDraggedRT)
{
//var prevLocalPos = transform.localPosition;
//Debug.Log("SetNormalizedPosition " + normalizedPos);
transform.position = Vector3.Lerp(_StartPoint.position, _EndPoint.position, normalizedPos);
if (updateDraggedRT)
UpdateDraggedRTFromDraggerPos();
// Commented: doesn't work very well in the current form
//if (!_DragSelf)
// transform.localPosition = prevLocalPos;
}
void UpdateDraggedRTFromDraggerPos()
{
float myCurrentInset = GetMyCurrentInsetFromDraggedEdge();
float deltaInset = myCurrentInset - _MyInitialInset;
SetDraggedRTInsetAndSize(_DraggedRTInitialInset + deltaInset, _DraggedRTInitialSize - deltaInset);
}
float GetDraggedRTCurrentInsetFromDraggedEdge() { return GetRTCurrentInsetFromDraggedEdge(_DraggedRectTransform); }
float GetMyCurrentInsetFromDraggedEdge() { return GetRTCurrentInsetFromDraggedEdge(_RT); }
float GetRTCurrentInsetFromDraggedEdge(RectTransform rt) { return rt.GetInsetFromParentEdge(rt.parent as RectTransform, _DraggedEdge); }
float GetRTSize(RectTransform rt)
{
float s;
if (_DraggedEdge == RectTransform.Edge.Left || _DraggedEdge == RectTransform.Edge.Right)
s = _DraggedRectTransform.rect.width;
else
s = _DraggedRectTransform.rect.height;
return s;
}
void SetDraggedRTInsetAndSize(float inset, float size)
{
_DraggedRectTransform.SetInsetAndSizeFromParentEdgeWithCurrentAnchors(_DraggedEdge, inset, size);
}
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: c3b69d91db991124a895acd8f3b60111
timeCreated: 1495890159
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: b531d38311ab2f2418f3af57c56e95a9
folderAsset: yes
timeCreated: 1532607286
licenseType: Store
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,32 @@
using System;
using System.Reflection;
namespace Com.ForbiddenByte.OSA.AdditionalComponents
{
public class InputFieldInScrollRectFixer : InputFieldInScrollRectFixerBase
{
MethodInfo _ActivateInputFieldMI;
PropertyInfo _isFocusedPI;
/// <summary>Using reflection so you won't get compile-time errors</summary>
protected override void CacheMethods()
{
var type = _InputField.GetType();
string reqComp = "UnityEngine.UI.InputField";
if (type.FullName != reqComp)
throw new InvalidOperationException("This script can only be attached to a GameObject containing a " + reqComp);
_ActivateInputFieldMI = type.GetMethod("ActivateInputField");
_isFocusedPI = type.GetProperty("isFocused");
}
protected override void ActivateInputField()
{
if (_ActivateInputFieldMI != null)
_ActivateInputFieldMI.Invoke(_InputField, null);
}
protected override bool IsInputFieldFocused() { return (bool)_isFocusedPI.GetValue(_InputField, null); }
}
}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 55f8d870c6da97e45ab5020ef4ae817e
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
@@ -0,0 +1,196 @@
using UnityEngine;
using UnityEngine.UI;
using System;
using UnityEngine.EventSystems;
namespace Com.ForbiddenByte.OSA.AdditionalComponents
{
/// <summary>
/// Utility that allows dragging a ScrollRect even if the PointerDown event has started inside a child InputField (which cancels the dragging by default)
/// </summary>
public abstract class InputFieldInScrollRectFixerBase : MonoBehaviour, IInitializePotentialDragHandler, IBeginDragHandler, IDragHandler, IEndDragHandler, IPointerClickHandler, IPointerDownHandler, IPointerUpHandler, IScrollHandler
{
protected Selectable _InputField;
Image _ImageOnMeIfChild;
const string CHILD_NAME = "InputFieldFixer-Child";
bool _IAmChild;
bool _DragInProgress;
protected virtual void Awake()
{
_InputField = GetComponent<Selectable>();
_IAmChild = _InputField == null;
if (_IAmChild)
{
InitAsChild();
}
else
{
CacheMethods();
InitAsParent();
}
}
protected abstract void CacheMethods();
void OnDisable()
{
_DragInProgress = false;
}
void InitAsParent()
{
var inputFieldImg = _InputField.image;
if (inputFieldImg)
inputFieldImg.raycastTarget = false;
var tr = transform.Find(CHILD_NAME);
GameObject go;
RectTransform goRT;
// The child may already exist if you'll instantiate an existing InputField with InputFieldInScrollRectFixer attached
if (tr == null)
{
go = new GameObject(CHILD_NAME, typeof(RectTransform));
goRT = go.transform as RectTransform;
goRT.SetParent(_InputField.transform, false);
go.AddComponent(GetType() /*add the same component as <this>'s type, i.e. the one for InputField or TMPro.TMP_InputField*/);
}
// Parent not needed anymore
Destroy(this);
}
void InitAsChild()
{
name = CHILD_NAME;
_InputField = transform.parent.GetComponent<Selectable>();
if (!_InputField)
throw new InvalidOperationException("Child InputFieldInScrollRectFixer: InputField not found in parent");
CacheMethods();
var inputFieldImg = _InputField.image;
if (!inputFieldImg)
throw new InvalidOperationException("Child InputFieldInScrollRectFixer: InputField must have an image attached (can be invisible)");
// May have already been created if this is an instance of a another runtime instance
_ImageOnMeIfChild = GetComponent<Image>();
if (!_ImageOnMeIfChild)
{
_ImageOnMeIfChild = gameObject.AddComponent<Image>();
_ImageOnMeIfChild.sprite = inputFieldImg.sprite;
}
var goRT = transform as RectTransform;
goRT.SetAsLastSibling();
goRT.anchorMin = Vector2.zero;
goRT.anchorMax = Vector2.one;
goRT.sizeDelta = Vector2.zero;
_ImageOnMeIfChild.color = Color.clear;
}
protected abstract void ActivateInputField();
protected abstract bool IsInputFieldFocused();
void IPointerDownHandler.OnPointerDown(PointerEventData eventData)
{
var par = GetInputFieldIfActiveOrNextComponentInItsParents<IPointerDownHandler>();
if (par != null)
par.OnPointerDown(eventData);
}
void IPointerUpHandler.OnPointerUp(PointerEventData eventData)
{
var par = GetInputFieldIfActiveOrNextComponentInItsParents<IPointerUpHandler>();
if (par != null)
par.OnPointerUp(eventData);
}
void IPointerClickHandler.OnPointerClick(PointerEventData eventData)
{
if (InputFieldActiveAndFocused())
{
(_InputField as IPointerClickHandler).OnPointerClick(eventData);
return;
}
if (_DragInProgress)
return;
if (eventData.useDragThreshold)
{
var dragDist = Vector2.Distance(eventData.pressPosition, eventData.position);
if (dragDist > EventSystem.current.pixelDragThreshold)
return;
}
if (CanInputFieldBeFocused())
{
ActivateInputField();
return;
}
var par = GetComponentInInputFieldParents<IPointerClickHandler>();
if (par != null)
par.OnPointerClick(eventData);
}
void IInitializePotentialDragHandler.OnInitializePotentialDrag(PointerEventData eventData)
{
if (!InputFieldActiveAndFocused())
{
var par = GetComponentInInputFieldParents<IInitializePotentialDragHandler>();
if (par != null)
par.OnInitializePotentialDrag(eventData);
}
}
void IBeginDragHandler.OnBeginDrag(PointerEventData eventData)
{
_DragInProgress = true;
var par = GetInputFieldIfActiveOrNextComponentInItsParents<IBeginDragHandler>();
if (par != null)
par.OnBeginDrag(eventData);
}
void IDragHandler.OnDrag(PointerEventData eventData)
{
var par = GetInputFieldIfActiveOrNextComponentInItsParents<IDragHandler>();
if (par != null)
par.OnDrag(eventData);
}
void IScrollHandler.OnScroll(PointerEventData eventData)
{
var par = GetInputFieldIfActiveOrNextComponentInItsParents<IScrollHandler>();
if (par != null)
par.OnScroll(eventData);
}
void IEndDragHandler.OnEndDrag(PointerEventData eventData)
{
_DragInProgress = false;
var par = GetInputFieldIfActiveOrNextComponentInItsParents<IEndDragHandler>();
if (par != null)
par.OnEndDrag(eventData);
}
bool InputFieldActiveAndFocused() { return CanInputFieldBeFocused() && IsInputFieldFocused(); }
bool CanInputFieldBeFocused() { return _InputField.isActiveAndEnabled && _InputField.interactable; }
T GetInputFieldIfActiveOrNextComponentInItsParents<T>()
{
if (InputFieldActiveAndFocused())
return (T)(object)_InputField;
return GetComponentInInputFieldParents<T>();
}
T GetComponentInInputFieldParents<T>() { return (T)(object)_InputField.transform.parent.GetComponentInParent(typeof(T)); }
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 800d0ae0d22df834fad82405cbc91e8a
timeCreated: 1563646918
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,36 @@
// Uncomment this if you have TMPro installed and want to use this script
//#define TMPRO_AVAILABLE
using System;
using System.Reflection;
namespace Com.ForbiddenByte.OSA.AdditionalComponents
{
public class InputFieldInScrollRectFixerTMPro : InputFieldInScrollRectFixerBase
{
MethodInfo _ActivateInputFieldMI;
PropertyInfo _isFocusedPI;
/// <summary>Using reflection so you won't get compile-time errors</summary>
protected override void CacheMethods()
{
var type = _InputField.GetType();
string reqComp = "TMPro.TMP_InputField";
if (type.FullName != reqComp)
throw new InvalidOperationException("This script can only be attached to a GameObject containing a " + reqComp);
_ActivateInputFieldMI = type.GetMethod("ActivateInputField");
_isFocusedPI = type.GetProperty("isFocused");
}
protected override void ActivateInputField()
{
if (_ActivateInputFieldMI != null)
_ActivateInputFieldMI.Invoke(_InputField, null);
}
protected override bool IsInputFieldFocused() { return (bool)_isFocusedPI.GetValue(_InputField, null); }
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 6ba1a9fc18218a848a11e8cddd64bdb3
timeCreated: 1563646918
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,21 @@
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Events;
using frame8.Logic.Misc.Other.Extensions;
using frame8.Logic.Misc.Visual.UI;
using frame8.Logic.Misc.Visual.UI.MonoBehaviours;
using UnityEngine.EventSystems;
namespace Com.ForbiddenByte.OSA.Util.ScrollViews
{
public class NonInteractableScrollRect : ScrollRect
{
public override void OnInitializePotentialDrag(PointerEventData eventData) { }
public override void OnBeginDrag(PointerEventData eventData) { }
public override void OnDrag(PointerEventData eventData) { }
public override void OnEndDrag(PointerEventData eventData) { }
public override void OnScroll(PointerEventData data) { }
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 45de622d467858c4d9599c5c2ac8ec4d
timeCreated: 1532623175
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,53 @@
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Events;
using frame8.Logic.Misc.Other.Extensions;
using frame8.Logic.Misc.Visual.UI;
using frame8.Logic.Misc.Visual.UI.MonoBehaviours;
using UnityEngine.EventSystems;
namespace Com.ForbiddenByte.OSA.Util.ScrollViews
{
/// <summary>
/// Provides access to a Unity's ScrollRect through <see cref="IScrollRectProxy"/>.
/// For example, it can be added to a regular ScrollRect so <see cref="ScrollbarFixer8"/> can communicate with it, in case you want to use the <see cref="ScrollbarFixer8"/> without OSA.
/// </summary>
[RequireComponent(typeof(ScrollRect))]
public class UnityScrollRectProxy : MonoBehaviour, IScrollRectProxy
{
#region IScrollRectProxy properties implementation
public bool IsInitialized { get { return ScrollRect != null; } }
public Vector2 Velocity { get { return ScrollRect.velocity; } set { ScrollRect.velocity = value; } }
public bool IsHorizontal { get { return ScrollRect.horizontal; } }
public bool IsVertical { get { return ScrollRect.vertical; } }
public RectTransform Content { get { return ScrollRect.content; } }
public RectTransform Viewport { get { return ScrollRect.viewport; } }
public double ContentInsetFromViewportStart { get { return Content.GetInsetFromParentEdge(Viewport, (this as IScrollRectProxy).GetStartEdge()); } }
public double ContentInsetFromViewportEnd { get { return Content.GetInsetFromParentEdge(Viewport, (this as IScrollRectProxy).GetEndEdge()); } }
#endregion
ScrollRect ScrollRect { get { if (!_ScrollRect) _ScrollRect = GetComponent<ScrollRect>(); return _ScrollRect; } }
ScrollRect _ScrollRect;
void Awake()
{
if (ScrollRect == null)
throw new UnityException(GetType().Name + ": No ScrollRect component found");
}
#region IScrollRectProxy methods implementation
#pragma warning disable 0067
public event System.Action<double> ScrollPositionChanged;
#pragma warning restore 0067
public void SetNormalizedPosition(double normalizedPosition) { if (IsHorizontal) ScrollRect.horizontalNormalizedPosition = (float)normalizedPosition; else ScrollRect.verticalNormalizedPosition = (float)normalizedPosition; }
public double GetNormalizedPosition() { return IsHorizontal ? ScrollRect.horizontalNormalizedPosition : ScrollRect.verticalNormalizedPosition; }
public double GetContentSize() { return IsHorizontal ? Content.rect.width : Content.rect.height; }
public double GetViewportSize() { return IsHorizontal ? Viewport.rect.width : Viewport.rect.height; }
public void StopMovement() { ScrollRect.StopMovement(); }
#endregion
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 7b56953da65ca4d40aa44696aca06058
timeCreated: 1532607872
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,83 @@
using UnityEngine;
using UnityEngine.UI;
using frame8.Logic.Misc.Visual.UI;
using Com.ForbiddenByte.OSA.Core;
namespace Com.ForbiddenByte.OSA.Util
{
[RequireComponent(typeof(Scrollbar))]
public class ScrollbarRotateOnPull : MonoBehaviour
{
[SerializeField]
float _DegreesOfFreedom = 5f;
[SerializeField]
float _RotationSensivity = .5f;
IOSA _Adapter;
RectTransform _HandleRT;
Vector2 _SrollbarPivotOnInit;
void Start()
{
_Adapter = GetComponentInParent<IOSA>();
_HandleRT = transform as RectTransform;
//_HandleRT = _Scrollbar.handleRect;
_SrollbarPivotOnInit = _HandleRT.pivot;
}
void Update()
{
if (_Adapter == null)
return;
float pullAmount01 = 0f;
var piv = _SrollbarPivotOnInit;
int sign = 1;
if (_Adapter.GetContentSizeToViewportRatio() > 1d)
{
var insetStart = _Adapter.ContentVirtualInsetFromViewportStart;
if (insetStart > 0d)
{
if (_Adapter.IsHorizontal)
{
pullAmount01 = (float)(insetStart / _Adapter.BaseParameters.Viewport.rect.width);
piv.x = 0f;
}
else
{
pullAmount01 = (float)(insetStart / _Adapter.BaseParameters.Viewport.rect.height);
piv.y = 1f;
}
}
else
{
var insetEnd = _Adapter.ContentVirtualInsetFromViewportEnd;
if (insetEnd > 0d)
{
sign = -1;
pullAmount01 = (float)(insetEnd / _Adapter.GetContentSize());
if (_Adapter.IsHorizontal)
{
pullAmount01 = (float)(insetEnd / _Adapter.BaseParameters.Viewport.rect.width);
piv.x = 1f;
}
else
{
pullAmount01 = (float)(insetEnd / _Adapter.BaseParameters.Viewport.rect.height);
piv.y = 0f;
}
}
}
}
if (_HandleRT.pivot != piv)
_HandleRT.pivot = piv;
var euler = _HandleRT.localEulerAngles;
// Multiplying argument by _Speed to speed up sine function growth
euler.z = Mathf.Sin(pullAmount01 * _RotationSensivity * Mathf.PI) * _DegreesOfFreedom * sign;
_HandleRT.localEulerAngles = euler;
}
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 04b05756efdf6204aa6664d62e060e57
timeCreated: 1529355226
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: