/// 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 { } }