using System; using System.Collections; using System.Collections.Generic; using System.Threading; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; namespace AlicizaX.UI { /// /// RecyclerView 的核心组件,负责适配器绑定、布局刷新、滚动控制与焦点导航。 /// public class RecyclerView : MonoBehaviour { /// /// 滚动条拖拽手柄允许的最小像素长度。 /// private const float MinScrollbarHandlePixels = 18f; /// /// 记录 Unity 主线程的托管线程标识。 /// private static int mainThreadId = -1; #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(0.5f, 50f)] private float scrollSpeed = 7f; /// /// 鼠标滚轮滚动时的速度系数。 /// [HideInInspector] [SerializeField, Range(1f, 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 RecyclerNavigationController navigationController; /// /// 下一帧恢复 UI 焦点时使用的协程句柄。 /// private Coroutine focusRecoveryCoroutine; /// /// 是否存在等待滚动结束后执行的焦点请求。 /// private bool hasPendingFocusRequest; /// /// 挂起焦点请求期望采用的对齐方式。 /// private ScrollAlignment pendingFocusAlignment; /// /// 挂起焦点请求对应的数据索引。 /// private int pendingFocusIndex = -1; /// /// 当前可见区间的起始布局索引。 /// 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; if (scroller != null) { scroller.enabled = scroll; scroller.WheelSpeed = wheelSpeed; scroller.Snap = snap; } { scrollbar.gameObject.SetActive(showScrollBar && scroll); } RequestLayout(); } } /// /// 获取或设置是否在停止滚动后自动吸附到最近项。 /// public bool Snap { get => snap; set { bool newSnap = value & scroll; if (snap == newSnap) return; snap = newSnap; if (scroller != null) { scroller.Snap = snap; } // 如需在启用吸附后立即校正位置,可在此触发最近项吸附。 } } /// /// 获取或设置平滑滚动速度系数。 /// 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; } } } /// /// 获取或设置是否仅在内容可滚动时显示滚动条。 /// public bool ShowScrollBarOnlyWhenScrollable { get => showScrollBarOnlyWhenScrollable; set { if (showScrollBarOnlyWhenScrollable == value) return; showScrollBarOnlyWhenScrollable = value; RequestLayout(); } } #endregion #region Public Properties - Components /// /// 获取或设置用于创建列表项的模板集合。 /// public ViewHolder[] Templates { get => templates; set => templates = value; } /// /// 获取内容节点;未显式指定时会尝试从首个子节点推断。 /// public RectTransform Content { get { if (content == null) { if (transform.childCount == 0) { throw new InvalidOperationException("RecyclerView content is not assigned and no child RectTransform exists."); } content = transform.GetChild(0).GetComponent(); if (content == null) { throw new InvalidOperationException("RecyclerView content child must have a RectTransform component."); } } return content; } } /// /// 获取当前绑定的滚动条组件。 /// public Scrollbar Scrollbar => scrollbar; /// /// 获取当前绑定的滚动器实例。 /// public Scroller Scroller => scroller; /// /// 获取视图提供器;首次访问时根据模板数量自动创建。 /// public ViewProvider ViewProvider { get { if (viewProvider == null) { if (templates == null || templates.Length == 0) { throw new InvalidOperationException("RecyclerView templates can not be null or empty."); } viewProvider = templates.Length > 1 ? new MixedViewProvider(this, templates) : new SimpleViewProvider(this, templates); } return viewProvider; } } /// /// 获取当前对象池的统计信息文本。 /// public string PoolStats => viewProvider?.PoolStats ?? string.Empty; /// /// 获取当前布局管理器实例。 /// public LayoutManager LayoutManager => layoutManager; /// /// 获取导航控制器;首次访问时自动创建。 /// public RecyclerNavigationController NavigationController => navigationController ??= new RecyclerNavigationController(this); #endregion #region Public Properties - State /// /// 获取或设置当前绑定的适配器实例。 /// internal IAdapter RecyclerViewAdapter { get; private set; } /// /// 获取当前记录的内部逻辑索引。 /// 仅供框架内部的导航与布局逻辑使用;业务层请改用 维护自身状态, /// 或使用适配器上的 ChoiceIndex 表示业务选中项。 /// internal int CurrentIndex => currentIndex; #endregion #region Events /// /// 当当前逻辑索引发生变化时触发。 /// public Action OnFocusIndexChanged; /// /// 当滚动位置发生变化时触发。 /// public Action OnScrollValueChanged; /// /// 当滚动停止时触发。 /// public Action OnScrollStopped; /// /// 当拖拽状态变化时触发。 /// public Action OnScrollDraggingChanged; #endregion #region Unity Lifecycle /// /// 初始化模板、滚动器、滚动条与导航桥接组件。 /// private void Awake() { if (mainThreadId < 0) { mainThreadId = Thread.CurrentThread.ManagedThreadId; } InitializeTemplates(); ConfigureScroller(); ConfigureScrollbar(); EnsureNavigationBridge(); } #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 EnsureNavigationBridge() { #if UX_NAVIGATION if (GetComponent() == null) { gameObject.AddComponent(); } #endif } /// /// 查找当前可见列表边缘对应的数据索引。 /// /// 表示查找最大的布局索引;否则查找最小的布局索引。 /// 找到的边缘数据索引;不存在可见项时返回 -1 private int FindVisibleEdgeDataIndex(bool useMax) { if (ViewProvider.ViewHolders.Count == 0) { return -1; } int best = useMax ? int.MinValue : int.MaxValue; for (int i = 0; i < ViewProvider.ViewHolders.Count; i++) { ViewHolder holder = ViewProvider.ViewHolders[i]; if (holder == null || holder.Index < 0) { continue; } if (useMax) { if (holder.Index > best) { best = holder.Index; } } else if (holder.Index < best) { best = holder.Index; } } return best is int.MinValue or int.MaxValue ? -1 : best; } /// /// 配置滚动器参数并注册滚动回调。 /// private void ConfigureScroller() { if (scroller == null) return; scroller.ScrollSpeed = scrollSpeed; scroller.WheelSpeed = wheelSpeed; scroller.Snap = snap; scroller.OnValueChanged.AddListener(OnScrollChanged); scroller.OnMoveStoped.AddListener(OnMoveStoped); scroller.OnDragging.AddListener(OnScrollerDraggingChanged); 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 /// /// 绑定新的适配器,并重建 RecyclerView 与布局管理器之间的关联关系。 /// /// 要绑定的适配器实例。 internal void SetAdapter(IAdapter adapter) { if (!EnsureMainThread(nameof(SetAdapter))) { return; } if (adapter == null) { Debug.LogError("Adapter cannot be null"); return; } if (layoutManager == null) { Debug.LogError("LayoutManager cannot be null"); return; } if (ReferenceEquals(RecyclerViewAdapter, adapter)) { return; } viewProvider?.Clear(); (RecyclerViewAdapter as IItemRenderCacheOwner)?.ReleaseAllItemRenders(); 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; startIndex = 0; endIndex = -1; currentIndex = -1; ClearPendingFocusRequest(); } /// /// 尝试获取当前可见区域内指定索引对应的视图持有者。 /// /// 目标布局索引。 /// 返回找到的视图持有者。 /// 找到且该持有者仍处于可见范围内时返回 ;否则返回 internal bool TryGetVisibleViewHolder(int index, out ViewHolder viewHolder) { viewHolder = ViewProvider.GetViewHolder(index); return viewHolder != null && layoutManager != null && layoutManager.IsVisible(viewHolder.Index); } /// /// 尝试将焦点移动到指定索引对应的列表项。 /// /// 目标数据索引。 /// 是否先以平滑滚动方式将目标项滚入可见区域。 /// 目标项滚动完成后的对齐方式。 /// 成功定位并应用焦点时返回 ;否则返回 public bool TryFocusIndex(int index, bool smooth = false, ScrollAlignment alignment = ScrollAlignment.Center) { if (RecyclerViewAdapter == null || RecyclerViewAdapter.GetItemCount() <= 0 || index < 0 || index >= RecyclerViewAdapter.GetItemCount()) { return false; } if (smooth && (!TryGetVisibleViewHolder(index, out ViewHolder smoothHolder) || !IsFullyVisible(smoothHolder))) { QueueFocusRequest(index, alignment); ScrollToWithAlignment(index, alignment, 0f, true); return true; } if (!TryGetVisibleViewHolder(index, out ViewHolder holder) || !IsFullyVisible(holder)) { ScrollToWithAlignment(index, alignment, 0f, false); if (!TryGetVisibleViewHolder(index, out holder)) { Refresh(); TryGetVisibleViewHolder(index, out holder); } } if (holder == null) { return false; } if (!IsFullyVisible(holder)) { ScrollToWithAlignment(index, alignment, 0f, false); Refresh(); TryGetVisibleViewHolder(index, out holder); } if (holder == null || !IsFullyVisible(holder) || !TryResolveFocusTarget(holder, out GameObject target)) { return false; } ApplyFocus(target); UpdateCurrentIndex(index); return true; } /// /// 按进入方向尝试将焦点移入当前列表。 /// /// 焦点进入列表时的方向。 /// 成功聚焦某个列表项时返回 ;否则返回 public bool TryFocusEntry( MoveDirection entryDirection, bool smooth = false, ScrollAlignment alignment = ScrollAlignment.Center) { if (RecyclerViewAdapter == null) { return false; } int realCount = RecyclerViewAdapter.GetRealCount(); if (realCount <= 0) { return false; } int targetIndex = entryDirection is MoveDirection.Up or MoveDirection.Left ? FindVisibleEdgeDataIndex(true) : FindVisibleEdgeDataIndex(false); if (targetIndex < 0) { targetIndex = entryDirection is MoveDirection.Up or MoveDirection.Left ? realCount - 1 : Mathf.Clamp(CurrentIndex, 0, realCount - 1); } int step = entryDirection is MoveDirection.Up or MoveDirection.Left ? -1 : 1; return TryFocusIndexRange(targetIndex, step, realCount, smooth, alignment); } /// /// 解析指定持有者最终应被聚焦的目标对象。 /// /// 目标视图持有者。 /// 返回解析得到的焦点对象。 /// 成功解析到可聚焦对象时返回 ;否则返回 internal bool TryResolveFocusTarget(ViewHolder holder, out GameObject target) { target = null; if (holder == null) { return false; } ItemInteractionProxy proxy = holder.GetComponent(); if (proxy != null) { return proxy.TryGetFocusTarget(out target); } Selectable selectable = holder.GetComponent(); if (!IsSelectableFocusable(selectable)) { Selectable[] selectables = holder.GetComponentsInChildren(true); for (int i = 0; i < selectables.Length; i++) { if (IsSelectableFocusable(selectables[i])) { selectable = selectables[i]; break; } } } if (IsSelectableFocusable(selectable)) { target = selectable.gameObject; return true; } if (ExecuteEvents.CanHandleEvent(holder.gameObject) || ExecuteEvents.CanHandleEvent(holder.gameObject) || ExecuteEvents.CanHandleEvent(holder.gameObject)) { target = holder.gameObject; return true; } return false; } /// /// 判断指定持有者是否已经完整处于当前视口内。 /// /// 待检测的视图持有者。 /// 完整可见时返回 ;否则返回 private bool IsFullyVisible(ViewHolder holder) { if (holder == null) { return false; } RectTransform viewport = content != null ? content.parent as RectTransform : null; if (viewport == null) { viewport = transform as RectTransform; } if (viewport == null) { return true; } Bounds bounds = RectTransformUtility.CalculateRelativeRectTransformBounds(viewport, holder.RectTransform); Rect viewportRect = viewport.rect; const float epsilon = 0.01f; return direction switch { Direction.Vertical => bounds.min.y >= viewportRect.yMin - epsilon && bounds.max.y <= viewportRect.yMax + epsilon, Direction.Horizontal => bounds.min.x >= viewportRect.xMin - epsilon && bounds.max.x <= viewportRect.xMax + epsilon, _ => bounds.min.x >= viewportRect.xMin - epsilon && bounds.max.x <= viewportRect.xMax + epsilon && bounds.min.y >= viewportRect.yMin - epsilon && bounds.max.y <= viewportRect.yMax + epsilon }; } /// /// 将 EventSystem 焦点切换到指定目标,并在下一帧做一次恢复校正。 /// /// 目标焦点对象。 private void ApplyFocus(GameObject target) { if (target == null) { return; } EventSystem eventSystem = EventSystem.current; if (eventSystem == null) { return; } if (ReferenceEquals(eventSystem.currentSelectedGameObject, target)) { ScheduleFocusRecovery(target); return; } if (eventSystem.alreadySelecting) { ScheduleFocusRecovery(target); return; } eventSystem.SetSelectedGameObject(target); ScheduleFocusRecovery(target); } private void ScheduleFocusRecovery(GameObject target) { if (focusRecoveryCoroutine != null) { StopCoroutine(focusRecoveryCoroutine); } focusRecoveryCoroutine = StartCoroutine(RecoverFocusNextFrame(target)); } private bool TryFocusIndexRange(int startIndex, int step, int itemCount, bool smooth, ScrollAlignment alignment) { if (itemCount <= 0 || step == 0) { return false; } int index = Mathf.Clamp(startIndex, 0, itemCount - 1); while (index >= 0 && index < itemCount) { if (TryFocusIndex(index, smooth, alignment)) { return true; } index += step; } return false; } private static bool IsSelectableFocusable(Selectable selectable) { return selectable != null && selectable.IsActive() && selectable.IsInteractable(); } /// /// 在下一帧尝试恢复目标对象的焦点,避免布局刷新期间焦点丢失。 /// /// 需要恢复焦点的目标对象。 /// 用于协程调度的枚举器。 private IEnumerator RecoverFocusNextFrame(GameObject target) { yield return null; focusRecoveryCoroutine = null; if (target == null || !target.activeInHierarchy) { yield break; } EventSystem eventSystem = EventSystem.current; if (eventSystem == null || ReferenceEquals(eventSystem.currentSelectedGameObject, target)) { yield break; } eventSystem.SetSelectedGameObject(target); } /// /// 重置视图池、滚动位置与当前索引状态。 /// internal void Reset() { if (!EnsureMainThread(nameof(Reset))) { return; } viewProvider?.Reset(); if (scroller != null) { scroller.Position = 0; } if (scrollbar != null) { scrollbar.SetValueWithoutNotify(0); } startIndex = 0; endIndex = -1; currentIndex = -1; ClearPendingFocusRequest(); } #endregion #region Public Methods - Layout /// /// 按当前滚动位置重新创建可见范围内的所有视图持有者。 /// internal void Refresh() { if (!EnsureMainThread(nameof(Refresh))) { return; } ViewProvider.Clear(); if (layoutManager == null || RecyclerViewAdapter == null || RecyclerViewAdapter.GetItemCount() <= 0) { startIndex = 0; endIndex = -1; return; } startIndex = Mathf.Max(0, layoutManager.GetStartIndex()); endIndex = layoutManager.GetEndIndex(); if (endIndex < startIndex) { return; } for (int i = startIndex; i <= endIndex; i += layoutManager.Unit) { ViewProvider.CreateViewHolder(i); } layoutManager.DoItemAnimation(); } /// /// 重新计算内容尺寸、滚动能力与对象池预热状态。 /// internal void RequestLayout() { if (!EnsureMainThread(nameof(RequestLayout))) { return; } if (layoutManager == null) { UpdateScrollbarVisibility(); return; } layoutManager.SetContentSize(); if (scroller == null) { viewProvider?.PreparePool(); UpdateScrollbarVisibility(); return; } scroller.Direction = direction; scroller.ViewSize = layoutManager.ViewportSize; scroller.ContentSize = layoutManager.ContentSize; scroller.Position = Mathf.Clamp(scroller.Position, 0, scroller.MaxPosition); viewProvider?.PreparePool(); UpdateScrollerState(); UpdateScrollbarVisibility(); UpdateScrollbarValue(scroller.Position); } #endregion #region Public Methods - Scrolling /// /// 获取当前滚动位置。 /// /// 当前滚动偏移量;未启用滚动器时返回 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 (duration > 0 && smooth) { scroller.ScrollToDuration(targetPosition, duration); } 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; // 计算目标项的原始位置,不在此阶段做范围限制。 float itemPosition = CalculateRawItemPosition(index); float targetPosition = alignment switch { ScrollAlignment.Start => itemPosition, ScrollAlignment.Center => itemPosition - (viewportLength - itemSize) / 2f, ScrollAlignment.End => itemPosition - viewportLength + itemSize, _ => itemPosition }; // 叠加调用方传入的额外偏移量。 targetPosition += offset; // 将结果限制在可滚动范围内。 return Mathf.Clamp(targetPosition, 0, scroller.MaxPosition); } /// /// 计算指定索引对应项在内容区域中的原始起始位置。 /// /// 目标数据索引。 /// 未做边界限制的原始滚动位置。 private float CalculateRawItemPosition(int index) { // 根据滚动方向选择对应轴向的间距与内边距。 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; // 直接基于索引、尺寸与间距推导原始位置。 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(position); } /// /// 响应滚动器停止移动事件,处理吸附与挂起焦点请求。 /// private void OnMoveStoped() { if (snap && SnapToNearestItem()) { return; } TryProcessPendingFocusRequest(); OnScrollStopped?.Invoke(); } /// /// 响应滚动器拖拽状态变化。 /// /// 当前是否正在拖拽。 private void OnScrollerDraggingChanged(bool dragging) { OnScrollDraggingChanged?.Invoke(dragging); } /// /// 响应滚动条值变化并同步滚动器位置。 /// /// 滚动条归一化值。 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() { if (layoutManager == null || RecyclerViewAdapter == null || RecyclerViewAdapter.GetItemCount() <= 0) { return; } // 处理可见区间起始端的回收与补充。 if (layoutManager.IsFullInvisibleStart(startIndex)) { viewProvider.RemoveViewHolder(startIndex); startIndex += layoutManager.Unit; } else if (layoutManager.IsFullVisibleStart(startIndex)) { if (startIndex == 0) { // TODO: 在滚动到列表起始端时补充刷新逻辑。 } else { startIndex -= layoutManager.Unit; viewProvider.CreateViewHolder(startIndex); } } // 处理可见区间末端的回收与补充。 if (layoutManager.IsFullInvisibleEnd(endIndex)) { viewProvider.RemoveViewHolder(endIndex); endIndex -= layoutManager.Unit; } else if (layoutManager.IsFullVisibleEnd(endIndex)) { if (endIndex >= viewProvider.GetItemCount() - layoutManager.Unit) { // TODO: 在滚动到列表末端时补充加载更多逻辑。 } else { endIndex += layoutManager.Unit; viewProvider.CreateViewHolder(endIndex); } } // 若增量更新后的区间与实际可见区不一致,则退化为全量刷新。 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() { float contentLength; float viewLength; float trackLength; if (direction == Direction.Vertical) { contentLength = scroller.ContentSize.y; viewLength = scroller.ViewSize.y; trackLength = GetScrollbarTrackLength(true); } else { contentLength = scroller.ContentSize.x; viewLength = scroller.ViewSize.x; trackLength = GetScrollbarTrackLength(false); } if (contentLength <= 0f || viewLength <= 0f) { scrollbar.size = 1f; return; } float normalizedSize = viewLength / contentLength; float minNormalizedSize = trackLength > 0f ? Mathf.Clamp01(MinScrollbarHandlePixels / trackLength) : 0f; scrollbar.size = Mathf.Clamp(Mathf.Max(normalizedSize, minNormalizedSize), minNormalizedSize, 1f); } /// /// 获取滚动条轨道的像素长度。 /// /// 是否按垂直滚动条计算。 /// 滚动条轨道长度;无法获取时返回 0 private float GetScrollbarTrackLength(bool vertical) { if (scrollbar == null) { return 0f; } RectTransform scrollbarRect = scrollbar.transform as RectTransform; if (scrollbarRect == null) { return 0f; } Rect rect = scrollbarRect.rect; return vertical ? rect.height : rect.width; } /// /// 根据当前内容是否可滚动来更新滚动器启用状态。 /// 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 bool SnapToNearestItem() { int index = layoutManager.PositionToIndex(GetScrollPosition()); float targetPosition = layoutManager.IndexToPosition(index); if (Mathf.Abs(targetPosition - GetScrollPosition()) <= 0.1f) { return false; } ScrollTo(index, true); return true; } /// /// 更新当前逻辑索引,并在变化时触发事件通知。 /// /// 新的候选索引。 private void UpdateCurrentIndex(int index) { if (RecyclerViewAdapter == null) return; int itemCount = RecyclerViewAdapter.GetRealCount(); if (itemCount <= 0) { itemCount = RecyclerViewAdapter.GetItemCount(); } if (itemCount <= 0) { if (currentIndex != -1) { currentIndex = -1; OnFocusIndexChanged?.Invoke(currentIndex); } return; } index %= itemCount; index = index < 0 ? itemCount + index : index; if (currentIndex != index) { currentIndex = index; OnFocusIndexChanged?.Invoke(currentIndex); } } /// /// 重新绑定当前可见区域内指定数据索引对应的所有持有者。 /// /// 要重绑的数据索引。 /// 实际完成重绑的持有者数量。 internal int RebindVisibleDataIndex(int dataIndex) { if (!EnsureMainThread(nameof(RebindVisibleDataIndex)) || RecyclerViewAdapter == null || !ViewProvider.TryGetViewHoldersByDataIndex(dataIndex, out IReadOnlyList holders)) { return 0; } int reboundCount = 0; for (int i = 0; i < holders.Count; i++) { ViewHolder holder = holders[i]; if (holder == null) { continue; } RecyclerViewAdapter.OnBindViewHolder(holder, holder.Index); reboundCount++; } return reboundCount; } /// /// 重新绑定当前可见区域内指定数据区间对应的所有持有者。 /// /// 起始数据索引。 /// 需要重绑的数据项数量。 /// 实际完成重绑的持有者总数。 internal int RebindVisibleDataRange(int startDataIndex, int count) { if (count <= 0) { return 0; } int reboundCount = 0; int endDataIndex = startDataIndex + count; for (int dataIndex = startDataIndex; dataIndex < endDataIndex; dataIndex++) { reboundCount += RebindVisibleDataIndex(dataIndex); } return reboundCount; } /// /// 缓存一条等待滚动结束后执行的焦点请求。 /// /// 待聚焦的数据索引。 /// 目标对齐方式。 private void QueueFocusRequest(int index, ScrollAlignment alignment) { hasPendingFocusRequest = true; pendingFocusIndex = index; pendingFocusAlignment = alignment; } /// /// 清除当前缓存的焦点请求。 /// private void ClearPendingFocusRequest() { hasPendingFocusRequest = false; pendingFocusIndex = -1; pendingFocusAlignment = ScrollAlignment.Center; } /// /// 尝试执行当前缓存的焦点请求。 /// private void TryProcessPendingFocusRequest() { if (!hasPendingFocusRequest) { return; } int index = pendingFocusIndex; ScrollAlignment alignment = pendingFocusAlignment; ClearPendingFocusRequest(); TryFocusIndex(index, false, alignment); } /// /// 判断当前调用线程是否为 Unity 主线程。 /// /// 当前线程为主线程时返回 ;否则返回 private static bool IsMainThread() { return mainThreadId < 0 || Thread.CurrentThread.ManagedThreadId == mainThreadId; } /// /// 校验当前调用是否发生在 Unity 主线程上。 /// /// 发起校验的调用方名称。 /// 位于主线程时返回 ;否则返回 private bool EnsureMainThread(string caller) { if (IsMainThread()) { return true; } Debug.LogError($"RecyclerView.{caller} must run on Unity main thread."); return false; } #endregion } }