com.unity.uiextensions.nosa.../Runtime/Scripts/Layout/FancyScrollView/Core/FancyScrollView.cs

216 lines
7.8 KiB
C#

/// Credit setchi (https://github.com/setchi)
/// Sourced from - https://github.com/setchi/FancyScrollView
using System.Collections.Generic;
namespace UnityEngine.UI.Extensions
{
/// <summary>
/// スクロールビューを実装するための抽象基底クラス.
/// 無限スクロールおよびスナップに対応しています.
/// <see cref="FancyScrollView{TItemData, TContext}.Context"/> が不要な場合は
/// 代わりに <see cref="FancyScrollView{TItemData}"/> を使用します.
/// </summary>
/// <typeparam name="TItemData">アイテムのデータ型.</typeparam>
/// <typeparam name="TContext"><see cref="Context"/> の型.</typeparam>
public abstract class FancyScrollView<TItemData, TContext> : MonoBehaviour where TContext : class, new()
{
/// <summary>
/// セル同士の間隔.
/// </summary>
[SerializeField, Range(1e-2f, 1f)] protected float cellInterval = 0.2f;
/// <summary>
/// スクロール位置の基準.
/// </summary>
/// <remarks>
/// たとえば、 <c>0.5</c> を指定してスクロール位置が <c>0</c> の場合, 中央に最初のセルが配置されます.
/// </remarks>
[SerializeField, Range(0f, 1f)] protected float scrollOffset = 0.5f;
/// <summary>
/// セルを循環して配置させるどうか.
/// </summary>
/// <remarks>
/// <c>true</c> にすると最後のセルの後に最初のセル, 最初のセルの前に最後のセルが並ぶようになります.
/// 無限スクロールを実装する場合は <c>true</c> を指定します.
/// </remarks>
[SerializeField] protected bool loop = false;
/// <summary>
/// セルの親要素となる <c>Transform</c>.
/// </summary>
[SerializeField] protected Transform cellContainer = default;
readonly IList<FancyCell<TItemData, TContext>> pool = new List<FancyCell<TItemData, TContext>>();
/// <summary>
/// 初期化済みかどうか.
/// </summary>
protected bool initialized;
/// <summary>
/// 現在のスクロール位置.
/// </summary>
protected float currentPosition;
/// <summary>
/// セルの Prefab.
/// </summary>
protected abstract GameObject CellPrefab { get; }
/// <summary>
/// アイテム一覧のデータ.
/// </summary>
protected IList<TItemData> ItemsSource { get; set; } = new List<TItemData>();
/// <summary>
/// <typeparamref name="TContext"/> のインスタンス.
/// セルとスクロールビュー間で同じインスタンスが共有されます. 情報の受け渡しや状態の保持に使用します.
/// </summary>
protected TContext Context { get; } = new TContext();
/// <summary>
/// 初期化を行います.
/// </summary>
/// <remarks>
/// 最初にセルが生成される直前に呼び出されます.
/// </remarks>
protected virtual void Initialize() { }
/// <summary>
/// 渡されたアイテム一覧に基づいて表示内容を更新します.
/// </summary>
/// <param name="itemsSource">アイテム一覧.</param>
protected virtual void UpdateContents(IList<TItemData> itemsSource)
{
ItemsSource = itemsSource;
Refresh();
}
/// <summary>
/// セルのレイアウトを強制的に更新します.
/// </summary>
protected virtual void Relayout() => UpdatePosition(currentPosition, false);
/// <summary>
/// セルのレイアウトと表示内容を強制的に更新します.
/// </summary>
protected virtual void Refresh() => UpdatePosition(currentPosition, true);
/// <summary>
/// スクロール位置を更新します.
/// </summary>
/// <param name="position">スクロール位置.</param>
protected virtual void UpdatePosition(float position) => UpdatePosition(position, false);
void UpdatePosition(float position, bool forceRefresh)
{
if (!initialized)
{
Initialize();
initialized = true;
}
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)
{
Debug.Assert(CellPrefab != null);
Debug.Assert(cellContainer != null);
var addCount = Mathf.CeilToInt((1f - firstPosition) / cellInterval) - pool.Count;
for (var i = 0; i < addCount; i++)
{
var cell = Instantiate(CellPrefab, cellContainer).GetComponent<FancyCell<TItemData, TContext>>();
if (cell == null)
{
throw new MissingComponentException(string.Format(
"FancyCell<{0}, {1}> component not found in {2}.",
typeof(TItemData).FullName, typeof(TContext).FullName, CellPrefab.name));
}
cell.SetContext(Context);
cell.Initialize();
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
}
/// <summary>
/// <see cref="FancyScrollView{TItemData}"/> のコンテキストクラス.
/// </summary>
public sealed class NullContext { }
/// <summary>
/// スクロールビューを実装するための抽象基底クラス.
/// 無限スクロールおよびスナップに対応しています.
/// </summary>
/// <typeparam name="TItemData"></typeparam>
/// <seealso cref="FancyScrollView{TItemData, TContext}"/>
public abstract class FancyScrollView<TItemData> : FancyScrollView<TItemData, NullContext> { }
}