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(); } 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 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(); if (scrollbarEx == null) { scrollbarEx = scrollbar.gameObject.AddComponent(); } 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 } }