/// Credit setchi (https://github.com/setchi)
/// Sourced from - https://github.com/setchi/FancyScrollView
using System;
using System.Collections.Generic;
using UnityEngine.UI.Extensions.EasingCore;
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 abstract float CellSize { get; }
///
/// スクロール可能かどうか.
///
///
/// アイテム数が十分少なくビューポート内に全てのセルが収まっている場合は false, それ以外は true になります.
///
protected virtual bool Scrollable => MaxScrollPosition > 0f;
Scroller cachedScroller;
///
/// スクロール位置を制御する のインスタンス.
///
///
/// のスクロール位置を変更する際は必ず を使用して変換した位置を使用してください.
///
protected Scroller Scroller => cachedScroller ?? (cachedScroller = GetComponent());
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.ScrollDirection = Scroller.ScrollDirection;
Context.CalculateScrollSize = () =>
{
var interval = CellSize + spacing;
var reuseMargin = interval * reuseCellMarginCount;
var scrollSize = Scroller.ViewportSize + interval + reuseMargin * 2f;
return (scrollSize, reuseMargin);
};
AdjustCellIntervalAndScrollOffset();
Scroller.OnValueChanged(OnScrollerValueChanged);
}
///
/// のスクロール位置が変更された際の処理.
///
/// のスクロール位置.
void OnScrollerValueChanged(float p)
{
base.UpdatePosition(ToFancyScrollViewPosition(Scrollable ? p : 0f));
if (Scroller.Scrollbar)
{
if (p > ItemsSource.Count - 1)
{
ShrinkScrollbar(p - (ItemsSource.Count - 1));
}
else if (p < 0f)
{
ShrinkScrollbar(-p);
}
}
}
///
/// スクロール範囲を超えてスクロールされた量に基づいて, スクロールバーのサイズを縮小します.
///
/// スクロール範囲を超えてスクロールされた量.
void ShrinkScrollbar(float offset)
{
var scale = 1f - ToFancyScrollViewPosition(offset) / (ViewportLength - PaddingHeadLength);
UpdateScrollbarSize((ViewportLength - PaddingHeadLength) * scale);
}
///
protected override void Refresh()
{
AdjustCellIntervalAndScrollOffset();
RefreshScroller();
base.Refresh();
}
///
protected override void Relayout()
{
AdjustCellIntervalAndScrollOffset();
RefreshScroller();
base.Relayout();
}
///
/// の各種状態を更新します.
///
protected void RefreshScroller()
{
Scroller.Draggable = Scrollable;
Scroller.ScrollSensitivity = ToScrollerPosition(ViewportLength - PaddingHeadLength);
Scroller.Position = ToScrollerPosition(currentPosition);
if (Scroller.Scrollbar)
{
Scroller.Scrollbar.gameObject.SetActive(Scrollable);
UpdateScrollbarSize(ViewportLength);
}
}
///
protected override void UpdateContents(IList items)
{
AdjustCellIntervalAndScrollOffset();
base.UpdateContents(items);
Scroller.SetTotalCount(items.Count);
RefreshScroller();
}
///
/// スクロール位置を更新します.
///
/// スクロール位置.
protected new void UpdatePosition(float position)
{
Scroller.Position = ToScrollerPosition(position, 0.5f);
}
///
/// 指定したアイテムの位置までジャンプします.
///
/// アイテムのインデックス.
/// ビューポート内におけるセル位置の基準. 0f(先頭) ~ 1f(末尾).
protected virtual void JumpTo(int itemIndex, float alignment = 0.5f)
{
Scroller.Position = ToScrollerPosition(itemIndex, alignment);
}
///
/// 指定したアイテムの位置まで移動します.
///
/// アイテムのインデックス.
/// 移動にかける秒数.
/// ビューポート内におけるセル位置の基準. 0f(先頭) ~ 1f(末尾).
/// 移動が完了した際に呼び出されるコールバック.
protected virtual void ScrollTo(int index, float duration, float alignment = 0.5f, Action onComplete = null)
{
Scroller.ScrollTo(ToScrollerPosition(index, alignment), duration, onComplete);
}
///
/// 指定したアイテムの位置まで移動します.
///
/// アイテムのインデックス.
/// 移動にかける秒数.
/// 移動に使用するイージング.
/// ビューポート内におけるセル位置の基準. 0f(先頭) ~ 1f(末尾).
/// 移動が完了した際に呼び出されるコールバック.
protected virtual void ScrollTo(int index, float duration, Ease easing, float alignment = 0.5f, Action onComplete = null)
{
Scroller.ScrollTo(ToScrollerPosition(index, alignment), duration, easing, onComplete);
}
///
/// ビューポートとコンテンツの長さに基づいてスクロールバーのサイズを更新します.
///
/// ビューポートのサイズ.
protected void UpdateScrollbarSize(float viewportLength)
{
var contentLength = Mathf.Max(ItemsSource.Count + (paddingHead + paddingTail - spacing) / (CellSize + spacing), 1);
Scroller.Scrollbar.size = Scrollable ? Mathf.Clamp01(viewportLength / contentLength) : 1f;
}
///
/// が扱うスクロール位置を が扱うスクロール位置に変換します.
///
/// が扱うスクロール位置.
/// が扱うスクロール位置.
protected float ToFancyScrollViewPosition(float position)
{
return position / Mathf.Max(ItemsSource.Count - 1, 1) * MaxScrollPosition - PaddingHeadLength;
}
///
/// が扱うスクロール位置を が扱うスクロール位置に変換します.
///
/// が扱うスクロール位置.
/// が扱うスクロール位置.
protected float ToScrollerPosition(float position)
{
return (position + PaddingHeadLength) / MaxScrollPosition * Mathf.Max(ItemsSource.Count - 1, 1);
}
///
/// が扱うスクロール位置を が扱うスクロール位置に変換します.
///
/// が扱うスクロール位置.
/// ビューポート内におけるセル位置の基準. 0f(先頭) ~ 1f(末尾).
/// が扱うスクロール位置.
protected float ToScrollerPosition(float position, float alignment = 0.5f)
{
var offset = alignment * (ScrollLength - (1f + reuseCellMarginCount * 2f))
+ (1f - alignment - 0.5f) * spacing / (CellSize + spacing);
return ToScrollerPosition(Mathf.Clamp(position - offset, 0f, MaxScrollPosition));
}
///
/// 指定された設定を実現するための
/// と
/// を計算して適用します.
///
protected void AdjustCellIntervalAndScrollOffset()
{
var totalSize = Scroller.ViewportSize + (CellSize + spacing) * (1f + reuseCellMarginCount * 2f);
cellInterval = (CellSize + spacing) / totalSize;
scrollOffset = cellInterval * (1f + reuseCellMarginCount);
}
protected virtual void OnValidate()
{
AdjustCellIntervalAndScrollOffset();
if (loop)
{
loop = false;
Debug.LogError("Loop is currently not supported in FancyScrollRect.");
}
if (Scroller.SnapEnabled)
{
Scroller.SnapEnabled = false;
Debug.LogError("Snap is currently not supported in FancyScrollRect.");
}
if (Scroller.MovementType == MovementType.Unrestricted)
{
Scroller.MovementType = MovementType.Elastic;
Debug.LogError("MovementType.Unrestricted is currently not supported in FancyScrollRect.");
}
}
}
///
/// ScrollRect スタイルのスクロールビューを実装するための抽象基底クラス.
/// 無限スクロールおよびスナップには対応していません.
///
/// アイテムのデータ型.
///
public abstract class FancyScrollRect : FancyScrollRect { }
}