using System; using UnityEngine; using UnityEngine.UI; namespace AlicizaX.UI { /// /// RecyclerView 核心组件,用于高效显示大量列表数据 /// 通过视图回收和复用机制,只渲染可见区域的项目,大幅提升性能 /// 支持垂直/水平滚动、网格布局、循环滚动等多种布局模式 /// 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 bool showScrollBarOnlyWhenScrollable; [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; } /// /// 获取或设置列表项之间的间距(X轴和Y轴) /// public Vector2 Spacing { get => spacing; set => spacing = value; } /// /// 获取或设置列表内容的内边距(X轴和Y轴) /// public Vector2 Padding { get => padding; set => padding = value; } #endregion #region Public Properties - Scroll Settings /// /// 获取或设置是否启用滚动功能 /// 启用时会激活 Scroller 组件并显示滚动条(如果配置了) /// 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(); } } /// /// 获取或设置是否启用吸附功能 /// 启用时滚动停止后会自动对齐到最近的列表项 /// 注意:此功能依赖于 Scroll 属性,只有在滚动启用时才生效 /// 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(); } } /// /// 获取或设置滚动速度(范围:1-50) /// 值越大,滚动响应越快 /// public float ScrollSpeed { get => scrollSpeed; set { if (Mathf.Approximately(scrollSpeed, value)) return; scrollSpeed = value; if (scroller != null) { scroller.ScrollSpeed = scrollSpeed; } } } /// /// 获取或设置鼠标滚轮的滚动速度(范围:10-50) /// 值越大,滚轮滚动的距离越大 /// public float WheelSpeed { get => wheelSpeed; set { if (Mathf.Approximately(wheelSpeed, value)) return; wheelSpeed = value; if (scroller != null) { scroller.WheelSpeed = wheelSpeed; } } } /// /// 是否仅在内容超出可视区域时显示滚动条并允许滚动 /// public bool ShowScrollBarOnlyWhenScrollable { get => showScrollBarOnlyWhenScrollable; set { if (showScrollBarOnlyWhenScrollable == value) return; showScrollBarOnlyWhenScrollable = value; RequestLayout(); } } #endregion #region Public Properties - Components /// /// 获取或设置 ViewHolder 模板数组 /// 用于创建和复用列表项视图 /// public ViewHolder[] Templates { get => templates; set => templates = value; } /// /// 获取内容容器的 RectTransform /// 所有列表项都会作为此容器的子对象 /// public RectTransform Content { get { if (content == null) { content = transform.GetChild(0).GetComponent(); } return content; } } /// /// 获取滚动条组件 /// public Scrollbar Scrollbar => scrollbar; /// /// 获取滚动控制器组件 /// public Scroller Scroller => scroller; /// /// 获取视图提供器 /// 负责创建、回收和管理 ViewHolder 实例 /// 根据模板数量自动选择 SimpleViewProvider 或 MixedViewProvider /// 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 /// /// 获取或设置当前绑定的适配器 /// 适配器负责提供数据和创建 ViewHolder /// 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); UpdateScrollerState(); } private void ConfigureScrollbar() { if (!showScrollBar || scrollbar == null) return; scrollbar.onValueChanged.AddListener(OnScrollbarChanged); var scrollbarEx = scrollbar.gameObject.GetComponent(); if (scrollbarEx == null) { scrollbarEx = scrollbar.gameObject.AddComponent(); } scrollbarEx.OnDragEnd = OnScrollbarDragEnd; UpdateScrollbarVisibility(); } #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; } /// /// 重置列表状态 /// 清空所有 ViewHolder 并将滚动位置重置为起始位置 /// public void Reset() { viewProvider?.Reset(); if (scroller != null) { scroller.Position = 0; } if (scrollbar != null) { scrollbar.SetValueWithoutNotify(0); } } #endregion #region Public Methods - Layout /// /// 刷新列表显示 /// 清空当前所有 ViewHolder 并根据当前滚动位置重新创建可见项 /// 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() { if (layoutManager == null) { UpdateScrollbarVisibility(); return; } layoutManager.SetContentSize(); if (scroller == null) { UpdateScrollbarVisibility(); return; } scroller.Direction = direction; scroller.ViewSize = layoutManager.ViewportSize; scroller.ContentSize = layoutManager.ContentSize; scroller.Position = Mathf.Clamp(scroller.Position, 0, scroller.MaxPosition); UpdateScrollerState(); UpdateScrollbarVisibility(); UpdateScrollbarValue(scroller.Position); } #endregion #region Public Methods - Scrolling /// /// 获取当前的滚动位置 /// /// 当前滚动位置值,如果没有 Scroller 则返回 0 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) { return; } bool shouldShow = ShouldShowScrollbar(); scrollbar.gameObject.SetActive(shouldShow); scrollbar.interactable = shouldShow; if (shouldShow) { ConfigureScrollbarDirection(); ConfigureScrollbarSize(); } } private bool ShouldShowScrollbar() { if (!showScrollBar || !scroll || scrollbar == null || scroller == null || layoutManager == null || !SupportsOverflowCheck()) { return false; } if (!HasValidScrollMetrics()) { return false; } if (!ShouldLimitScrollToOverflow()) { return true; } return HasScrollableContent(); } 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 UpdateScrollerState() { if (scroller == null) { return; } scroller.enabled = scroll && (!ShouldLimitScrollToOverflow() || HasScrollableContent()); } private bool ShouldLimitScrollToOverflow() { return showScrollBar && showScrollBarOnlyWhenScrollable && SupportsOverflowCheck(); } private bool HasScrollableContent() { if (layoutManager == null) { return false; } if (direction == Direction.Vertical) { return layoutManager.ContentSize.y > layoutManager.ViewportSize.y; } if (direction == Direction.Horizontal) { return layoutManager.ContentSize.x > layoutManager.ViewportSize.x; } return false; } private bool SupportsOverflowCheck() { return direction == Direction.Vertical || direction == Direction.Horizontal; } private bool HasValidScrollMetrics() { if (direction == Direction.Vertical) { return layoutManager.ContentSize.y > 0f && layoutManager.ViewportSize.y > 0f; } if (direction == Direction.Horizontal) { return layoutManager.ContentSize.x > 0f && layoutManager.ViewportSize.x > 0f; } return false; } 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 } }