diff --git a/Editor/FancyScrollView.meta b/Editor/FancyScrollView.meta new file mode 100644 index 0000000..28ed9d7 --- /dev/null +++ b/Editor/FancyScrollView.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 77a80d348c62e93459753f8e704f474d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/FancyScrollView/ScrollerEditor.cs b/Editor/FancyScrollView/ScrollerEditor.cs new file mode 100644 index 0000000..3771ce4 --- /dev/null +++ b/Editor/FancyScrollView/ScrollerEditor.cs @@ -0,0 +1,152 @@ +/// Credit setchi (https://github.com/setchi) +/// Sourced from - https://github.com/setchi/FancyScrollView + +using UnityEditor; +using UnityEditor.AnimatedValues; + +// For manteinance, every new [SerializeField] variable in Scroller must be declared here + +namespace UnityEngine.UI.Extensions +{ + [CustomEditor(typeof(Scroller))] + [CanEditMultipleObjects] + public class ScrollerEditor : Editor + { + SerializedProperty viewport; + SerializedProperty directionOfRecognize; + SerializedProperty movementType; + SerializedProperty elasticity; + SerializedProperty scrollSensitivity; + SerializedProperty inertia; + SerializedProperty decelerationRate; + SerializedProperty snap; + SerializedProperty snapEnable; + SerializedProperty snapVelocityThreshold; + SerializedProperty snapDuration; + SerializedProperty snapEasing; + + AnimBool showElasticity; + AnimBool showInertiaRelatedValues; + AnimBool showSnapEnableRelatedValues; + + void OnEnable() + { + viewport = serializedObject.FindProperty("viewport"); + directionOfRecognize = serializedObject.FindProperty("directionOfRecognize"); + movementType = serializedObject.FindProperty("movementType"); + elasticity = serializedObject.FindProperty("elasticity"); + scrollSensitivity = serializedObject.FindProperty("scrollSensitivity"); + inertia = serializedObject.FindProperty("inertia"); + decelerationRate = serializedObject.FindProperty("decelerationRate"); + snap = serializedObject.FindProperty("snap"); + snapEnable = serializedObject.FindProperty("snap.Enable"); + snapVelocityThreshold = serializedObject.FindProperty("snap.VelocityThreshold"); + snapDuration = serializedObject.FindProperty("snap.Duration"); + snapEasing = serializedObject.FindProperty("snap.Easing"); + + showElasticity = new AnimBool(Repaint); + showInertiaRelatedValues = new AnimBool(Repaint); + showSnapEnableRelatedValues = new AnimBool(Repaint); + SetAnimBools(true); + } + + void OnDisable() + { + showElasticity.valueChanged.RemoveListener(Repaint); + showInertiaRelatedValues.valueChanged.RemoveListener(Repaint); + showSnapEnableRelatedValues.valueChanged.RemoveListener(Repaint); + } + + void SetAnimBools(bool instant) + { + SetAnimBool(showElasticity, !movementType.hasMultipleDifferentValues && movementType.enumValueIndex == (int)Scroller.MovementType.Elastic, instant); + SetAnimBool(showInertiaRelatedValues, !inertia.hasMultipleDifferentValues && inertia.boolValue, instant); + SetAnimBool(showSnapEnableRelatedValues, !snapEnable.hasMultipleDifferentValues && snapEnable.boolValue, instant); + } + + void SetAnimBool(AnimBool a, bool value, bool instant) + { + if (instant) + { + a.value = value; + } + else + { + a.target = value; + } + } + + public override void OnInspectorGUI() + { + SetAnimBools(false); + + serializedObject.Update(); + EditorGUILayout.PropertyField(viewport); + EditorGUILayout.PropertyField(directionOfRecognize); + EditorGUILayout.PropertyField(movementType); + DrawMovementTypeRelatedValue(); + EditorGUILayout.PropertyField(scrollSensitivity); + EditorGUILayout.PropertyField(inertia); + DrawInertiaRelatedValues(); + serializedObject.ApplyModifiedProperties(); + } + + void DrawMovementTypeRelatedValue() + { + using (var group = new EditorGUILayout.FadeGroupScope(showElasticity.faded)) + { + if (!group.visible) + { + return; + } + + using (new EditorGUI.IndentLevelScope()) + { + EditorGUILayout.PropertyField(elasticity); + } + } + } + + void DrawInertiaRelatedValues() + { + using (var group = new EditorGUILayout.FadeGroupScope(showInertiaRelatedValues.faded)) + { + if (!group.visible) + { + return; + } + + using (new EditorGUI.IndentLevelScope()) + { + EditorGUILayout.PropertyField(decelerationRate); + EditorGUILayout.PropertyField(snap); + + using (new EditorGUI.IndentLevelScope()) + { + DrawSnapRelatedValues(); + } + } + } + } + + void DrawSnapRelatedValues() + { + if (snap.isExpanded) + { + EditorGUILayout.PropertyField(snapEnable); + + using (var group = new EditorGUILayout.FadeGroupScope(showSnapEnableRelatedValues.faded)) + { + if (!group.visible) + { + return; + } + + EditorGUILayout.PropertyField(snapVelocityThreshold); + EditorGUILayout.PropertyField(snapDuration); + EditorGUILayout.PropertyField(snapEasing); + } + } + } + } +} diff --git a/Editor/FancyScrollView/ScrollerEditor.cs.meta b/Editor/FancyScrollView/ScrollerEditor.cs.meta new file mode 100644 index 0000000..936aad4 --- /dev/null +++ b/Editor/FancyScrollView/ScrollerEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e82bfdd42ec254849830933cbaf350fa +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Examples.meta b/Examples.meta new file mode 100644 index 0000000..6eb5852 --- /dev/null +++ b/Examples.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 5a0b1710131366b4f82f636d29814556 +folderAsset: yes +timeCreated: 1467468503 +licenseType: Pro +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Layout/FancyScrollView.cs b/Scripts/Layout/FancyScrollView.cs deleted file mode 100644 index 1321307..0000000 --- a/Scripts/Layout/FancyScrollView.cs +++ /dev/null @@ -1,172 +0,0 @@ -/// Credit setchi (https://github.com/setchi) -/// Sourced from - https://github.com/setchi/FancyScrollView - -using System.Collections.Generic; - -namespace UnityEngine.UI.Extensions -{ - public abstract class FancyScrollView : MonoBehaviour where TContext : class - { - [SerializeField, Range(float.Epsilon, 1f)] - float cellInterval = 0; - [SerializeField, Range(0f, 1f)] - float cellOffset = 0; - [SerializeField] - bool loop = false; - [SerializeField] - GameObject cellBase = null; - [SerializeField] - Transform cellContainer = null; - - readonly List> cells = new List>(); - float currentPosition; - - protected List cellData = new List(); - protected TContext Context { get; private set; } - - /// - /// Sets the context. - /// - /// Context. - protected void SetContext(TContext context) - { - Context = context; - - for (int i = 0; i < cells.Count; i++) - { - cells[i].SetContext(context); - } - } - - /// - /// Updates the contents. - /// - protected void UpdateContents() - { - UpdatePosition(currentPosition, true); - } - - /// - /// Updates the scroll position. - /// - /// Position. - /// If set to true force update contents. - protected void UpdatePosition(float position, bool forceUpdateContents = false) - { - currentPosition = position; - - var visibleMinPosition = position - (cellOffset / cellInterval); - var firstCellPosition = (Mathf.Ceil(visibleMinPosition) - visibleMinPosition) * cellInterval; - var dataStartIndex = Mathf.CeilToInt(visibleMinPosition); - var count = 0; - - for (float p = firstCellPosition; p <= 1f; p += cellInterval, count++) - { - if (count >= cells.Count) - { - cells.Add(CreateCell()); - } - } - - count = 0; - - for (float p = firstCellPosition; p <= 1f; p += cellInterval, count++) - { - var dataIndex = dataStartIndex + count; - var cell = cells[GetCircularIndex(dataIndex, cells.Count)]; - - UpdateCell(cell, dataIndex, forceUpdateContents); - - if (cell.gameObject.activeSelf) - { - cell.UpdatePosition(p); - } - } - - while (count < cells.Count) - { - cells[GetCircularIndex(dataStartIndex + count, cells.Count)].SetVisible(false); - count++; - } - } - - /// - /// Updates the cell. - /// - /// Cell. - /// Data index. - /// If set to true force update contents. - void UpdateCell(FancyScrollViewCell cell, int dataIndex, bool forceUpdateContents = false) - { - if (loop) - { - dataIndex = GetCircularIndex(dataIndex, cellData.Count); - } - else if (dataIndex < 0 || dataIndex > cellData.Count - 1) - { - // セルに対応するデータが存在しなければセルを表示しない - cell.SetVisible(false); - return; - } - - if (forceUpdateContents || cell.DataIndex != dataIndex || !cell.IsVisible) - { - cell.DataIndex = dataIndex; - cell.SetVisible(true); - cell.UpdateContent(cellData[dataIndex]); - } - } - - /// - /// Creates the cell. - /// - /// The cell. - FancyScrollViewCell CreateCell() - { - var cellObject = Instantiate(cellBase, cellContainer); - var cell = cellObject.GetComponent>(); - - cell.SetContext(Context); - cell.SetVisible(false); - cell.DataIndex = -1; - - return cell; - } - - /// - /// Gets the circular index. - /// - /// The circular index. - /// Index. - /// Max size. - int GetCircularIndex(int index, int maxSize) - { - return index < 0 ? maxSize - 1 + (index + 1) % maxSize : index % maxSize; - } - -#if UNITY_EDITOR - bool cachedLoop; - float cachedCellInterval, cachedCellOffset; - - void LateUpdate() - { - if (cachedLoop != loop || cachedCellOffset != cellOffset || cachedCellInterval != cellInterval) - { - cachedLoop = loop; - cachedCellOffset = cellOffset; - cachedCellInterval = cellInterval; - - UpdatePosition(currentPosition); - } - } -#endif - } - - public sealed class FancyScrollViewNullContext - { - } - - public abstract class FancyScrollView : FancyScrollView - { - } -} \ No newline at end of file diff --git a/Scripts/Layout/FancyScrollView.meta b/Scripts/Layout/FancyScrollView.meta new file mode 100644 index 0000000..d65ed5a --- /dev/null +++ b/Scripts/Layout/FancyScrollView.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d1ddf7c4a2094c8429c834b8c71bb812 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Layout/FancyScrollView/EasingCore.cs b/Scripts/Layout/FancyScrollView/EasingCore.cs new file mode 100644 index 0000000..877f7ca --- /dev/null +++ b/Scripts/Layout/FancyScrollView/EasingCore.cs @@ -0,0 +1,214 @@ +// +// EasingCore - https://github.com/setchi/EasingCore +// +// The MIT License (MIT) +// +// Copyright (c) 2019 setchi +// +// 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. + +using System; +using UnityEngine; + +namespace UnityEngine.UI.Extensions.EasingCore +{ + public enum Ease + { + Linear, + InBack, + InBounce, + InCirc, + InCubic, + InElastic, + InExpo, + InQuad, + InQuart, + InQuint, + InSine, + OutBack, + OutBounce, + OutCirc, + OutCubic, + OutElastic, + OutExpo, + OutQuad, + OutQuart, + OutQuint, + OutSine, + InOutBack, + InOutBounce, + InOutCirc, + InOutCubic, + InOutElastic, + InOutExpo, + InOutQuad, + InOutQuart, + InOutQuint, + InOutSine, + } + + public static class EasingFunction + { + /// + /// Gets the easing function + /// + /// ease type + /// easing function + public static Func Get(Ease type) + { + switch (type) + { + case Ease.Linear: return Linear; + case Ease.InBack: return InBack; + case Ease.InBounce: return InBounce; + case Ease.InCirc: return InCirc; + case Ease.InCubic: return InCubic; + case Ease.InElastic: return InElastic; + case Ease.InExpo: return InExpo; + case Ease.InQuad: return InQuad; + case Ease.InQuart: return InQuart; + case Ease.InQuint: return InQuint; + case Ease.InSine: return InSine; + case Ease.OutBack: return OutBack; + case Ease.OutBounce: return OutBounce; + case Ease.OutCirc: return OutCirc; + case Ease.OutCubic: return OutCubic; + case Ease.OutElastic: return OutElastic; + case Ease.OutExpo: return OutExpo; + case Ease.OutQuad: return OutQuad; + case Ease.OutQuart: return OutQuart; + case Ease.OutQuint: return OutQuint; + case Ease.OutSine: return OutSine; + case Ease.InOutBack: return InOutBack; + case Ease.InOutBounce: return InOutBounce; + case Ease.InOutCirc: return InOutCirc; + case Ease.InOutCubic: return InOutCubic; + case Ease.InOutElastic: return InOutElastic; + case Ease.InOutExpo: return InOutExpo; + case Ease.InOutQuad: return InOutQuad; + case Ease.InOutQuart: return InOutQuart; + case Ease.InOutQuint: return InOutQuint; + case Ease.InOutSine: return InOutSine; + default: return Linear; + } + } + + static float Linear(float t) => t; + + static float InBack(float t) => t * t * t - t * Mathf.Sin(t * Mathf.PI); + + static float OutBack(float t) => 1f - InBack(1f - t); + + static float InOutBack(float t) => + t < 0.5f + ? 0.5f * InBack(2f * t) + : 0.5f * OutBack(2f * t - 1f) + 0.5f; + + static float InBounce(float t) => 1f - OutBounce(1f - t); + + static float OutBounce(float t) => + t < 4f / 11.0f ? + (121f * t * t) / 16.0f : + t < 8f / 11.0f ? + (363f / 40.0f * t * t) - (99f / 10.0f * t) + 17f / 5.0f : + t < 9f / 10.0f ? + (4356f / 361.0f * t * t) - (35442f / 1805.0f * t) + 16061f / 1805.0f : + (54f / 5.0f * t * t) - (513f / 25.0f * t) + 268f / 25.0f; + + static float InOutBounce(float t) => + t < 0.5f + ? 0.5f * InBounce(2f * t) + : 0.5f * OutBounce(2f * t - 1f) + 0.5f; + + static float InCirc(float t) => 1f - Mathf.Sqrt(1f - (t * t)); + + static float OutCirc(float t) => Mathf.Sqrt((2f - t) * t); + + static float InOutCirc(float t) => + t < 0.5f + ? 0.5f * (1 - Mathf.Sqrt(1f - 4f * (t * t))) + : 0.5f * (Mathf.Sqrt(-((2f * t) - 3f) * ((2f * t) - 1f)) + 1f); + + static float InCubic(float t) => t * t * t; + + static float OutCubic(float t) => InCubic(t - 1f) + 1f; + + static float InOutCubic(float t) => + t < 0.5f + ? 4f * t * t * t + : 0.5f * InCubic(2f * t - 2f) + 1f; + + static float InElastic(float t) => Mathf.Sin(13f * (Mathf.PI * 0.5f) * t) * Mathf.Pow(2f, 10f * (t - 1f)); + + static float OutElastic(float t) => Mathf.Sin(-13f * (Mathf.PI * 0.5f) * (t + 1)) * Mathf.Pow(2f, -10f * t) + 1f; + + static float InOutElastic(float t) => + t < 0.5f + ? 0.5f * Mathf.Sin(13f * (Mathf.PI * 0.5f) * (2f * t)) * Mathf.Pow(2f, 10f * ((2f * t) - 1f)) + : 0.5f * (Mathf.Sin(-13f * (Mathf.PI * 0.5f) * ((2f * t - 1f) + 1f)) * Mathf.Pow(2f, -10f * (2f * t - 1f)) + 2f); + + static float InExpo(float t) => Mathf.Approximately(0.0f, t) ? t : Mathf.Pow(2f, 10f * (t - 1f)); + + static float OutExpo(float t) => Mathf.Approximately(1.0f, t) ? t : 1f - Mathf.Pow(2f, -10f * t); + + static float InOutExpo(float v) => + Mathf.Approximately(0.0f, v) || Mathf.Approximately(1.0f, v) + ? v + : v < 0.5f + ? 0.5f * Mathf.Pow(2f, (20f * v) - 10f) + : -0.5f * Mathf.Pow(2f, (-20f * v) + 10f) + 1f; + + static float InQuad(float t) => t * t; + + static float OutQuad(float t) => -t * (t - 2f); + + static float InOutQuad(float t) => + t < 0.5f + ? 2f * t * t + : -2f * t * t + 4f * t - 1f; + + static float InQuart(float t) => t * t * t * t; + + static float OutQuart(float t) + { + var u = t - 1f; + return u * u * u * (1f - t) + 1f; + } + + static float InOutQuart(float t) => + t < 0.5f + ? 8f * InQuart(t) + : -8f * InQuart(t - 1f) + 1f; + + static float InQuint(float t) => t * t * t * t * t; + + static float OutQuint(float t) => InQuint(t - 1f) + 1f; + + static float InOutQuint(float t) => + t < 0.5f + ? 16f * InQuint(t) + : 0.5f * InQuint(2f * t - 2f) + 1f; + + static float InSine(float t) => Mathf.Sin((t - 1f) * (Mathf.PI * 0.5f)) + 1f; + + static float OutSine(float t) => Mathf.Sin(t * (Mathf.PI * 0.5f)); + + static float InOutSine(float t) => 0.5f * (1f - Mathf.Cos(t * Mathf.PI)); + } +} diff --git a/Scripts/Layout/FancyScrollView/EasingCore.cs.meta b/Scripts/Layout/FancyScrollView/EasingCore.cs.meta new file mode 100644 index 0000000..7cdac42 --- /dev/null +++ b/Scripts/Layout/FancyScrollView/EasingCore.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b5e2a916ad19c74468262e656243bc6f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Layout/FancyScrollView/FancyScrollView.cs b/Scripts/Layout/FancyScrollView/FancyScrollView.cs new file mode 100644 index 0000000..49a3830 --- /dev/null +++ b/Scripts/Layout/FancyScrollView/FancyScrollView.cs @@ -0,0 +1,149 @@ +/// Credit setchi (https://github.com/setchi) +/// Sourced from - https://github.com/setchi/FancyScrollView + +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace UnityEngine.UI.Extensions +{ + public abstract class FancyScrollView : MonoBehaviour where TContext : class, new() + { + [SerializeField, Range(float.Epsilon, 1f)] protected float cellInterval = 0.2f; + [SerializeField, Range(0f, 1f)] protected float scrollOffset = 0.5f; + [SerializeField] protected bool loop = false; + [SerializeField] protected Transform cellContainer = default; + + readonly IList> pool = + new List>(); + + float currentPosition; + + protected abstract GameObject CellPrefab { get; } + protected IList ItemsSource { get; set; } = new List(); + protected TContext Context { get; } = new TContext(); + + /// + /// Updates the contents. + /// + /// Items source. + protected void UpdateContents(IList itemsSource) + { + ItemsSource = itemsSource; + Refresh(); + } + + /// + /// Refreshes the cells. + /// + protected void Refresh() => UpdatePosition(currentPosition, true); + + /// + /// Updates the scroll position. + /// + /// Position. + protected void UpdatePosition(float position) => UpdatePosition(position, false); + + void UpdatePosition(float position, bool forceRefresh) + { + currentPosition = position; + + var p = position - scrollOffset / cellInterval; + var firstIndex = Mathf.CeilToInt(p); + var firstPosition = (Mathf.Ceil(p) - p) * cellInterval; + + if (firstPosition + pool.Count * cellInterval < 1f) + { + ResizePool(firstPosition); + } + + UpdateCells(firstPosition, firstIndex, forceRefresh); + } + + void ResizePool(float firstPosition) + { + if (CellPrefab == null) + { + throw new NullReferenceException(nameof(CellPrefab)); + } + + if (cellContainer == null) + { + throw new MissingComponentException(nameof(cellContainer)); + } + + var addCount = Mathf.CeilToInt((1f - firstPosition) / cellInterval) - pool.Count; + for (var i = 0; i < addCount; i++) + { + var cell = Instantiate(CellPrefab, cellContainer).GetComponent>(); + if (cell == null) + { + throw new MissingComponentException( + $"FancyScrollViewCell<{typeof(TItemData).FullName}, {typeof(TContext).FullName}> " + + $"component not found in {CellPrefab.name}."); + } + + cell.SetupContext(Context); + cell.SetVisible(false); + pool.Add(cell); + } + } + + void UpdateCells(float firstPosition, int firstIndex, bool forceRefresh) + { + for (var i = 0; i < pool.Count; i++) + { + var index = firstIndex + i; + var position = firstPosition + i * cellInterval; + var cell = pool[CircularIndex(index, pool.Count)]; + + if (loop) + { + index = CircularIndex(index, ItemsSource.Count); + } + + if (index < 0 || index >= ItemsSource.Count || position > 1f) + { + cell.SetVisible(false); + continue; + } + + if (forceRefresh || cell.Index != index || !cell.IsVisible) + { + cell.Index = index; + cell.SetVisible(true); + cell.UpdateContent(ItemsSource[index]); + } + + cell.UpdatePosition(position); + } + } + + int CircularIndex(int i, int size) => size < 1 ? 0 : i < 0 ? size - 1 + (i + 1) % size : i % size; + +#if UNITY_EDITOR + bool cachedLoop; + float cachedCellInterval, cachedScrollOffset; + + void LateUpdate() + { + if (cachedLoop != loop || cachedCellInterval != cellInterval || cachedScrollOffset != scrollOffset) + { + cachedLoop = loop; + cachedCellInterval = cellInterval; + cachedScrollOffset = scrollOffset; + + UpdatePosition(currentPosition); + } + } +#endif + } + + public sealed class FancyScrollViewNullContext + { + } + + public abstract class FancyScrollView : FancyScrollView + { + } +} diff --git a/Scripts/Layout/FancyScrollView.cs.meta b/Scripts/Layout/FancyScrollView/FancyScrollView.cs.meta similarity index 69% rename from Scripts/Layout/FancyScrollView.cs.meta rename to Scripts/Layout/FancyScrollView/FancyScrollView.cs.meta index d8ff05a..b969106 100644 --- a/Scripts/Layout/FancyScrollView.cs.meta +++ b/Scripts/Layout/FancyScrollView/FancyScrollView.cs.meta @@ -1,8 +1,7 @@ fileFormatVersion: 2 -guid: 51a6dd27af9048f45a7fc0019884d41e -timeCreated: 1501610618 -licenseType: Free +guid: 0e38f4de7b1b5a5429228884014d12ec MonoImporter: + externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 diff --git a/Scripts/Layout/FancyScrollViewCell.cs b/Scripts/Layout/FancyScrollView/FancyScrollViewCell.cs similarity index 60% rename from Scripts/Layout/FancyScrollViewCell.cs rename to Scripts/Layout/FancyScrollView/FancyScrollViewCell.cs index 7e6a809..ed3ecd2 100644 --- a/Scripts/Layout/FancyScrollViewCell.cs +++ b/Scripts/Layout/FancyScrollView/FancyScrollViewCell.cs @@ -1,21 +1,23 @@ /// Credit setchi (https://github.com/setchi) /// Sourced from - https://github.com/setchi/FancyScrollView +using UnityEngine; + namespace UnityEngine.UI.Extensions { - public abstract class FancyScrollViewCell : MonoBehaviour where TContext : class + public abstract class FancyScrollViewCell : MonoBehaviour where TContext : class, new() { /// /// Gets or sets the index of the data. /// /// The index of the data. - public int DataIndex { get; set; } + public int Index { get; set; } = -1; /// /// Gets a value indicating whether this is visible. /// /// true if is visible; otherwise, false. - public virtual bool IsVisible { get { return gameObject.activeSelf; } } + public virtual bool IsVisible => gameObject.activeSelf; /// /// Gets the context. @@ -24,41 +26,32 @@ namespace UnityEngine.UI.Extensions protected TContext Context { get; private set; } /// - /// Sets the context. + /// Setup the context. /// /// Context. - public virtual void SetContext(TContext context) - { - Context = context; - } + public virtual void SetupContext(TContext context) => Context = context; /// /// Sets the visible. /// /// If set to true visible. - public virtual void SetVisible(bool visible) - { - gameObject.SetActive(visible); - } + public virtual void SetVisible(bool visible) => gameObject.SetActive(visible); /// /// Updates the content. /// /// Item data. - public virtual void UpdateContent(TData itemData) - { - } + public abstract void UpdateContent(TItemData itemData); /// /// Updates the position. /// /// Position. - public virtual void UpdatePosition(float position) - { - } + public abstract void UpdatePosition(float position); } - public abstract class FancyScrollViewCell : FancyScrollViewCell + public abstract class FancyScrollViewCell : FancyScrollViewCell { + public sealed override void SetupContext(FancyScrollViewNullContext context) => base.SetupContext(context); } -} \ No newline at end of file +} diff --git a/Scripts/Layout/FancyScrollViewCell.cs.meta b/Scripts/Layout/FancyScrollView/FancyScrollViewCell.cs.meta similarity index 69% rename from Scripts/Layout/FancyScrollViewCell.cs.meta rename to Scripts/Layout/FancyScrollView/FancyScrollViewCell.cs.meta index 32abbb5..e71ac73 100644 --- a/Scripts/Layout/FancyScrollViewCell.cs.meta +++ b/Scripts/Layout/FancyScrollView/FancyScrollViewCell.cs.meta @@ -1,8 +1,7 @@ fileFormatVersion: 2 -guid: 73c54b1a82a56fb4f906ab8c75f7a030 -timeCreated: 1501610618 -licenseType: Free +guid: 6610dee308450ec40899aeedfd85e972 MonoImporter: + externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 diff --git a/Scripts/Layout/FancyScrollView/Scroller.cs b/Scripts/Layout/FancyScrollView/Scroller.cs new file mode 100644 index 0000000..1cdffca --- /dev/null +++ b/Scripts/Layout/FancyScrollView/Scroller.cs @@ -0,0 +1,359 @@ +/// Credit setchi (https://github.com/setchi) +/// Sourced from - https://github.com/setchi/FancyScrollView + +using System; +using UnityEngine.EventSystems; +using UnityEngine.UI.Extensions.EasingCore; + +namespace UnityEngine.UI.Extensions +{ + public class Scroller : UIBehaviour, IBeginDragHandler, IEndDragHandler, IDragHandler + { + [SerializeField] RectTransform viewport = default; + [SerializeField] ScrollDirection directionOfRecognize = ScrollDirection.Vertical; + [SerializeField] MovementType movementType = MovementType.Elastic; + [SerializeField] float elasticity = 0.1f; + [SerializeField] float scrollSensitivity = 1f; + [SerializeField] bool inertia = true; + [SerializeField] float decelerationRate = 0.03f; + [SerializeField] Snap snap = new Snap { + Enable = true, + VelocityThreshold = 0.5f, + Duration = 0.3f, + Easing = Ease.InOutCubic + }; + + readonly AutoScrollState autoScrollState = new AutoScrollState(); + + Action onValueChanged; + Action onSelectionChanged; + + Vector2 pointerStartLocalPosition; + float dragStartScrollPosition; + float prevScrollPosition; + float currentScrollPosition; + + int totalCount; + + bool dragging; + float velocity; + + enum ScrollDirection + { + Vertical, + Horizontal, + } + + public enum MovementType + { + Unrestricted = ScrollRect.MovementType.Unrestricted, + Elastic = ScrollRect.MovementType.Elastic, + Clamped = ScrollRect.MovementType.Clamped + } + + [Serializable] + class Snap + { + public bool Enable; + public float VelocityThreshold; + public float Duration; + public Ease Easing; + } + + static readonly Func DefaultEasingFunction = EasingFunction.Get(Ease.OutCubic); + + class AutoScrollState + { + public bool Enable; + public bool Elastic; + public float Duration; + public Func EasingFunction; + public float StartTime; + public float EndScrollPosition; + + public Action OnComplete; + + public void Reset() + { + Enable = false; + Elastic = false; + Duration = 0f; + StartTime = 0f; + EasingFunction = DefaultEasingFunction; + EndScrollPosition = 0f; + OnComplete = null; + } + + public void Complete() + { + OnComplete?.Invoke(); + Reset(); + } + } + + public void OnValueChanged(Action callback) => onValueChanged = callback; + + public void OnSelectionChanged(Action callback) => onSelectionChanged = callback; + + public void SetTotalCount(int totalCount) => this.totalCount = totalCount; + + public void ScrollTo(int index, float duration, Action onComplete = null) => ScrollTo(index, duration, Ease.OutCubic, onComplete); + + public void ScrollTo(int index, float duration, Ease easing, Action onComplete = null) => ScrollTo(index, duration, EasingFunction.Get(easing), onComplete); + + public void ScrollTo(int index, float duration, Func easingFunction, Action onComplete = null) + { + if (duration <= 0f) + { + JumpTo(index); + return; + } + + autoScrollState.Reset(); + autoScrollState.Enable = true; + autoScrollState.Duration = duration; + autoScrollState.EasingFunction = easingFunction ?? DefaultEasingFunction; + autoScrollState.StartTime = Time.unscaledTime; + autoScrollState.EndScrollPosition = CalculateDestinationIndex(index); + autoScrollState.OnComplete = onComplete; + + velocity = 0f; + dragStartScrollPosition = currentScrollPosition; + + UpdateSelection(Mathf.RoundToInt(CircularPosition(autoScrollState.EndScrollPosition, totalCount))); + } + + public void JumpTo(int index) + { + autoScrollState.Reset(); + + velocity = 0f; + dragging = false; + + index = CalculateDestinationIndex(index); + + UpdateSelection(index); + UpdatePosition(index); + } + + void IBeginDragHandler.OnBeginDrag(PointerEventData eventData) + { + if (eventData.button != PointerEventData.InputButton.Left) + { + return; + } + + pointerStartLocalPosition = Vector2.zero; + RectTransformUtility.ScreenPointToLocalPointInRectangle( + viewport, + eventData.position, + eventData.pressEventCamera, + out pointerStartLocalPosition); + + dragStartScrollPosition = currentScrollPosition; + dragging = true; + autoScrollState.Reset(); + } + + void IDragHandler.OnDrag(PointerEventData eventData) + { + if (eventData.button != PointerEventData.InputButton.Left) + { + return; + } + + if (!dragging) + { + return; + } + + if (!RectTransformUtility.ScreenPointToLocalPointInRectangle( + viewport, + eventData.position, + eventData.pressEventCamera, + out var localCursor)) + { + return; + } + + var pointerDelta = localCursor - pointerStartLocalPosition; + var position = (directionOfRecognize == ScrollDirection.Horizontal ? -pointerDelta.x : pointerDelta.y) + / ViewportSize + * scrollSensitivity + + dragStartScrollPosition; + + var offset = CalculateOffset(position); + position += offset; + + if (movementType == MovementType.Elastic) + { + if (offset != 0f) + { + position -= RubberDelta(offset, scrollSensitivity); + } + } + + UpdatePosition(position); + } + + void IEndDragHandler.OnEndDrag(PointerEventData eventData) + { + if (eventData.button != PointerEventData.InputButton.Left) + { + return; + } + + dragging = false; + } + + float ViewportSize => directionOfRecognize == ScrollDirection.Horizontal + ? viewport.rect.size.x + : viewport.rect.size.y; + + float CalculateOffset(float position) + { + if (movementType == MovementType.Unrestricted) + { + return 0f; + } + + if (position < 0f) + { + return -position; + } + + if (position > totalCount - 1) + { + return totalCount - 1 - position; + } + + return 0f; + } + + void UpdatePosition(float position) + { + currentScrollPosition = position; + onValueChanged?.Invoke(currentScrollPosition); + } + + void UpdateSelection(int index) => onSelectionChanged?.Invoke(index); + + float RubberDelta(float overStretching, float viewSize) => + (1 - 1 / (Mathf.Abs(overStretching) * 0.55f / viewSize + 1)) * viewSize * Mathf.Sign(overStretching); + + void Update() + { + var deltaTime = Time.unscaledDeltaTime; + var offset = CalculateOffset(currentScrollPosition); + + if (autoScrollState.Enable) + { + var position = 0f; + + if (autoScrollState.Elastic) + { + position = Mathf.SmoothDamp(currentScrollPosition, currentScrollPosition + offset, ref velocity, + elasticity, Mathf.Infinity, deltaTime); + + if (Mathf.Abs(velocity) < 0.01f) + { + position = Mathf.Clamp(Mathf.RoundToInt(position), 0, totalCount - 1); + velocity = 0f; + autoScrollState.Complete(); + } + } + else + { + var alpha = Mathf.Clamp01((Time.unscaledTime - autoScrollState.StartTime) / + Mathf.Max(autoScrollState.Duration, float.Epsilon)); + position = Mathf.LerpUnclamped(dragStartScrollPosition, autoScrollState.EndScrollPosition, + autoScrollState.EasingFunction(alpha)); + + if (Mathf.Approximately(alpha, 1f)) + { + autoScrollState.Complete(); + } + } + + UpdatePosition(position); + } + else if (!dragging && (!Mathf.Approximately(offset, 0f) || !Mathf.Approximately(velocity, 0f))) + { + var position = currentScrollPosition; + + if (movementType == MovementType.Elastic && !Mathf.Approximately(offset, 0f)) + { + autoScrollState.Reset(); + autoScrollState.Enable = true; + autoScrollState.Elastic = true; + + UpdateSelection(Mathf.Clamp(Mathf.RoundToInt(position), 0, totalCount - 1)); + } + else if (inertia) + { + velocity *= Mathf.Pow(decelerationRate, deltaTime); + + if (Mathf.Abs(velocity) < 0.001f) + { + velocity = 0f; + } + + position += velocity * deltaTime; + + if (snap.Enable && Mathf.Abs(velocity) < snap.VelocityThreshold) + { + ScrollTo(Mathf.RoundToInt(currentScrollPosition), snap.Duration, snap.Easing); + } + } + else + { + velocity = 0f; + } + + if (!Mathf.Approximately(velocity, 0f)) + { + if (movementType == MovementType.Clamped) + { + offset = CalculateOffset(position); + position += offset; + + if (Mathf.Approximately(position, 0f) || Mathf.Approximately(position, totalCount - 1f)) + { + velocity = 0f; + UpdateSelection(Mathf.RoundToInt(position)); + } + } + + UpdatePosition(position); + } + } + + if (!autoScrollState.Enable && dragging && inertia) + { + var newVelocity = (currentScrollPosition - prevScrollPosition) / deltaTime; + velocity = Mathf.Lerp(velocity, newVelocity, deltaTime * 10f); + } + + prevScrollPosition = currentScrollPosition; + } + + int CalculateDestinationIndex(int index) => movementType == MovementType.Unrestricted + ? CalculateClosestIndex(index) + : Mathf.Clamp(index, 0, totalCount - 1); + + int CalculateClosestIndex(int index) + { + var diff = CircularPosition(index, totalCount) + - CircularPosition(currentScrollPosition, totalCount); + + if (Mathf.Abs(diff) > totalCount * 0.5f) + { + diff = Mathf.Sign(-diff) * (totalCount - Mathf.Abs(diff)); + } + + return Mathf.RoundToInt(diff + currentScrollPosition); + } + + float CircularPosition(float p, int size) => size < 1 ? 0 : p < 0 ? size - 1 + (p + 1) % size : p % size; + } +} diff --git a/Scripts/Layout/FancyScrollView/Scroller.cs.meta b/Scripts/Layout/FancyScrollView/Scroller.cs.meta new file mode 100644 index 0000000..47ea59a --- /dev/null +++ b/Scripts/Layout/FancyScrollView/Scroller.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 006f67d6afad7c2479f7188b033aea31 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: