com.alicizax.unity.ui.exten.../Runtime/RecyclerView/RecyclerView.cs
2025-12-26 14:22:46 +08:00

552 lines
15 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);
}
#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
}
}