216 lines
7.8 KiB
C#
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> { }
|
|
} |