633 lines
18 KiB
C#
633 lines
18 KiB
C#
using System;
|
||
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
|
||
namespace AlicizaX.UI
|
||
{
|
||
public class RecyclerView : MonoBehaviour
|
||
{
|
||
#region Serialized Fields - Layout Settings
|
||
|
||
[HideInInspector] [SerializeField] private Direction direction;
|
||
[HideInInspector] [SerializeField] private Alignment alignment;
|
||
[HideInInspector] [SerializeField] private Vector2 spacing;
|
||
[HideInInspector] [SerializeField] private Vector2 padding;
|
||
|
||
#endregion
|
||
|
||
#region Serialized Fields - Scroll Settings
|
||
|
||
[HideInInspector] [SerializeField] private bool scroll;
|
||
[HideInInspector] [SerializeField] private bool snap;
|
||
|
||
[HideInInspector] [SerializeField, Range(1f, 50f)]
|
||
private float scrollSpeed = 7f;
|
||
|
||
[HideInInspector] [SerializeField, Range(10f, 50f)]
|
||
private float wheelSpeed = 30f;
|
||
|
||
#endregion
|
||
|
||
#region Serialized Fields - Components
|
||
|
||
[HideInInspector] [SerializeField] private ViewHolder[] templates;
|
||
[HideInInspector] [SerializeField] private RectTransform content;
|
||
[HideInInspector] [SerializeField] private bool showScrollBar;
|
||
[HideInInspector] [SerializeField] private Scrollbar scrollbar;
|
||
|
||
#endregion
|
||
|
||
#region Serialized Fields - Internal (Hidden in Inspector)
|
||
|
||
[HideInInspector] [SerializeField] private string _layoutManagerTypeName;
|
||
[SerializeReference] private LayoutManager layoutManager;
|
||
[HideInInspector] [SerializeField] private string _scrollerTypeName;
|
||
[HideInInspector] [SerializeReference] private Scroller scroller;
|
||
|
||
#endregion
|
||
|
||
#region Private Fields
|
||
|
||
private ViewProvider viewProvider;
|
||
private int startIndex;
|
||
private int endIndex;
|
||
private int currentIndex;
|
||
|
||
#endregion
|
||
|
||
#region Public Properties - Layout Settings
|
||
|
||
public Direction Direction
|
||
{
|
||
get => direction;
|
||
set => direction = value;
|
||
}
|
||
|
||
public Alignment Alignment
|
||
{
|
||
get => alignment;
|
||
set => alignment = value;
|
||
}
|
||
|
||
public Vector2 Spacing
|
||
{
|
||
get => spacing;
|
||
set => spacing = value;
|
||
}
|
||
|
||
public Vector2 Padding
|
||
{
|
||
get => padding;
|
||
set => padding = value;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Public Properties - Scroll Settings
|
||
|
||
public bool Scroll
|
||
{
|
||
get => scroll;
|
||
set
|
||
{
|
||
if (scroll == value) return;
|
||
scroll = value;
|
||
|
||
// 启/停 scroller(如果存在)
|
||
if (scroller != null)
|
||
{
|
||
// 如果 Scroller 是 MonoBehaviour,可以启/停组件;否则回退到设置一个 Position/flags
|
||
scroller.enabled = scroll;
|
||
|
||
// 将当前的滚动相关配置下发到 scroller,保持一致性
|
||
scroller.ScrollSpeed = scrollSpeed;
|
||
scroller.WheelSpeed = wheelSpeed;
|
||
scroller.Snap = snap;
|
||
}
|
||
|
||
// 更新 scrollbar 显示(只有在 showScrollBar 为 true 时才显示)
|
||
if (scrollbar != null)
|
||
{
|
||
scrollbar.gameObject.SetActive(showScrollBar && scroll);
|
||
}
|
||
|
||
// 如果启用/禁用滚动后需要调整布局或滚动条大小,刷新布局
|
||
RequestLayout();
|
||
}
|
||
}
|
||
|
||
public bool Snap
|
||
{
|
||
get => snap;
|
||
set
|
||
{
|
||
// Snap 依赖于 Scroll(与原逻辑保持一致)
|
||
bool newSnap = value & scroll;
|
||
if (snap == newSnap) return;
|
||
snap = newSnap;
|
||
|
||
if (scroller != null)
|
||
{
|
||
scroller.Snap = snap;
|
||
}
|
||
|
||
// 如果开启了 snap,可以选做:立即对齐到最近项
|
||
// if (snap && scroller != null) SnapToNearestItem();
|
||
}
|
||
}
|
||
|
||
public float ScrollSpeed
|
||
{
|
||
get => scrollSpeed;
|
||
set
|
||
{
|
||
if (Mathf.Approximately(scrollSpeed, value)) return;
|
||
scrollSpeed = value;
|
||
|
||
if (scroller != null)
|
||
{
|
||
scroller.ScrollSpeed = scrollSpeed;
|
||
}
|
||
}
|
||
}
|
||
|
||
public float WheelSpeed
|
||
{
|
||
get => wheelSpeed;
|
||
set
|
||
{
|
||
if (Mathf.Approximately(wheelSpeed, value)) return;
|
||
wheelSpeed = value;
|
||
|
||
if (scroller != null)
|
||
{
|
||
scroller.WheelSpeed = wheelSpeed;
|
||
}
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
|
||
#region Public Properties - Components
|
||
|
||
public ViewHolder[] Templates
|
||
{
|
||
get => templates;
|
||
set => templates = value;
|
||
}
|
||
|
||
public RectTransform Content
|
||
{
|
||
get
|
||
{
|
||
if (content == null)
|
||
{
|
||
content = transform.GetChild(0).GetComponent<RectTransform>();
|
||
}
|
||
|
||
return content;
|
||
}
|
||
}
|
||
|
||
public Scrollbar Scrollbar => scrollbar;
|
||
|
||
public Scroller Scroller => scroller;
|
||
|
||
public ViewProvider ViewProvider
|
||
{
|
||
get
|
||
{
|
||
if (viewProvider == null)
|
||
{
|
||
viewProvider = templates.Length > 1
|
||
? new MixedViewProvider(this, templates)
|
||
: new SimpleViewProvider(this, templates);
|
||
}
|
||
|
||
return viewProvider;
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Public Properties - State
|
||
|
||
public IAdapter RecyclerViewAdapter { get; set; }
|
||
|
||
public int CurrentIndex
|
||
{
|
||
get => currentIndex;
|
||
set => currentIndex = value;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Events
|
||
|
||
public Action<int> OnIndexChanged;
|
||
public Action OnScrollValueChanged;
|
||
|
||
#endregion
|
||
|
||
#region Unity Lifecycle
|
||
|
||
private void Awake()
|
||
{
|
||
InitializeTemplates();
|
||
ConfigureScroller();
|
||
ConfigureScrollbar();
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Initialization
|
||
|
||
private void InitializeTemplates()
|
||
{
|
||
if (templates == null) return;
|
||
|
||
for (int i = 0; i < templates.Length; i++)
|
||
{
|
||
if (templates[i] != null)
|
||
{
|
||
templates[i].gameObject.SetActive(false);
|
||
}
|
||
}
|
||
}
|
||
|
||
private void ConfigureScroller()
|
||
{
|
||
if (scroller == null) return;
|
||
|
||
scroller.ScrollSpeed = scrollSpeed;
|
||
scroller.WheelSpeed = wheelSpeed;
|
||
scroller.Snap = snap;
|
||
scroller.OnValueChanged.AddListener(OnScrollChanged);
|
||
scroller.OnMoveStoped.AddListener(OnMoveStoped);
|
||
}
|
||
|
||
private void ConfigureScrollbar()
|
||
{
|
||
if (!showScrollBar || scrollbar == null) return;
|
||
|
||
scrollbar.gameObject.SetActive(scroll);
|
||
scrollbar.onValueChanged.AddListener(OnScrollbarChanged);
|
||
|
||
var scrollbarEx = scrollbar.gameObject.GetComponent<ScrollbarEx>();
|
||
if (scrollbarEx == null)
|
||
{
|
||
scrollbarEx = scrollbar.gameObject.AddComponent<ScrollbarEx>();
|
||
}
|
||
|
||
scrollbarEx.OnDragEnd = OnScrollbarDragEnd;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Public Methods - Setup
|
||
|
||
public void SetAdapter(IAdapter adapter)
|
||
{
|
||
if (adapter == null)
|
||
{
|
||
Debug.LogError("Adapter cannot be null");
|
||
return;
|
||
}
|
||
|
||
RecyclerViewAdapter = adapter;
|
||
ViewProvider.Adapter = adapter;
|
||
ViewProvider.LayoutManager = layoutManager;
|
||
|
||
layoutManager.RecyclerView = this;
|
||
layoutManager.Adapter = adapter;
|
||
layoutManager.ViewProvider = viewProvider;
|
||
layoutManager.Direction = direction;
|
||
layoutManager.Alignment = alignment;
|
||
layoutManager.Spacing = spacing;
|
||
layoutManager.Padding = padding;
|
||
}
|
||
|
||
public void Reset()
|
||
{
|
||
viewProvider?.Reset();
|
||
|
||
if (scroller != null)
|
||
{
|
||
scroller.Position = 0;
|
||
}
|
||
|
||
if (scrollbar != null)
|
||
{
|
||
scrollbar.SetValueWithoutNotify(0);
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Public Methods - Layout
|
||
|
||
public void Refresh()
|
||
{
|
||
ViewProvider.Clear();
|
||
|
||
startIndex = layoutManager.GetStartIndex();
|
||
endIndex = layoutManager.GetEndIndex();
|
||
|
||
for (int i = startIndex; i <= endIndex; i += layoutManager.Unit)
|
||
{
|
||
ViewProvider.CreateViewHolder(i);
|
||
}
|
||
|
||
layoutManager.DoItemAnimation();
|
||
}
|
||
|
||
public void RequestLayout()
|
||
{
|
||
layoutManager.SetContentSize();
|
||
|
||
if (scroller == null) return;
|
||
|
||
scroller.Direction = direction;
|
||
scroller.ViewSize = layoutManager.ViewportSize;
|
||
scroller.ContentSize = layoutManager.ContentSize;
|
||
|
||
UpdateScrollbarVisibility();
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Public Methods - Scrolling
|
||
|
||
public float GetScrollPosition()
|
||
{
|
||
return scroller != null ? scroller.Position : 0;
|
||
}
|
||
|
||
public void ScrollTo(int index, bool smooth = false)
|
||
{
|
||
if (!scroll || scroller == null) return;
|
||
|
||
scroller.ScrollTo(layoutManager.IndexToPosition(index), smooth);
|
||
|
||
if (!smooth)
|
||
{
|
||
Refresh();
|
||
}
|
||
|
||
UpdateCurrentIndex(index);
|
||
}
|
||
|
||
public void ScrollToWithAlignment(int index, ScrollAlignment alignment, float offset = 0f, bool smooth = false, float duration = 0.3f)
|
||
{
|
||
if (!scroll || scroller == null) return;
|
||
|
||
float targetPosition = CalculateScrollPositionWithAlignment(index, alignment, offset);
|
||
|
||
if (UGListExtensions.DebugScrollTo)
|
||
{
|
||
Debug.Log($"[RecyclerView] ScrollToWithAlignment: index={index}, alignment={alignment}, offset={offset}, targetPosition={targetPosition}, maxPosition={scroller.MaxPosition}");
|
||
}
|
||
|
||
if (duration > 0 && smooth)
|
||
{
|
||
scroller.ScrollTo(targetPosition, true);
|
||
}
|
||
else
|
||
{
|
||
scroller.ScrollTo(targetPosition, smooth);
|
||
}
|
||
|
||
if (!smooth)
|
||
{
|
||
Refresh();
|
||
}
|
||
|
||
UpdateCurrentIndex(index);
|
||
}
|
||
|
||
private float CalculateScrollPositionWithAlignment(int index, ScrollAlignment alignment, float offset)
|
||
{
|
||
if (RecyclerViewAdapter == null || index < 0 || index >= RecyclerViewAdapter.GetItemCount())
|
||
{
|
||
return scroller.Position;
|
||
}
|
||
|
||
float itemSize = GetItemSize(index);
|
||
float viewportLength = direction == Direction.Vertical ? layoutManager.ViewportSize.y : layoutManager.ViewportSize.x;
|
||
float contentLength = direction == Direction.Vertical ? layoutManager.ContentSize.y : layoutManager.ContentSize.x;
|
||
|
||
// Calculate the raw position of the item (without any clamping)
|
||
float itemPosition = CalculateRawItemPosition(index);
|
||
|
||
float targetPosition = alignment switch
|
||
{
|
||
ScrollAlignment.Start => itemPosition,
|
||
ScrollAlignment.Center => itemPosition - (viewportLength - itemSize) / 2f,
|
||
ScrollAlignment.End => itemPosition - viewportLength + itemSize,
|
||
_ => itemPosition
|
||
};
|
||
|
||
// Apply custom offset
|
||
targetPosition += offset;
|
||
|
||
if (UGListExtensions.DebugScrollTo)
|
||
{
|
||
Debug.Log($"[RecyclerView] CalculateScrollPosition: index={index}, itemPosition={itemPosition}, itemSize={itemSize}, viewportLength={viewportLength}, contentLength={contentLength}, targetPosition={targetPosition}, maxPosition={scroller.MaxPosition}");
|
||
}
|
||
|
||
// Clamp to valid scroll range
|
||
return Mathf.Clamp(targetPosition, 0, scroller.MaxPosition);
|
||
}
|
||
|
||
private float CalculateRawItemPosition(int index)
|
||
{
|
||
// Get spacing based on direction
|
||
Vector2 spacing = layoutManager.Spacing;
|
||
Vector2 padding = layoutManager.Padding;
|
||
float itemSize = GetItemSize(index);
|
||
float spacingValue = direction == Direction.Vertical ? spacing.y : spacing.x;
|
||
float paddingValue = direction == Direction.Vertical ? padding.y : padding.x;
|
||
|
||
// Calculate raw position without clamping
|
||
return index * (itemSize + spacingValue) + paddingValue;
|
||
}
|
||
|
||
private float GetItemSize(int index)
|
||
{
|
||
Vector2 itemSize = ViewProvider.CalculateViewSize(index);
|
||
return direction == Direction.Vertical ? itemSize.y : itemSize.x;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Private Methods - Scroll Callbacks
|
||
|
||
private void OnScrollChanged(float position)
|
||
{
|
||
layoutManager.UpdateLayout();
|
||
UpdateScrollbarValue(position);
|
||
UpdateVisibleRange();
|
||
layoutManager.DoItemAnimation();
|
||
OnScrollValueChanged?.Invoke();
|
||
}
|
||
|
||
private void OnMoveStoped()
|
||
{
|
||
if (snap)
|
||
{
|
||
SnapToNearestItem();
|
||
}
|
||
}
|
||
|
||
private void OnScrollbarChanged(float ratio)
|
||
{
|
||
if (scroller != null)
|
||
{
|
||
scroller.ScrollToRatio(ratio);
|
||
}
|
||
}
|
||
|
||
private void OnScrollbarDragEnd()
|
||
{
|
||
if (scroller == null) return;
|
||
|
||
if (scroller.Position < scroller.MaxPosition && snap)
|
||
{
|
||
SnapToNearestItem();
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Private Methods - Scroll Helpers
|
||
|
||
private void UpdateScrollbarValue(float position)
|
||
{
|
||
if (scrollbar != null && scroller != null)
|
||
{
|
||
float ratio = scroller.MaxPosition > 0 ? position / scroller.MaxPosition : 0;
|
||
scrollbar.SetValueWithoutNotify(ratio);
|
||
}
|
||
}
|
||
|
||
private void UpdateVisibleRange()
|
||
{
|
||
// Handle start index
|
||
if (layoutManager.IsFullInvisibleStart(startIndex))
|
||
{
|
||
viewProvider.RemoveViewHolder(startIndex);
|
||
startIndex += layoutManager.Unit;
|
||
}
|
||
else if (layoutManager.IsFullVisibleStart(startIndex))
|
||
{
|
||
if (startIndex == 0)
|
||
{
|
||
// TODO: Implement refresh logic
|
||
}
|
||
else
|
||
{
|
||
startIndex -= layoutManager.Unit;
|
||
viewProvider.CreateViewHolder(startIndex);
|
||
}
|
||
}
|
||
|
||
// Handle end index
|
||
if (layoutManager.IsFullInvisibleEnd(endIndex))
|
||
{
|
||
viewProvider.RemoveViewHolder(endIndex);
|
||
endIndex -= layoutManager.Unit;
|
||
}
|
||
else if (layoutManager.IsFullVisibleEnd(endIndex))
|
||
{
|
||
if (endIndex >= viewProvider.GetItemCount() - layoutManager.Unit)
|
||
{
|
||
// TODO: Implement load more logic
|
||
}
|
||
else
|
||
{
|
||
endIndex += layoutManager.Unit;
|
||
viewProvider.CreateViewHolder(endIndex);
|
||
}
|
||
}
|
||
|
||
// Refresh if out of visible range
|
||
if (!layoutManager.IsVisible(startIndex) || !layoutManager.IsVisible(endIndex))
|
||
{
|
||
Refresh();
|
||
}
|
||
}
|
||
|
||
private void UpdateScrollbarVisibility()
|
||
{
|
||
if (scrollbar == null || scroller == null || layoutManager.ContentSize == Vector2.zero)
|
||
return;
|
||
|
||
bool shouldShow = ShouldShowScrollbar();
|
||
scrollbar.gameObject.SetActive(shouldShow);
|
||
|
||
if (shouldShow)
|
||
{
|
||
ConfigureScrollbarDirection();
|
||
ConfigureScrollbarSize();
|
||
}
|
||
}
|
||
|
||
private bool ShouldShowScrollbar()
|
||
{
|
||
if (direction == Direction.Custom) return false;
|
||
|
||
if (direction == Direction.Vertical)
|
||
{
|
||
return layoutManager.ContentSize.y > layoutManager.ViewportSize.y;
|
||
}
|
||
else // Horizontal
|
||
{
|
||
return layoutManager.ContentSize.x > layoutManager.ViewportSize.x;
|
||
}
|
||
}
|
||
|
||
private void ConfigureScrollbarDirection()
|
||
{
|
||
scrollbar.direction = direction == Direction.Vertical
|
||
? Scrollbar.Direction.TopToBottom
|
||
: Scrollbar.Direction.LeftToRight;
|
||
}
|
||
|
||
private void ConfigureScrollbarSize()
|
||
{
|
||
if (direction == Direction.Vertical)
|
||
{
|
||
scrollbar.size = scroller.ViewSize.y / scroller.ContentSize.y;
|
||
}
|
||
else
|
||
{
|
||
scrollbar.size = scroller.ViewSize.x / scroller.ContentSize.x;
|
||
}
|
||
}
|
||
|
||
private void SnapToNearestItem()
|
||
{
|
||
int index = layoutManager.PositionToIndex(GetScrollPosition());
|
||
ScrollTo(index, true);
|
||
}
|
||
|
||
private void UpdateCurrentIndex(int index)
|
||
{
|
||
if (RecyclerViewAdapter == null) return;
|
||
|
||
int itemCount = RecyclerViewAdapter.GetItemCount();
|
||
index %= itemCount;
|
||
index = index < 0 ? itemCount + index : index;
|
||
|
||
if (currentIndex != index)
|
||
{
|
||
currentIndex = index;
|
||
OnIndexChanged?.Invoke(currentIndex);
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
}
|
||
}
|