From 0a5444b22bf4a159395d6c598c70a546444b4e2e Mon Sep 17 00:00:00 2001 From: "Simon (darkside) Jackson" Date: Wed, 4 Dec 2019 17:25:31 +0000 Subject: [PATCH] Updated Fancy ScrollView and added Example 8 --- Examples | 2 +- .../FancyScrollView/Core/FancyScrollView.cs | 92 +++++++- .../Core/FancyScrollViewCell.cs | 39 ++-- Scripts/Layout/FancyScrollView/GridView.meta | 8 + .../FancyScrollView/GridView/FancyGridView.cs | 144 ++++++++++++ .../GridView/FancyGridView.cs.meta | 11 + .../GridView/FancyGridViewContext.cs | 23 ++ .../GridView/FancyGridViewContext.cs.meta | 11 + .../GridView/FancyGridViewRow.cs | 81 +++++++ .../GridView/FancyGridViewRow.cs.meta | 11 + .../GridView/IFancyGridViewContext.cs | 18 ++ .../GridView/IFancyGridViewContext.cs.meta | 11 + .../ScrollRect/FancyScrollRect.cs | 205 ++++++++++++++---- .../ScrollRect/FancyScrollRectCell.cs | 25 ++- .../ScrollRect/FancyScrollRectContext.cs | 3 + .../ScrollRect/IFancyScrollRectContext.cs | 4 +- .../FancyScrollView/Scroller/Scroller.cs | 162 ++++++++++++-- 17 files changed, 760 insertions(+), 90 deletions(-) create mode 100644 Scripts/Layout/FancyScrollView/GridView.meta create mode 100644 Scripts/Layout/FancyScrollView/GridView/FancyGridView.cs create mode 100644 Scripts/Layout/FancyScrollView/GridView/FancyGridView.cs.meta create mode 100644 Scripts/Layout/FancyScrollView/GridView/FancyGridViewContext.cs create mode 100644 Scripts/Layout/FancyScrollView/GridView/FancyGridViewContext.cs.meta create mode 100644 Scripts/Layout/FancyScrollView/GridView/FancyGridViewRow.cs create mode 100644 Scripts/Layout/FancyScrollView/GridView/FancyGridViewRow.cs.meta create mode 100644 Scripts/Layout/FancyScrollView/GridView/IFancyGridViewContext.cs create mode 100644 Scripts/Layout/FancyScrollView/GridView/IFancyGridViewContext.cs.meta diff --git a/Examples b/Examples index a5914a9..4878f8d 160000 --- a/Examples +++ b/Examples @@ -1 +1 @@ -Subproject commit a5914a92198efb1422bd66f84e8cde6cb0842b87 +Subproject commit 4878f8d7d74c72a334b6204dffc9cb117a9463f2 diff --git a/Scripts/Layout/FancyScrollView/Core/FancyScrollView.cs b/Scripts/Layout/FancyScrollView/Core/FancyScrollView.cs index c524730..a225142 100644 --- a/Scripts/Layout/FancyScrollView/Core/FancyScrollView.cs +++ b/Scripts/Layout/FancyScrollView/Core/FancyScrollView.cs @@ -5,26 +5,84 @@ namespace UnityEngine.UI.Extensions { + /// + /// スクロールビューを実装するための抽象基底クラス. + /// 無限スクロールおよびスナップに対応しています. + /// が不要な場合は + /// 代わりに を使用します. + /// + /// アイテムのデータ型. + /// の型. public abstract class FancyScrollView : MonoBehaviour where TContext : class, new() { + /// + /// セル同士の間隔. + /// [SerializeField, Range(1e-2f, 1f)] protected float cellInterval = 0.2f; + + /// + /// スクロール位置の基準. + /// + /// + /// たとえば、 0.5 を指定してスクロール位置が 0 の場合, 中央に最初のセルが配置されます. + /// [SerializeField, Range(0f, 1f)] protected float scrollOffset = 0.5f; + + /// + /// セルを循環して配置させるどうか. + /// + /// + /// true にすると最後のセルの後に最初のセル, 最初のセルの前に最後のセルが並ぶようになります. + /// 無限スクロールを実装する場合は true を指定します. + /// [SerializeField] protected bool loop = false; + + /// + /// セルの親要素となる Transform. + /// [SerializeField] protected Transform cellContainer = default; readonly IList> pool = new List>(); + /// + /// 初期化済みかどうか. + /// + protected bool initialized; + + /// + /// 現在のスクロール位置. + /// protected float currentPosition; + /// + /// セルの Prefab. + /// protected abstract GameObject CellPrefab { get; } + + /// + /// アイテム一覧のデータ. + /// protected IList ItemsSource { get; set; } = new List(); + + /// + /// のインスタンス. + /// セルとスクロールビュー間で同じインスタンスが共有されます. 情報の受け渡しや状態の保持に使用します. + /// protected TContext Context { get; } = new TContext(); /// - /// Updates the contents. + /// 初期化を行います. + /// + /// + /// 最初にセルが生成される直前に呼び出されます. + /// + protected virtual void Initialize() { } + + /// + /// 渡されたアイテム一覧に基づいて表示内容を更新します. /// - /// Items source. + /// アイテム一覧. protected virtual void UpdateContents(IList itemsSource) { ItemsSource = itemsSource; @@ -32,18 +90,24 @@ protected virtual void UpdateContents(IList itemsSource) } /// - /// Refreshes the cells. + /// セルの表示内容を更新します. /// protected virtual void Refresh() => UpdatePosition(currentPosition, true); /// - /// Updates the scroll position. + /// スクロール位置を更新します. /// - /// Position. + /// スクロール位置. protected virtual void UpdatePosition(float position) => UpdatePosition(position, false); - protected void UpdatePosition(float position, bool forceRefresh) + void UpdatePosition(float position, bool forceRefresh) { + if (!initialized) + { + Initialize(); + initialized = true; + } + currentPosition = position; var p = position - scrollOffset / cellInterval; @@ -66,7 +130,8 @@ void ResizePool(float firstPosition) var addCount = Mathf.CeilToInt((1f - firstPosition) / cellInterval) - pool.Count; for (var i = 0; i < addCount; i++) { - var cell = Instantiate(CellPrefab, cellContainer).GetComponent>(); + var cell = Instantiate(CellPrefab, cellContainer) + .GetComponent>(); if (cell == null) { throw new MissingComponentException( @@ -118,7 +183,9 @@ void UpdateCells(float firstPosition, int firstIndex, bool forceRefresh) void LateUpdate() { - if (cachedLoop != loop || cachedCellInterval != cellInterval || cachedScrollOffset != scrollOffset) + if (cachedLoop != loop || + cachedCellInterval != cellInterval || + cachedScrollOffset != scrollOffset) { cachedLoop = loop; cachedCellInterval = cellInterval; @@ -130,7 +197,16 @@ void LateUpdate() #endif } + /// + /// のコンテキストクラス. + /// public sealed class FancyScrollViewNullContext { } + /// + /// スクロールビューを実装するための抽象基底クラス. + /// 無限スクロールおよびスナップに対応しています. + /// + /// + /// public abstract class FancyScrollView : FancyScrollView { } } \ No newline at end of file diff --git a/Scripts/Layout/FancyScrollView/Core/FancyScrollViewCell.cs b/Scripts/Layout/FancyScrollView/Core/FancyScrollViewCell.cs index 7628d28..676a9a7 100644 --- a/Scripts/Layout/FancyScrollView/Core/FancyScrollViewCell.cs +++ b/Scripts/Layout/FancyScrollView/Core/FancyScrollViewCell.cs @@ -3,53 +3,64 @@ namespace UnityEngine.UI.Extensions { + /// + /// のセルを実装するための抽象基底クラス. + /// が不要な場合は + /// 代わりに を使用します. + /// + /// アイテムのデータ型. + /// の型. public abstract class FancyScrollViewCell : MonoBehaviour where TContext : class, new() { /// - /// Gets or sets the index of the data. + /// このセルで表示しているデータのインデックス. /// - /// The index of the data. public int Index { get; set; } = -1; /// - /// Gets a value indicating whether this is visible. + /// このセルの可視状態. /// - /// true if is visible; otherwise, false. public virtual bool IsVisible => gameObject.activeSelf; /// - /// Gets the context. + /// の参照. + /// セルとスクロールビュー間で同じインスタンスが共有されます. 情報の受け渡しや状態の保持に使用します. /// - /// The context. protected TContext Context { get; private set; } /// - /// Setup the context. + /// のセットアップを行います. /// - /// Context. + /// コンテキスト. public virtual void SetupContext(TContext context) => Context = context; /// - /// Sets the visible. + /// このセルの可視状態を設定します. /// - /// If set to true visible. + /// 可視状態なら true, 非可視状態なら false. public virtual void SetVisible(bool visible) => gameObject.SetActive(visible); /// - /// Updates the content. + /// アイテムデータに基づいてこのセルの表示内容を更新します. /// - /// Item data. + /// アイテムデータ. public abstract void UpdateContent(TItemData itemData); /// - /// Updates the position. + /// 0.0f ~ 1.0f の値に基づいてこのセルのスクロール位置を更新します. /// - /// Position. + /// ビューポート範囲の正規化されたスクロール位置. public abstract void UpdatePosition(float position); } + /// + /// のセルを実装するための抽象基底クラス. + /// + /// アイテムのデータ型. + /// 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/FancyScrollView/GridView.meta b/Scripts/Layout/FancyScrollView/GridView.meta new file mode 100644 index 0000000..f61d146 --- /dev/null +++ b/Scripts/Layout/FancyScrollView/GridView.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c7768f2982b0142ab876d2bb4b597646 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Layout/FancyScrollView/GridView/FancyGridView.cs b/Scripts/Layout/FancyScrollView/GridView/FancyGridView.cs new file mode 100644 index 0000000..e74f517 --- /dev/null +++ b/Scripts/Layout/FancyScrollView/GridView/FancyGridView.cs @@ -0,0 +1,144 @@ +/// Credit setchi (https://github.com/setchi) +/// Sourced from - https://github.com/setchi/FancyScrollView + +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine.UI.Extensions.EasingCore; + +namespace UnityEngine.UI.Extensions +{ + /// + /// グリッドレイアウトのスクロールビューを実装するための抽象基底クラス. + /// 無限スクロールおよびスナップには対応していません. + /// + /// アイテムのデータ型. + /// の型. + public abstract class FancyGridView : FancyScrollRect + where TContext : class, IFancyScrollRectContext, IFancyGridViewContext, new() + { + /// + /// カラム同士の余白. + /// + [SerializeField] protected float columnSpacing = 0f; + + GameObject cachedRowPrefab; + + /// + /// 行の Prefab. + /// + /// + /// では, + /// を行オブジェクトとして使用します. + /// + protected sealed override GameObject CellPrefab => cachedRowPrefab ?? (cachedRowPrefab = SetupRowTemplate()); + + /// + /// 一行あたりの要素数. + /// + protected abstract int ColumnCount { get; } + + /// + /// セルのテンプレート. + /// + protected abstract FancyScrollViewCell CellTemplate { get; } + + /// + /// 行オブジェクトのテンプレート. + /// + protected abstract FancyGridViewRow RowTemplate { get; } + + /// + /// アイテムの総数. + /// + public int DataCount { get; private set; } + + /// + protected override void Initialize() + { + base.Initialize(); + + Debug.Assert(RowTemplate != null); + Debug.Assert(CellTemplate != null); + Debug.Assert(ColumnCount > 0); + + Context.CellTemplate = CellTemplate.gameObject; + Context.ScrollDirection = Scroller.ScrollDirection; + Context.GetColumnCount = () => ColumnCount; + Context.GetColumnSpacing = () => columnSpacing; + } + + /// + /// 行オブジェクトのセットアップを行います. + /// + /// 行を構成する GameObject. + protected virtual GameObject SetupRowTemplate() + { + var cell = CellTemplate.GetComponent(); + var row = RowTemplate.GetComponent(); + + row.sizeDelta = Scroller.ScrollDirection == ScrollDirection.Horizontal + ? new Vector2(cell.rect.width, row.sizeDelta.y) + : new Vector2(row.sizeDelta.x, cell.rect.height); + + return row.gameObject; + } + + /// + /// 渡されたアイテム一覧に基づいて表示内容を更新します. + /// + /// アイテム一覧. + public virtual void UpdateContents(IList items) + { + DataCount = items.Count; + + var rows = items + .Select((item, index) => (item, index)) + .GroupBy( + x => x.index / ColumnCount, + x => x.item) + .Select(group => group.ToArray()) + .ToArray(); + + UpdateContents(rows); + } + + /// + /// 指定したアイテムの位置まで移動します. + /// + /// アイテムのインデックス. + /// 移動にかける秒数. + /// . + /// 移動が完了した際に呼び出されるコールバック. + public override void ScrollTo(int itemIndex, float duration, Alignment alignment = Alignment.Center, Action onComplete = null) + { + var rowIndex = itemIndex / Context.GetColumnCount(); + base.ScrollTo(rowIndex, duration, alignment, onComplete); + } + + /// + /// 指定したアイテムの位置まで移動します. + /// + /// アイテムのインデックス. + /// 移動にかける秒数. + /// 移動に使用するイージング. + /// . + /// 移動が完了した際に呼び出されるコールバック. + public override void ScrollTo(int itemIndex, float duration, Ease easing, Alignment alignment = Alignment.Center, Action onComplete = null) + { + var rowIndex = itemIndex / Context.GetColumnCount(); + base.ScrollTo(rowIndex, duration, easing, alignment, onComplete); + } + + /// + /// 指定したアイテムの位置までジャンプします. + /// + /// アイテムのインデックス. + /// . + public virtual void JumpTo(int itemIndex, Alignment alignment = Alignment.Center) + { + var rowIndex = itemIndex / Context.GetColumnCount(); + UpdatePosition(rowIndex, alignment); + } + } +} \ No newline at end of file diff --git a/Scripts/Layout/FancyScrollView/GridView/FancyGridView.cs.meta b/Scripts/Layout/FancyScrollView/GridView/FancyGridView.cs.meta new file mode 100644 index 0000000..e1254c5 --- /dev/null +++ b/Scripts/Layout/FancyScrollView/GridView/FancyGridView.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e806c8d18b1ff45bb87e9a5b87ec85e3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Layout/FancyScrollView/GridView/FancyGridViewContext.cs b/Scripts/Layout/FancyScrollView/GridView/FancyGridViewContext.cs new file mode 100644 index 0000000..05090c8 --- /dev/null +++ b/Scripts/Layout/FancyScrollView/GridView/FancyGridViewContext.cs @@ -0,0 +1,23 @@ +/// Credit setchi (https://github.com/setchi) +/// Sourced from - https://github.com/setchi/FancyScrollView + +using System; + +namespace UnityEngine.UI.Extensions +{ + /// + /// のコンテキスト基底クラス. + /// + public class FancyGridViewContext : IFancyGridViewContext, IFancyScrollRectContext + { + Func<(float ScrollSize, float ReuseMargin)> IFancyScrollRectContext.CalculateScrollSize { get; set; } + + GameObject IFancyGridViewContext.CellTemplate { get; set; } + + ScrollDirection IFancyGridViewContext.ScrollDirection { get; set; } + + public Func GetColumnCount { get; set; } + + public Func GetColumnSpacing { get; set; } + } +} \ No newline at end of file diff --git a/Scripts/Layout/FancyScrollView/GridView/FancyGridViewContext.cs.meta b/Scripts/Layout/FancyScrollView/GridView/FancyGridViewContext.cs.meta new file mode 100644 index 0000000..49a8040 --- /dev/null +++ b/Scripts/Layout/FancyScrollView/GridView/FancyGridViewContext.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 954a17398bfb54ee7baac3d7ab7e822c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Layout/FancyScrollView/GridView/FancyGridViewRow.cs b/Scripts/Layout/FancyScrollView/GridView/FancyGridViewRow.cs new file mode 100644 index 0000000..3613bce --- /dev/null +++ b/Scripts/Layout/FancyScrollView/GridView/FancyGridViewRow.cs @@ -0,0 +1,81 @@ +/// Credit setchi (https://github.com/setchi) +/// Sourced from - https://github.com/setchi/FancyScrollView + +using System.Linq; + +namespace UnityEngine.UI.Extensions +{ + /// + /// の行を実装するための抽象基底クラス. + /// + /// アイテムのデータ型. + /// の型. + public abstract class FancyGridViewRow : FancyScrollRectCell + where TContext : class, IFancyScrollRectContext, IFancyGridViewContext, new() + { + /// + /// この行で表示するセルの配列. + /// + protected virtual FancyScrollViewCell[] Cells { get; private set; } + + /// + /// この行で表示するセルの配列をインスタンス化します. + /// + /// この行で表示するセルの配列. + protected virtual FancyScrollViewCell[] InstantiateCells() + { + return Enumerable.Range(0, Context.GetColumnCount()) + .Select(_ => Instantiate(Context.CellTemplate, transform)) + .Select(x => x.GetComponent>()) + .ToArray(); + } + + /// + public override void SetupContext(TContext context) + { + base.SetupContext(context); + + Cells = InstantiateCells(); + Debug.Assert(Cells.Length == Context.GetColumnCount()); + + for (var i = 0; i < Cells.Length; i++) + { + Cells[i].SetupContext(context); + } + } + + /// + public override void UpdateContent(TItemData[] rowContents) + { + for (var i = 0; i < Cells.Length; i++) + { + Cells[i].Index = i + Index * Context.GetColumnCount(); + Cells[i].SetVisible(i < rowContents.Length); + + if (Cells[i].IsVisible) + { + Cells[i].UpdateContent(rowContents[i]); + } + } + } + + /// + public override void UpdatePosition(float position) + { + base.UpdatePosition(position); + + for (var i = 0; i < Cells.Length; i++) + { + Cells[i].UpdatePosition(position); + } + } + + /// + protected override void UpdatePosition(float position, float viewportPosition) + { + transform.localPosition = Context.ScrollDirection == ScrollDirection.Horizontal + ? new Vector2(viewportPosition, transform.localPosition.y) + : new Vector2(transform.localPosition.x, viewportPosition); + } + } +} \ No newline at end of file diff --git a/Scripts/Layout/FancyScrollView/GridView/FancyGridViewRow.cs.meta b/Scripts/Layout/FancyScrollView/GridView/FancyGridViewRow.cs.meta new file mode 100644 index 0000000..dbcb384 --- /dev/null +++ b/Scripts/Layout/FancyScrollView/GridView/FancyGridViewRow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 84300901ad8704c11b39587ed6d87468 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Layout/FancyScrollView/GridView/IFancyGridViewContext.cs b/Scripts/Layout/FancyScrollView/GridView/IFancyGridViewContext.cs new file mode 100644 index 0000000..70221da --- /dev/null +++ b/Scripts/Layout/FancyScrollView/GridView/IFancyGridViewContext.cs @@ -0,0 +1,18 @@ +/// Credit setchi (https://github.com/setchi) +/// Sourced from - https://github.com/setchi/FancyScrollView + +using System; + +namespace UnityEngine.UI.Extensions +{ + /// + /// のコンテキストインターフェース. + /// + public interface IFancyGridViewContext + { + GameObject CellTemplate { get; set; } + ScrollDirection ScrollDirection { get; set; } + Func GetColumnCount { get; set; } + Func GetColumnSpacing { get; set; } + } +} \ No newline at end of file diff --git a/Scripts/Layout/FancyScrollView/GridView/IFancyGridViewContext.cs.meta b/Scripts/Layout/FancyScrollView/GridView/IFancyGridViewContext.cs.meta new file mode 100644 index 0000000..1bed129 --- /dev/null +++ b/Scripts/Layout/FancyScrollView/GridView/IFancyGridViewContext.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3cf2d53d9c81945c28f7c558a7c40ba3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Layout/FancyScrollView/ScrollRect/FancyScrollRect.cs b/Scripts/Layout/FancyScrollView/ScrollRect/FancyScrollRect.cs index b6b76d3..54b5a66 100644 --- a/Scripts/Layout/FancyScrollView/ScrollRect/FancyScrollRect.cs +++ b/Scripts/Layout/FancyScrollView/ScrollRect/FancyScrollRect.cs @@ -7,38 +7,86 @@ namespace UnityEngine.UI.Extensions { + /// + /// ScrollRect スタイルのスクロールビューを実装するための抽象基底クラス. + /// 無限スクロールおよびスナップには対応していません. + /// が不要な場合は + /// 代わりに を使用します. + /// + /// アイテムのデータ型. + /// の型. [RequireComponent(typeof(Scroller))] public abstract class FancyScrollRect : FancyScrollView where TContext : class, IFancyScrollRectContext, new() { + /// + /// スクロール中にセルが再利用されるまでの余白のセル数. + /// + /// + /// 0 を指定するとセルが完全に隠れた直後に再利用されます. + /// 1 以上を指定すると, そのセル数だけ余分にスクロールしてから再利用されます. + /// [SerializeField] protected float reuseCellMarginCount = 0f; - [SerializeField] protected float paddingHead = 0f; - [SerializeField] protected float paddingTail = 0f; - [SerializeField] protected float spacing = 0f; - protected virtual float ScrollLength => 1f / Mathf.Max(cellInterval, 1e-2f) - 1f; - - protected virtual float ViewportLength => ScrollLength - reuseCellMarginCount * 2f; + /// + /// コンテンツ先頭の余白. + /// + [SerializeField] protected float paddingHead = 0f; - protected virtual float MaxScrollPosition => ItemsSource.Count - - ScrollLength - + reuseCellMarginCount * 2f - + (paddingHead + (paddingTail - spacing)) / (CellSize + spacing); + /// + /// コンテンツ末尾の余白. + /// + [SerializeField] protected float paddingTail = 0f; - protected virtual bool ScrollEnabled => MaxScrollPosition > 0f; + /// + /// セル同士の余白. + /// + [SerializeField] protected float spacing = 0f; + /// + /// スクロール可能かどうか. + /// + /// + /// アイテム数が十分少なくビューポート内に全てのセルが収まっている場合は false, それ以外は true になります. + /// + protected virtual bool Scrollable => MaxScrollPosition > 0f; + + /// + /// セルのサイズ. + /// protected virtual float CellSize => Scroller.ScrollDirection == ScrollDirection.Horizontal ? CellRectTransform.rect.width : CellRectTransform.rect.height; Scroller cachedScroller; + + /// + /// スクロール位置を制御する のインスタンス. + /// + /// + /// のスクロール位置を変更する際は必ず を使用して変換した位置を使用してください. + /// protected Scroller Scroller => cachedScroller ?? (cachedScroller = GetComponent()); RectTransform cachedCellRect; RectTransform CellRectTransform => cachedCellRect ?? (cachedCellRect = CellPrefab.transform as RectTransform); - protected virtual void Awake() + float ScrollLength => 1f / Mathf.Max(cellInterval, 1e-2f) - 1f; + + float ViewportLength => ScrollLength - reuseCellMarginCount * 2f; + + float PaddingHeadLength => (paddingHead - spacing * 0.5f) / (CellSize + spacing); + + float MaxScrollPosition => ItemsSource.Count + - ScrollLength + + reuseCellMarginCount * 2f + + (paddingHead + paddingTail - spacing) / (CellSize + spacing); + + /// + protected override void Initialize() { + base.Initialize(); + Context.CalculateScrollSize = () => { var interval = CellSize + spacing; @@ -46,17 +94,18 @@ protected virtual void Awake() var scrollSize = Scroller.ViewportSize + interval + reuseMargin * 2f; return (scrollSize, reuseMargin); }; - } - protected virtual void Start() - { AdjustCellIntervalAndScrollOffset(); Scroller.OnValueChanged(OnScrollerValueChanged); } + /// + /// のスクロール位置が変更された際の処理. + /// + /// のスクロール位置. void OnScrollerValueChanged(float p) { - base.UpdatePosition(ScrollEnabled ? ToFancyScrollViewPosition(p) : 0f); + base.UpdatePosition(Scrollable ? ToFancyScrollViewPosition(p) : 0f); if (Scroller.Scrollbar) { @@ -71,78 +120,139 @@ void OnScrollerValueChanged(float p) } } + /// + /// スクロール範囲を超えてスクロールされた量に基づいて, スクロールバーのサイズを縮小します. + /// + /// スクロール範囲を超えてスクロールされた量. void ShrinkScrollbar(float offset) { - var scale = 1f - ToFancyScrollViewPosition(offset) / ViewportLength; - UpdateScrollbarSize(ViewportLength * scale / Mathf.Max(ItemsSource.Count, 1)); + var scale = 1f - ToFancyScrollViewPosition(offset) / (ViewportLength - PaddingHeadLength); + UpdateScrollbarSize((ViewportLength - PaddingHeadLength) * scale); } - protected override void UpdateContents(IList items) + /// + protected override void Refresh() { - Debug.Assert(Context.CalculateScrollSize != null); - AdjustCellIntervalAndScrollOffset(); - base.UpdateContents(items); + RefreshScroller(); + base.Refresh(); + } - Scroller.SetTotalCount(items.Count); - Scroller.Draggable = ScrollEnabled; - Scroller.ScrollSensitivity = ToScrollerPosition(ViewportLength); + /// + /// の各種状態を更新します. + /// + protected void RefreshScroller() + { + Scroller.Draggable = Scrollable; + Scroller.ScrollSensitivity = ToScrollerPosition(ViewportLength - PaddingHeadLength); Scroller.Position = ToScrollerPosition(currentPosition); if (Scroller.Scrollbar) { - Scroller.Scrollbar.gameObject.SetActive(ScrollEnabled); - UpdateScrollbarSize(ViewportLength / Mathf.Max(ItemsSource.Count, 1)); + Scroller.Scrollbar.gameObject.SetActive(Scrollable); + UpdateScrollbarSize(ViewportLength); } } + /// + protected override void UpdateContents(IList items) + { + Debug.Assert(Context.CalculateScrollSize != null); + + AdjustCellIntervalAndScrollOffset(); + base.UpdateContents(items); + + Scroller.SetTotalCount(items.Count); + RefreshScroller(); + } + + /// + /// スクロール位置を更新します. + /// + /// スクロール位置. protected new void UpdatePosition(float position) { UpdatePosition(position, Alignment.Center); } + /// + /// スクロール位置を更新します. + /// + /// スクロール位置. + /// . protected virtual void UpdatePosition(float position, Alignment alignment) { Scroller.Position = ToScrollerPosition(position, alignment); } + /// + /// 指定したアイテムの位置まで移動します. + /// + /// アイテムのインデックス. + /// 移動にかける秒数. + /// . + /// 移動が完了した際に呼び出されるコールバック. public virtual void ScrollTo(int index, float duration, Alignment alignment = Alignment.Center, Action onComplete = null) { Scroller.ScrollTo(ToScrollerPosition(index, alignment), duration, onComplete); } + /// + /// 指定したアイテムの位置まで移動します. + /// + /// アイテムのインデックス. + /// 移動にかける秒数. + /// 移動に使用するイージング. + /// . + /// 移動が完了した際に呼び出されるコールバック. public virtual void ScrollTo(int index, float duration, Ease easing, Alignment alignment = Alignment.Center, Action onComplete = null) { Scroller.ScrollTo(ToScrollerPosition(index, alignment), duration, easing, onComplete); } - protected void UpdateScrollbarSize(float size) + /// + /// ビューポートとコンテンツの長さに基づいてスクロールバーのサイズを更新します. + /// + /// ビューポートのサイズ. + protected void UpdateScrollbarSize(float viewportLength) { - Scroller.Scrollbar.size = ScrollEnabled ? Mathf.Clamp01(size) : 1f; + var contentLength = Mathf.Max(ItemsSource.Count + (paddingHead + paddingTail - spacing) / (CellSize + spacing), 1); + Scroller.Scrollbar.size = Scrollable ? Mathf.Clamp01(viewportLength / contentLength) : 1f; } - protected virtual float ToFancyScrollViewPosition(float position) + /// + /// が扱うスクロール位置を が扱うスクロール位置に変換します. + /// + /// が扱うスクロール位置. + /// が扱うスクロール位置. + protected float ToFancyScrollViewPosition(float position) { - return position - / Mathf.Max(ItemsSource.Count - 1, 1) - * MaxScrollPosition - - (paddingHead - spacing * 0.5f) / (CellSize + spacing); + return position / Mathf.Max(ItemsSource.Count - 1, 1) * MaxScrollPosition - PaddingHeadLength; } - protected virtual float ToScrollerPosition(float position) + /// + /// が扱うスクロール位置を が扱うスクロール位置に変換します. + /// + /// が扱うスクロール位置. + /// が扱うスクロール位置. + protected float ToScrollerPosition(float position) { - return (position + (paddingHead - spacing * 0.5f) / (CellSize + spacing)) - / MaxScrollPosition - * Mathf.Max(ItemsSource.Count - 1, 1); + return (position + PaddingHeadLength) / MaxScrollPosition * Mathf.Max(ItemsSource.Count - 1, 1); } - protected virtual float ToScrollerPosition(float position, Alignment alignment = Alignment.Center) + /// + /// が扱うスクロール位置を が扱うスクロール位置に変換します. + /// + /// が扱うスクロール位置. + /// . + /// が扱うスクロール位置. + protected float ToScrollerPosition(float position, Alignment alignment = Alignment.Center) { var offset = (ScrollLength - (1f + reuseCellMarginCount * 2f)) * GetAnchore(alignment); return ToScrollerPosition(Mathf.Clamp(position - offset, 0f, MaxScrollPosition)); } - protected virtual float GetAnchore(Alignment alignment) + float GetAnchore(Alignment alignment) { switch (alignment) { @@ -153,7 +263,12 @@ protected virtual float GetAnchore(Alignment alignment) } } - void AdjustCellIntervalAndScrollOffset() + /// + /// 指定された設定を実現するための + /// と + /// を計算して適用します. + /// + protected void AdjustCellIntervalAndScrollOffset() { var totalSize = Scroller.ViewportSize + (CellSize + spacing) * (1f + reuseCellMarginCount * 2f); cellInterval = (CellSize + spacing) / totalSize; @@ -187,5 +302,11 @@ protected virtual void OnValidate() } } + /// + /// ScrollRect スタイルのスクロールビューを実装するための抽象基底クラス. + /// 無限スクロールおよびスナップには対応していません. + /// + /// アイテムのデータ型. + /// public abstract class FancyScrollRect : FancyScrollRect { } -} +} \ No newline at end of file diff --git a/Scripts/Layout/FancyScrollView/ScrollRect/FancyScrollRectCell.cs b/Scripts/Layout/FancyScrollView/ScrollRect/FancyScrollRectCell.cs index 6136b81..4e368c9 100644 --- a/Scripts/Layout/FancyScrollView/ScrollRect/FancyScrollRectCell.cs +++ b/Scripts/Layout/FancyScrollView/ScrollRect/FancyScrollRectCell.cs @@ -3,9 +3,17 @@ namespace UnityEngine.UI.Extensions { + /// + /// のセルを実装するための抽象基底クラス. + /// が不要な場合は + /// 代わりに を使用します. + /// + /// アイテムのデータ型. + /// の型. public abstract class FancyScrollRectCell : FancyScrollViewCell where TContext : class, IFancyScrollRectContext, new() { + /// public override void UpdatePosition(float position) { var (scrollSize, reuseMargin) = Context.CalculateScrollSize(); @@ -18,11 +26,26 @@ public override void UpdatePosition(float position) UpdatePosition(unclampedPosition, Mathf.Lerp(start, end, position)); } + /// + /// このセルの位置を更新します. + /// + /// + /// ビューポートの範囲で正規化されたスクロール位置. + /// の値に基づいて + /// 0.0 ~ 1.0 の範囲を超えた値が渡されることがあります. + /// + /// ローカル位置. protected virtual void UpdatePosition(float position, float viewportPosition) { } } + /// + /// のセルを実装するための抽象基底クラス. + /// + /// アイテムのデータ型. + /// public abstract class FancyScrollRectCell : FancyScrollRectCell { + /// public sealed override void SetupContext(FancyScrollRectContext context) => base.SetupContext(context); } -} +} \ No newline at end of file diff --git a/Scripts/Layout/FancyScrollView/ScrollRect/FancyScrollRectContext.cs b/Scripts/Layout/FancyScrollView/ScrollRect/FancyScrollRectContext.cs index 2e5ea39..17c113f 100644 --- a/Scripts/Layout/FancyScrollView/ScrollRect/FancyScrollRectContext.cs +++ b/Scripts/Layout/FancyScrollView/ScrollRect/FancyScrollRectContext.cs @@ -5,6 +5,9 @@ namespace UnityEngine.UI.Extensions { + /// + /// のコンテキスト基底クラス. + /// public class FancyScrollRectContext : IFancyScrollRectContext { Func<(float ScrollSize, float ReuseMargin)> IFancyScrollRectContext.CalculateScrollSize { get; set; } diff --git a/Scripts/Layout/FancyScrollView/ScrollRect/IFancyScrollRectContext.cs b/Scripts/Layout/FancyScrollView/ScrollRect/IFancyScrollRectContext.cs index 8ba33dd..0a83479 100644 --- a/Scripts/Layout/FancyScrollView/ScrollRect/IFancyScrollRectContext.cs +++ b/Scripts/Layout/FancyScrollView/ScrollRect/IFancyScrollRectContext.cs @@ -5,7 +5,9 @@ namespace UnityEngine.UI.Extensions { - + /// + /// のコンテキストインターフェース. + /// public interface IFancyScrollRectContext { Func<(float ScrollSize, float ReuseMargin)> CalculateScrollSize { get; set; } diff --git a/Scripts/Layout/FancyScrollView/Scroller/Scroller.cs b/Scripts/Layout/FancyScrollView/Scroller/Scroller.cs index 31a2801..262cabc 100644 --- a/Scripts/Layout/FancyScrollView/Scroller/Scroller.cs +++ b/Scripts/Layout/FancyScrollView/Scroller/Scroller.cs @@ -7,15 +7,82 @@ namespace UnityEngine.UI.Extensions { + /// + /// スクロール位置の制御を行うコンポーネント. + /// public class Scroller : UIBehaviour, IBeginDragHandler, IEndDragHandler, IDragHandler { [SerializeField] RectTransform viewport = default; + + /// + /// ビューポートのサイズ. + /// + public float ViewportSize => scrollDirection == ScrollDirection.Horizontal + ? viewport.rect.size.x + : viewport.rect.size.y; + [SerializeField] ScrollDirection scrollDirection = ScrollDirection.Vertical; + + /// + /// スクロール方向. + /// + public ScrollDirection ScrollDirection => scrollDirection; + [SerializeField] MovementType movementType = MovementType.Elastic; + + /// + /// コンテンツがスクロール範囲を越えて移動するときに使用する挙動. + /// + public MovementType MovementType + { + get => movementType; + set => movementType = value; + } + [SerializeField] float elasticity = 0.1f; + + /// + /// コンテンツがスクロール範囲を越えて移動するときに使用する弾力性の量. + /// + public float Elasticity + { + get => elasticity; + set => elasticity = value; + } + [SerializeField] float scrollSensitivity = 1f; + + /// + /// の端から端まで Drag したときのスクロール位置の変化量. + /// + public float ScrollSensitivity + { + get => scrollSensitivity; + set => scrollSensitivity = value; + } + [SerializeField] bool inertia = true; + + /// + /// 慣性を使用するかどうか. true を指定すると慣性が有効に, false を指定すると慣性が無効になります. + /// + public bool Inertia + { + get => inertia; + set => inertia = value; + } + [SerializeField] float decelerationRate = 0.03f; + + /// + /// スクロールの減速率. true の場合のみ有効です. + /// + public float DecelerationRate + { + get => decelerationRate; + set => decelerationRate = value; + } + [SerializeField] Snap snap = new Snap { @@ -24,41 +91,41 @@ public class Scroller : UIBehaviour, IBeginDragHandler, IEndDragHandler, IDragHa Duration = 0.3f, Easing = Ease.InOutCubic }; - [SerializeField] bool draggable = true; - [SerializeField] Scrollbar scrollbar = default; - - public ScrollDirection ScrollDirection => scrollDirection; - - public MovementType MovementType - { - get => movementType; - set => movementType = value; - } - - public float ViewportSize => scrollDirection == ScrollDirection.Horizontal - ? viewport.rect.size.x - : viewport.rect.size.y; + /// + /// true ならスナップし, falseならスナップしません. + /// + /// + /// スナップを有効にすると, 慣性でスクロールが止まる直前に最寄りのセルへ移動します. + /// public bool SnapEnabled { get => snap.Enable; set => snap.Enable = value; } - public float ScrollSensitivity - { - get => scrollSensitivity; - set => scrollSensitivity = value; - } + [SerializeField] bool draggable = true; + /// + /// Drag 入力を受付けるかどうか. + /// public bool Draggable { get => draggable; set => draggable = value; } + [SerializeField] Scrollbar scrollbar = default; + + /// + /// スクロールバーのオブジェクト. + /// public Scrollbar Scrollbar => scrollbar; + /// + /// 現在のスクロール位置. + /// + /// public float Position { get => currentPosition; @@ -137,16 +204,51 @@ protected override void Start() } } + /// + /// スクロール位置が変化したときのコールバックを設定します. + /// + /// スクロール位置が変化したときのコールバック. public void OnValueChanged(Action callback) => onValueChanged = callback; + /// + /// 選択位置が変化したときのコールバックを設定します. + /// + /// 選択位置が変化したときのコールバック. public void OnSelectionChanged(Action callback) => onSelectionChanged = callback; + /// + /// アイテムの総数を設定します. + /// + /// + /// を元に最大スクロール位置を計算します. + /// + /// アイテムの総数. public void SetTotalCount(int totalCount) => this.totalCount = totalCount; + /// + /// 指定した位置まで移動します. + /// + /// スクロール位置. 0f ~ totalCount - 1f の範囲. + /// 移動にかける秒数. + /// 移動が完了した際に呼び出されるコールバック. public void ScrollTo(float position, float duration, Action onComplete = null) => ScrollTo(position, duration, Ease.OutCubic, onComplete); + /// + /// 指定した位置まで移動します. + /// + /// スクロール位置. 0f ~ totalCount - 1f の範囲. + /// 移動にかける秒数. + /// 移動に使用するイージング. + /// 移動が完了した際に呼び出されるコールバック. public void ScrollTo(float position, float duration, Ease easing, Action onComplete = null) => ScrollTo(position, duration, EasingFunction.Get(easing), onComplete); + /// + /// 指定した位置まで移動します. + /// + /// スクロール位置. 0f ~ totalCount - 1f の範囲. + /// 移動にかける秒数. + /// 移動に使用するイージング関数. + /// 移動が完了した際に呼び出されるコールバック. public void ScrollTo(float position, float duration, Func easingFunction, Action onComplete = null) { if (duration <= 0f) @@ -170,6 +272,10 @@ public void ScrollTo(float position, float duration, Func easingFu UpdateSelection(Mathf.RoundToInt(CircularPosition(autoScrollState.EndPosition, totalCount))); } + /// + /// 指定したインデックスの位置までジャンプします. + /// + /// アイテムのインデックス. public void JumpTo(int index) { if (index < 0 || index > totalCount - 1) @@ -186,6 +292,13 @@ public void JumpTo(int index) UpdatePosition(index); } + /// + /// から に移動する際の移動方向を返します. + /// スクロール範囲が無制限に設定されている場合は, 最短距離の移動方向を返します. + /// + /// 移動元のインデックス. + /// 移動先のインデックス. + /// public MovementDirection GetMovementDirection(int sourceIndex, int destIndex) { var movementAmount = CalculateMovementAmount(sourceIndex, destIndex); @@ -198,6 +311,7 @@ public MovementDirection GetMovementDirection(int sourceIndex, int destIndex) : MovementDirection.Down; } + /// void IBeginDragHandler.OnBeginDrag(PointerEventData eventData) { if (!draggable || eventData.button != PointerEventData.InputButton.Left) @@ -216,6 +330,7 @@ void IBeginDragHandler.OnBeginDrag(PointerEventData eventData) autoScrollState.Reset(); } + /// void IDragHandler.OnDrag(PointerEventData eventData) { if (!draggable || eventData.button != PointerEventData.InputButton.Left || !dragging) @@ -252,6 +367,7 @@ void IDragHandler.OnDrag(PointerEventData eventData) UpdatePosition(position); } + /// void IEndDragHandler.OnEndDrag(PointerEventData eventData) { if (!draggable || eventData.button != PointerEventData.InputButton.Left) @@ -400,14 +516,14 @@ float CalculateMovementAmount(float sourcePosition, float destPosition) return Mathf.Clamp(destPosition, 0, totalCount - 1) - sourcePosition; } - var movementAmount = CircularPosition(destPosition, totalCount) - CircularPosition(sourcePosition, totalCount); + var amount = CircularPosition(destPosition, totalCount) - CircularPosition(sourcePosition, totalCount); - if (Mathf.Abs(movementAmount) > totalCount * 0.5f) + if (Mathf.Abs(amount) > totalCount * 0.5f) { - movementAmount = Mathf.Sign(-movementAmount) * (totalCount - Mathf.Abs(movementAmount)); + amount = Mathf.Sign(-amount) * (totalCount - Mathf.Abs(amount)); } - return movementAmount; + return amount; } float CircularPosition(float p, int size) => size < 1 ? 0 : p < 0 ? size - 1 + (p + 1) % size : p % size;