/// 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, 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();
///
/// 初期化を行います.
///
///
/// 最初にセルが生成される直前に呼び出されます.
///
protected virtual void Initialize() { }
///
/// 渡されたアイテム一覧に基づいて表示内容を更新します.
///
/// アイテム一覧.
protected virtual void UpdateContents(IList itemsSource)
{
ItemsSource = itemsSource;
Refresh();
}
///
/// セルの表示内容を更新します.
///
protected virtual void Refresh() => UpdatePosition(currentPosition, true);
///
/// スクロール位置を更新します.
///
/// スクロール位置.
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>();
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 { }
}