com.alicizax.unity.ui.exten.../Runtime/RecyclerView/RecyclerView.cs
2025-12-26 15:39:31 +08:00

633 lines
18 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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