2025-03-12 20:59:12 +08:00
|
|
|
|
using System;
|
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
using UnityEngine.UI;
|
|
|
|
|
|
|
2025-11-20 15:40:38 +08:00
|
|
|
|
namespace AlicizaX.UI
|
2025-03-12 20:59:12 +08:00
|
|
|
|
{
|
|
|
|
|
|
public class RecyclerView : MonoBehaviour
|
|
|
|
|
|
{
|
2025-12-26 14:22:46 +08:00
|
|
|
|
#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
|
2025-04-02 14:53:08 +08:00
|
|
|
|
|
2025-03-12 20:59:12 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region Public Properties - Scroll Settings
|
2025-04-02 14:53:08 +08:00
|
|
|
|
|
2025-03-12 20:59:12 +08:00
|
|
|
|
public bool Scroll
|
|
|
|
|
|
{
|
|
|
|
|
|
get => scroll;
|
2025-12-26 14:22:46 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2025-03-12 20:59:12 +08:00
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
// 如果启用/禁用滚动后需要调整布局或滚动条大小,刷新布局
|
|
|
|
|
|
RequestLayout();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-04-02 14:53:08 +08:00
|
|
|
|
|
2025-03-12 20:59:12 +08:00
|
|
|
|
public bool Snap
|
|
|
|
|
|
{
|
|
|
|
|
|
get => snap;
|
2025-12-26 14:22:46 +08:00
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
// Snap 依赖于 Scroll(与原逻辑保持一致)
|
|
|
|
|
|
bool newSnap = value & scroll;
|
|
|
|
|
|
if (snap == newSnap) return;
|
|
|
|
|
|
snap = newSnap;
|
|
|
|
|
|
|
|
|
|
|
|
if (scroller != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
scroller.Snap = snap;
|
|
|
|
|
|
}
|
2025-03-12 20:59:12 +08:00
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
// 如果开启了 snap,可以选做:立即对齐到最近项
|
|
|
|
|
|
// if (snap && scroller != null) SnapToNearestItem();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-04-02 14:53:08 +08:00
|
|
|
|
|
2025-03-12 20:59:12 +08:00
|
|
|
|
public float ScrollSpeed
|
|
|
|
|
|
{
|
|
|
|
|
|
get => scrollSpeed;
|
2025-12-26 14:22:46 +08:00
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
if (Mathf.Approximately(scrollSpeed, value)) return;
|
|
|
|
|
|
scrollSpeed = value;
|
2025-03-12 20:59:12 +08:00
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
if (scroller != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
scroller.ScrollSpeed = scrollSpeed;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-04-02 14:53:08 +08:00
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
public float WheelSpeed
|
2025-03-12 20:59:12 +08:00
|
|
|
|
{
|
|
|
|
|
|
get => wheelSpeed;
|
2025-12-26 14:22:46 +08:00
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
if (Mathf.Approximately(wheelSpeed, value)) return;
|
|
|
|
|
|
wheelSpeed = value;
|
|
|
|
|
|
|
|
|
|
|
|
if (scroller != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
scroller.WheelSpeed = wheelSpeed;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-03-12 20:59:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#region Public Properties - Components
|
2025-04-02 14:53:08 +08:00
|
|
|
|
|
2025-03-12 20:59:12 +08:00
|
|
|
|
public ViewHolder[] Templates
|
|
|
|
|
|
{
|
|
|
|
|
|
get => templates;
|
2025-12-26 14:22:46 +08:00
|
|
|
|
set => templates = value;
|
2025-03-12 20:59:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
public RectTransform Content
|
|
|
|
|
|
{
|
|
|
|
|
|
get
|
|
|
|
|
|
{
|
|
|
|
|
|
if (content == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
content = transform.GetChild(0).GetComponent<RectTransform>();
|
|
|
|
|
|
}
|
2025-05-30 10:32:08 +08:00
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
return content;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-05-30 10:32:08 +08:00
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
public Scrollbar Scrollbar => scrollbar;
|
2025-05-30 10:32:08 +08:00
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
public Scroller Scroller => scroller;
|
2025-05-30 10:32:08 +08:00
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
public ViewProvider ViewProvider
|
|
|
|
|
|
{
|
|
|
|
|
|
get
|
|
|
|
|
|
{
|
|
|
|
|
|
if (viewProvider == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
viewProvider = templates.Length > 1
|
|
|
|
|
|
? new MixedViewProvider(this, templates)
|
|
|
|
|
|
: new SimpleViewProvider(this, templates);
|
|
|
|
|
|
}
|
2025-05-30 10:32:08 +08:00
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
return viewProvider;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-05-30 10:32:08 +08:00
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
#endregion
|
2025-03-12 20:59:12 +08:00
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
#region Public Properties - State
|
2025-04-02 14:53:08 +08:00
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
public IAdapter RecyclerViewAdapter { get; set; }
|
2025-06-03 17:08:27 +08:00
|
|
|
|
|
2025-03-12 20:59:12 +08:00
|
|
|
|
public int CurrentIndex
|
|
|
|
|
|
{
|
|
|
|
|
|
get => currentIndex;
|
2025-12-26 14:22:46 +08:00
|
|
|
|
set => currentIndex = value;
|
2025-03-12 20:59:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
#endregion
|
2025-03-12 20:59:12 +08:00
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
#region Events
|
2025-03-12 20:59:12 +08:00
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
public Action<int> OnIndexChanged;
|
|
|
|
|
|
public Action OnScrollValueChanged;
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region Unity Lifecycle
|
|
|
|
|
|
|
|
|
|
|
|
private void Awake()
|
2025-03-12 20:59:12 +08:00
|
|
|
|
{
|
2025-12-26 14:22:46 +08:00
|
|
|
|
InitializeTemplates();
|
|
|
|
|
|
ConfigureScroller();
|
|
|
|
|
|
ConfigureScrollbar();
|
2025-03-12 20:59:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
#endregion
|
2025-03-12 20:59:12 +08:00
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
#region Initialization
|
2025-03-12 20:59:12 +08:00
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
private void InitializeTemplates()
|
2025-03-12 20:59:12 +08:00
|
|
|
|
{
|
2025-12-26 14:22:46 +08:00
|
|
|
|
if (templates == null) return;
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < templates.Length; i++)
|
2025-03-12 20:59:12 +08:00
|
|
|
|
{
|
2025-12-26 14:22:46 +08:00
|
|
|
|
if (templates[i] != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
templates[i].gameObject.SetActive(false);
|
|
|
|
|
|
}
|
2025-03-12 20:59:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
private void ConfigureScroller()
|
2025-03-12 20:59:12 +08:00
|
|
|
|
{
|
2025-12-26 14:22:46 +08:00
|
|
|
|
if (scroller == null) return;
|
2025-03-12 20:59:12 +08:00
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
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)
|
2025-03-12 20:59:12 +08:00
|
|
|
|
{
|
2025-12-26 14:22:46 +08:00
|
|
|
|
scrollbarEx = scrollbar.gameObject.AddComponent<ScrollbarEx>();
|
2025-03-12 20:59:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
scrollbarEx.OnDragEnd = OnScrollbarDragEnd;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region Public Methods - Setup
|
|
|
|
|
|
|
|
|
|
|
|
public void SetAdapter(IAdapter adapter)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (adapter == null)
|
2025-03-12 20:59:12 +08:00
|
|
|
|
{
|
2025-12-26 14:22:46 +08:00
|
|
|
|
Debug.LogError("Adapter cannot be null");
|
|
|
|
|
|
return;
|
2025-03-12 20:59:12 +08:00
|
|
|
|
}
|
2025-12-26 14:22:46 +08:00
|
|
|
|
|
|
|
|
|
|
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)
|
2025-03-12 20:59:12 +08:00
|
|
|
|
{
|
2025-12-26 14:22:46 +08:00
|
|
|
|
scroller.Position = 0;
|
2025-03-12 20:59:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
if (scrollbar != null)
|
2025-03-12 20:59:12 +08:00
|
|
|
|
{
|
2025-12-26 14:22:46 +08:00
|
|
|
|
scrollbar.SetValueWithoutNotify(0);
|
2025-03-12 20:59:12 +08:00
|
|
|
|
}
|
2025-12-26 14:22:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#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)
|
2025-03-12 20:59:12 +08:00
|
|
|
|
{
|
2025-12-26 14:22:46 +08:00
|
|
|
|
ViewProvider.CreateViewHolder(i);
|
2025-03-12 20:59:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
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)
|
2025-03-12 20:59:12 +08:00
|
|
|
|
{
|
|
|
|
|
|
Refresh();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
UpdateCurrentIndex(index);
|
2025-03-12 20:59:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 15:39:31 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region Private Methods - Scroll Callbacks
|
|
|
|
|
|
|
|
|
|
|
|
private void OnScrollChanged(float position)
|
2025-06-03 17:08:27 +08:00
|
|
|
|
{
|
2025-12-26 14:22:46 +08:00
|
|
|
|
layoutManager.UpdateLayout();
|
|
|
|
|
|
UpdateScrollbarValue(position);
|
|
|
|
|
|
UpdateVisibleRange();
|
|
|
|
|
|
layoutManager.DoItemAnimation();
|
|
|
|
|
|
OnScrollValueChanged?.Invoke();
|
2025-06-03 17:08:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-12 20:59:12 +08:00
|
|
|
|
private void OnMoveStoped()
|
|
|
|
|
|
{
|
2025-12-26 14:22:46 +08:00
|
|
|
|
if (snap)
|
2025-03-12 20:59:12 +08:00
|
|
|
|
{
|
2025-12-26 14:22:46 +08:00
|
|
|
|
SnapToNearestItem();
|
2025-03-12 20:59:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void OnScrollbarChanged(float ratio)
|
|
|
|
|
|
{
|
2025-12-26 14:22:46 +08:00
|
|
|
|
if (scroller != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
scroller.ScrollToRatio(ratio);
|
|
|
|
|
|
}
|
2025-03-12 20:59:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void OnScrollbarDragEnd()
|
|
|
|
|
|
{
|
2025-12-26 14:22:46 +08:00
|
|
|
|
if (scroller == null) return;
|
|
|
|
|
|
|
|
|
|
|
|
if (scroller.Position < scroller.MaxPosition && snap)
|
2025-03-12 20:59:12 +08:00
|
|
|
|
{
|
2025-12-26 14:22:46 +08:00
|
|
|
|
SnapToNearestItem();
|
2025-03-12 20:59:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region Private Methods - Scroll Helpers
|
|
|
|
|
|
|
|
|
|
|
|
private void UpdateScrollbarValue(float position)
|
2025-04-02 14:53:08 +08:00
|
|
|
|
{
|
2025-12-26 14:22:46 +08:00
|
|
|
|
if (scrollbar != null && scroller != null)
|
2025-04-02 14:53:08 +08:00
|
|
|
|
{
|
2025-12-26 14:22:46 +08:00
|
|
|
|
float ratio = scroller.MaxPosition > 0 ? position / scroller.MaxPosition : 0;
|
|
|
|
|
|
scrollbar.SetValueWithoutNotify(ratio);
|
2025-04-02 14:53:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
private void UpdateVisibleRange()
|
2025-04-01 15:21:02 +08:00
|
|
|
|
{
|
2025-12-26 14:22:46 +08:00
|
|
|
|
// 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-04-01 15:21:02 +08:00
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
// Handle end index
|
|
|
|
|
|
if (layoutManager.IsFullInvisibleEnd(endIndex))
|
2025-03-12 20:59:12 +08:00
|
|
|
|
{
|
2025-12-26 14:22:46 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2025-03-12 20:59:12 +08:00
|
|
|
|
}
|
2025-04-02 14:53:08 +08:00
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
// Refresh if out of visible range
|
|
|
|
|
|
if (!layoutManager.IsVisible(startIndex) || !layoutManager.IsVisible(endIndex))
|
2025-03-12 20:59:12 +08:00
|
|
|
|
{
|
2025-12-26 14:22:46 +08:00
|
|
|
|
Refresh();
|
2025-03-12 20:59:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
private void UpdateScrollbarVisibility()
|
2025-03-12 20:59:12 +08:00
|
|
|
|
{
|
2025-12-26 14:22:46 +08:00
|
|
|
|
if (scrollbar == null || scroller == null || layoutManager.ContentSize == Vector2.zero)
|
|
|
|
|
|
return;
|
2025-03-12 20:59:12 +08:00
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
bool shouldShow = ShouldShowScrollbar();
|
|
|
|
|
|
scrollbar.gameObject.SetActive(shouldShow);
|
2025-03-12 20:59:12 +08:00
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
if (shouldShow)
|
2025-03-12 20:59:12 +08:00
|
|
|
|
{
|
2025-12-26 14:22:46 +08:00
|
|
|
|
ConfigureScrollbarDirection();
|
|
|
|
|
|
ConfigureScrollbarSize();
|
2025-03-12 20:59:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
private bool ShouldShowScrollbar()
|
2025-03-12 20:59:12 +08:00
|
|
|
|
{
|
2025-12-26 14:22:46 +08:00
|
|
|
|
if (direction == Direction.Custom) return false;
|
|
|
|
|
|
|
|
|
|
|
|
if (direction == Direction.Vertical)
|
2025-05-30 10:32:08 +08:00
|
|
|
|
{
|
2025-12-26 14:22:46 +08:00
|
|
|
|
return layoutManager.ContentSize.y > layoutManager.ViewportSize.y;
|
2025-05-30 10:32:08 +08:00
|
|
|
|
}
|
2025-12-26 14:22:46 +08:00
|
|
|
|
else // Horizontal
|
2025-03-12 20:59:12 +08:00
|
|
|
|
{
|
2025-12-26 14:22:46 +08:00
|
|
|
|
return layoutManager.ContentSize.x > layoutManager.ViewportSize.x;
|
2025-03-12 20:59:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
private void ConfigureScrollbarDirection()
|
2025-03-12 20:59:12 +08:00
|
|
|
|
{
|
2025-12-26 14:22:46 +08:00
|
|
|
|
scrollbar.direction = direction == Direction.Vertical
|
|
|
|
|
|
? Scrollbar.Direction.TopToBottom
|
|
|
|
|
|
: Scrollbar.Direction.LeftToRight;
|
|
|
|
|
|
}
|
2025-03-12 20:59:12 +08:00
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
private void ConfigureScrollbarSize()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (direction == Direction.Vertical)
|
2025-03-12 20:59:12 +08:00
|
|
|
|
{
|
2025-12-26 14:22:46 +08:00
|
|
|
|
scrollbar.size = scroller.ViewSize.y / scroller.ContentSize.y;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
scrollbar.size = scroller.ViewSize.x / scroller.ContentSize.x;
|
2025-03-12 20:59:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
private void SnapToNearestItem()
|
2025-03-12 20:59:12 +08:00
|
|
|
|
{
|
2025-12-26 14:22:46 +08:00
|
|
|
|
int index = layoutManager.PositionToIndex(GetScrollPosition());
|
|
|
|
|
|
ScrollTo(index, true);
|
2025-03-12 20:59:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
private void UpdateCurrentIndex(int index)
|
2025-03-12 20:59:12 +08:00
|
|
|
|
{
|
2025-12-26 14:22:46 +08:00
|
|
|
|
if (RecyclerViewAdapter == null) return;
|
2025-03-12 20:59:12 +08:00
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
int itemCount = RecyclerViewAdapter.GetItemCount();
|
|
|
|
|
|
index %= itemCount;
|
|
|
|
|
|
index = index < 0 ? itemCount + index : index;
|
2025-03-12 20:59:12 +08:00
|
|
|
|
|
|
|
|
|
|
if (currentIndex != index)
|
|
|
|
|
|
{
|
|
|
|
|
|
currentIndex = index;
|
|
|
|
|
|
OnIndexChanged?.Invoke(currentIndex);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
#endregion
|
2025-03-12 20:59:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|