| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220 |
- /*
- * FancyScrollView (https://github.com/setchi/FancyScrollView)
- * Copyright (c) 2020 setchi
- * Licensed under MIT (https://github.com/setchi/FancyScrollView/blob/master/LICENSE)
- */
- using System.Collections.Generic;
- using UnityEngine;
- namespace FancyScrollView
- {
- /// <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> { }
- }
|