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