com.alicizax.unity.ui.exten.../Runtime/RecyclerView/RecyclerView.cs

1633 lines
52 KiB
C#
Raw Normal View History

using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
2025-03-12 20:59:12 +08:00
using UnityEngine;
using UnityEngine.EventSystems;
2025-03-12 20:59:12 +08:00
using UnityEngine.UI;
namespace AlicizaX.UI
2025-03-12 20:59:12 +08:00
{
2026-03-11 14:18:07 +08:00
/// <summary>
/// RecyclerView 的核心组件,负责适配器绑定、布局刷新、滚动控制与焦点导航。
2026-03-11 14:18:07 +08:00
/// </summary>
2025-03-12 20:59:12 +08:00
public class RecyclerView : MonoBehaviour
{
/// <summary>
/// 滚动条拖拽手柄允许的最小像素长度。
/// </summary>
private const float MinScrollbarHandlePixels = 18f;
/// <summary>
/// 记录 Unity 主线程的托管线程标识。
/// </summary>
private static int mainThreadId = -1;
2025-12-26 14:22:46 +08:00
#region Serialized Fields - Layout Settings
/// <summary>
/// 列表的主滚动方向。
/// </summary>
2025-12-26 14:22:46 +08:00
[HideInInspector] [SerializeField] private Direction direction;
/// <summary>
/// 列表项在交叉轴上的对齐方式。
/// </summary>
2025-12-26 14:22:46 +08:00
[HideInInspector] [SerializeField] private Alignment alignment;
/// <summary>
/// 列表项之间的间距。
/// </summary>
2025-12-26 14:22:46 +08:00
[HideInInspector] [SerializeField] private Vector2 spacing;
/// <summary>
/// 列表内容区域的内边距。
/// </summary>
2025-12-26 14:22:46 +08:00
[HideInInspector] [SerializeField] private Vector2 padding;
#endregion
#region Serialized Fields - Scroll Settings
/// <summary>
/// 是否启用滚动能力。
/// </summary>
2025-12-26 14:22:46 +08:00
[HideInInspector] [SerializeField] private bool scroll;
/// <summary>
/// 是否在停止滚动后自动吸附到最近项。
/// </summary>
2025-12-26 14:22:46 +08:00
[HideInInspector] [SerializeField] private bool snap;
/// <summary>
/// 平滑滚动时的速度系数。
/// </summary>
[HideInInspector] [SerializeField, Range(0.5f, 50f)]
2025-12-26 14:22:46 +08:00
private float scrollSpeed = 7f;
/// <summary>
/// 鼠标滚轮滚动时的速度系数。
/// </summary>
[HideInInspector] [SerializeField, Range(1f, 50f)]
2025-12-26 14:22:46 +08:00
private float wheelSpeed = 30f;
#endregion
#region Serialized Fields - Components
/// <summary>
/// 可用于创建列表项的模板集合。
/// </summary>
2025-12-26 14:22:46 +08:00
[HideInInspector] [SerializeField] private ViewHolder[] templates;
/// <summary>
/// 承载所有列表项的内容节点。
/// </summary>
2025-12-26 14:22:46 +08:00
[HideInInspector] [SerializeField] private RectTransform content;
/// <summary>
/// 是否显示滚动条。
/// </summary>
2025-12-26 14:22:46 +08:00
[HideInInspector] [SerializeField] private bool showScrollBar;
/// <summary>
/// 是否仅在内容可滚动时显示滚动条。
/// </summary>
[HideInInspector] [SerializeField] private bool showScrollBarOnlyWhenScrollable;
/// <summary>
/// 与当前列表关联的滚动条组件。
/// </summary>
2025-12-26 14:22:46 +08:00
[HideInInspector] [SerializeField] private Scrollbar scrollbar;
#endregion
#region Serialized Fields - Internal (Hidden in Inspector)
/// <summary>
/// 序列化保存的布局管理器类型名称。
/// </summary>
2025-12-26 14:22:46 +08:00
[HideInInspector] [SerializeField] private string _layoutManagerTypeName;
/// <summary>
/// 当前使用的布局管理器实例。
/// </summary>
2025-12-26 14:22:46 +08:00
[SerializeReference] private LayoutManager layoutManager;
/// <summary>
/// 序列化保存的滚动器类型名称。
/// </summary>
2025-12-26 14:22:46 +08:00
[HideInInspector] [SerializeField] private string _scrollerTypeName;
/// <summary>
/// 当前使用的滚动器实例。
/// </summary>
2025-12-26 14:22:46 +08:00
[HideInInspector] [SerializeReference] private Scroller scroller;
#endregion
#region Private Fields
/// <summary>
/// 负责创建、回收与查询视图持有者的提供器。
/// </summary>
2025-12-26 14:22:46 +08:00
private ViewProvider viewProvider;
/// <summary>
/// 负责处理列表内导航逻辑的控制器。
/// </summary>
private RecyclerNavigationController navigationController;
/// <summary>
/// 下一帧恢复 UI 焦点时使用的协程句柄。
/// </summary>
private Coroutine focusRecoveryCoroutine;
/// <summary>
/// 是否存在等待滚动结束后执行的焦点请求。
/// </summary>
private bool hasPendingFocusRequest;
/// <summary>
/// 挂起焦点请求期望采用的对齐方式。
/// </summary>
private ScrollAlignment pendingFocusAlignment;
/// <summary>
/// 挂起焦点请求对应的数据索引。
/// </summary>
private int pendingFocusIndex = -1;
/// <summary>
/// 当前可见区间的起始布局索引。
/// </summary>
2025-12-26 14:22:46 +08:00
private int startIndex;
/// <summary>
/// 当前可见区间的结束布局索引。
/// </summary>
2025-12-26 14:22:46 +08:00
private int endIndex;
/// <summary>
/// 当前记录的逻辑选中索引。
/// </summary>
2025-12-26 14:22:46 +08:00
private int currentIndex;
#endregion
#region Public Properties - Layout Settings
2025-04-02 14:53:08 +08:00
2026-03-11 14:18:07 +08:00
/// <summary>
/// 获取或设置列表的主滚动方向。
2026-03-11 14:18:07 +08:00
/// </summary>
2025-03-12 20:59:12 +08:00
public Direction Direction
{
get => direction;
set => direction = value;
}
2026-03-11 14:18:07 +08:00
/// <summary>
/// 获取或设置列表项在交叉轴上的对齐方式。
2026-03-11 14:18:07 +08:00
/// </summary>
2025-03-12 20:59:12 +08:00
public Alignment Alignment
{
get => alignment;
set => alignment = value;
}
2026-03-11 14:18:07 +08:00
/// <summary>
/// 获取或设置列表项之间的间距。
2026-03-11 14:18:07 +08:00
/// </summary>
2025-03-12 20:59:12 +08:00
public Vector2 Spacing
{
get => spacing;
set => spacing = value;
}
2026-03-11 14:18:07 +08:00
/// <summary>
/// 获取或设置列表内容区域的内边距。
2026-03-11 14:18:07 +08:00
/// </summary>
2025-03-12 20:59:12 +08:00
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
2026-03-11 14:18:07 +08:00
/// <summary>
/// 获取或设置是否启用滚动能力。
2026-03-11 14:18:07 +08:00
/// </summary>
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;
if (scroller != null)
{
scroller.enabled = scroll;
scroller.WheelSpeed = wheelSpeed;
scroller.Snap = snap;
}
{
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
2026-03-11 14:18:07 +08:00
/// <summary>
/// 获取或设置是否在停止滚动后自动吸附到最近项。
2026-03-11 14:18:07 +08:00
/// </summary>
2025-03-12 20:59:12 +08:00
public bool Snap
{
get => snap;
2025-12-26 14:22:46 +08:00
set
{
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
}
}
2025-04-02 14:53:08 +08:00
2026-03-11 14:18:07 +08:00
/// <summary>
/// 获取或设置平滑滚动速度系数。
2026-03-11 14:18:07 +08:00
/// </summary>
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
2026-03-11 14:18:07 +08:00
/// <summary>
/// 获取或设置鼠标滚轮滚动速度系数。
2026-03-11 14:18:07 +08:00
/// </summary>
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
}
/// <summary>
/// 获取或设置是否仅在内容可滚动时显示滚动条。
/// </summary>
public bool ShowScrollBarOnlyWhenScrollable
{
get => showScrollBarOnlyWhenScrollable;
set
{
if (showScrollBarOnlyWhenScrollable == value) return;
showScrollBarOnlyWhenScrollable = value;
RequestLayout();
}
}
2025-12-26 14:22:46 +08:00
#endregion
#region Public Properties - Components
2025-04-02 14:53:08 +08:00
2026-03-11 14:18:07 +08:00
/// <summary>
/// 获取或设置用于创建列表项的模板集合。
2026-03-11 14:18:07 +08:00
/// </summary>
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
}
2026-03-11 14:18:07 +08:00
/// <summary>
/// 获取内容节点;未显式指定时会尝试从首个子节点推断。
2026-03-11 14:18:07 +08:00
/// </summary>
2025-12-26 14:22:46 +08:00
public RectTransform Content
{
get
{
if (content == null)
{
if (transform.childCount == 0)
{
throw new InvalidOperationException("RecyclerView content is not assigned and no child RectTransform exists.");
}
2025-12-26 14:22:46 +08:00
content = transform.GetChild(0).GetComponent<RectTransform>();
if (content == null)
{
throw new InvalidOperationException("RecyclerView content child must have a RectTransform component.");
}
2025-12-26 14:22:46 +08:00
}
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
2026-03-11 14:18:07 +08:00
/// <summary>
/// 获取当前绑定的滚动条组件。
2026-03-11 14:18:07 +08:00
/// </summary>
2025-12-26 14:22:46 +08:00
public Scrollbar Scrollbar => scrollbar;
2025-05-30 10:32:08 +08:00
2026-03-11 14:18:07 +08:00
/// <summary>
/// 获取当前绑定的滚动器实例。
2026-03-11 14:18:07 +08:00
/// </summary>
2025-12-26 14:22:46 +08:00
public Scroller Scroller => scroller;
2025-05-30 10:32:08 +08:00
2026-03-11 14:18:07 +08:00
/// <summary>
/// 获取视图提供器;首次访问时根据模板数量自动创建。
2026-03-11 14:18:07 +08:00
/// </summary>
2025-12-26 14:22:46 +08:00
public ViewProvider ViewProvider
{
get
{
if (viewProvider == null)
{
if (templates == null || templates.Length == 0)
{
throw new InvalidOperationException("RecyclerView templates can not be null or empty.");
}
2025-12-26 14:22:46 +08:00
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
/// <summary>
/// 获取当前对象池的统计信息文本。
/// </summary>
public string PoolStats => viewProvider?.PoolStats ?? string.Empty;
/// <summary>
/// 获取当前布局管理器实例。
/// </summary>
public LayoutManager LayoutManager => layoutManager;
/// <summary>
/// 获取导航控制器;首次访问时自动创建。
/// </summary>
public RecyclerNavigationController NavigationController => navigationController ??= new RecyclerNavigationController(this);
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
2026-03-11 14:18:07 +08:00
/// <summary>
/// 获取或设置当前绑定的适配器实例。
2026-03-11 14:18:07 +08:00
/// </summary>
2026-04-01 11:58:08 +08:00
internal IAdapter RecyclerViewAdapter { get; private set; }
2025-06-03 17:08:27 +08:00
2026-03-11 14:18:07 +08:00
/// <summary>
2026-04-01 11:58:08 +08:00
/// 获取当前记录的内部逻辑索引。
2026-04-01 14:43:37 +08:00
/// 仅供框架内部的导航与布局逻辑使用;业务层请改用 <see cref="OnFocusIndexChanged"/> 维护自身状态,
2026-04-01 11:58:08 +08:00
/// 或使用适配器上的 <c>ChoiceIndex</c> 表示业务选中项。
2026-03-11 14:18:07 +08:00
/// </summary>
2026-04-01 11:58:08 +08:00
internal int CurrentIndex => currentIndex;
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
2026-03-11 14:18:07 +08:00
/// <summary>
/// 当当前逻辑索引发生变化时触发。
2026-03-11 14:18:07 +08:00
/// </summary>
2026-04-01 14:43:37 +08:00
public Action<int> OnFocusIndexChanged;
/// <summary>
/// 当滚动位置发生变化时触发。
/// </summary>
public Action<float> OnScrollValueChanged;
/// <summary>
/// 当滚动停止时触发。
/// </summary>
public Action OnScrollStopped;
/// <summary>
/// 当拖拽状态变化时触发。
/// </summary>
public Action<bool> OnScrollDraggingChanged;
2026-03-11 14:18:07 +08:00
2025-12-26 14:22:46 +08:00
#endregion
#region Unity Lifecycle
/// <summary>
/// 初始化模板、滚动器、滚动条与导航桥接组件。
/// </summary>
2025-12-26 14:22:46 +08:00
private void Awake()
2025-03-12 20:59:12 +08:00
{
if (mainThreadId < 0)
{
mainThreadId = Thread.CurrentThread.ManagedThreadId;
}
2025-12-26 14:22:46 +08:00
InitializeTemplates();
ConfigureScroller();
ConfigureScrollbar();
EnsureNavigationBridge();
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
/// <summary>
/// 初始化所有模板实例并将其隐藏,避免模板对象直接参与显示。
/// </summary>
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
}
}
/// <summary>
/// 确保当前对象挂载用于导航事件桥接的组件。
/// </summary>
private void EnsureNavigationBridge()
{
2026-04-13 15:45:34 +08:00
#if UX_NAVIGATION
if (GetComponent<RecyclerNavigationBridge>() == null)
{
gameObject.AddComponent<RecyclerNavigationBridge>();
}
2026-04-13 15:45:34 +08:00
#endif
}
/// <summary>
/// 查找当前可见列表边缘对应的数据索引。
/// </summary>
/// <param name="useMax"><see langword="true"/> 表示查找最大的布局索引;否则查找最小的布局索引。</param>
/// <returns>找到的边缘数据索引;不存在可见项时返回 <c>-1</c>。</returns>
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;
}
/// <summary>
/// 配置滚动器参数并注册滚动回调。
/// </summary>
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);
2026-04-01 14:43:37 +08:00
scroller.OnDragging.AddListener(OnScrollerDraggingChanged);
UpdateScrollerState();
2025-12-26 14:22:46 +08:00
}
/// <summary>
/// 配置滚动条监听与拖拽结束回调。
/// </summary>
2025-12-26 14:22:46 +08:00
private void ConfigureScrollbar()
{
if (!showScrollBar || scrollbar == null) return;
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;
UpdateScrollbarVisibility();
2025-12-26 14:22:46 +08:00
}
#endregion
#region Public Methods - Setup
2026-03-11 14:18:07 +08:00
/// <summary>
/// 绑定新的适配器,并重建 RecyclerView 与布局管理器之间的关联关系。
2026-03-11 14:18:07 +08:00
/// </summary>
/// <param name="adapter">要绑定的适配器实例。</param>
2026-04-01 11:58:08 +08:00
internal void SetAdapter(IAdapter adapter)
2025-12-26 14:22:46 +08:00
{
if (!EnsureMainThread(nameof(SetAdapter)))
{
return;
}
2025-12-26 14:22:46 +08:00
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
if (layoutManager == null)
{
Debug.LogError("LayoutManager cannot be null");
return;
}
if (ReferenceEquals(RecyclerViewAdapter, adapter))
{
return;
}
viewProvider?.Clear();
(RecyclerViewAdapter as IItemRenderCacheOwner)?.ReleaseAllItemRenders();
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;
startIndex = 0;
endIndex = -1;
currentIndex = -1;
ClearPendingFocusRequest();
}
/// <summary>
/// 尝试获取当前可见区域内指定索引对应的视图持有者。
/// </summary>
/// <param name="index">目标布局索引。</param>
/// <param name="viewHolder">返回找到的视图持有者。</param>
/// <returns>找到且该持有者仍处于可见范围内时返回 <see langword="true"/>;否则返回 <see langword="false"/>。</returns>
2026-04-01 11:58:08 +08:00
internal bool TryGetVisibleViewHolder(int index, out ViewHolder viewHolder)
{
viewHolder = ViewProvider.GetViewHolder(index);
return viewHolder != null && layoutManager != null && layoutManager.IsVisible(viewHolder.Index);
2025-12-26 14:22:46 +08:00
}
2026-03-11 14:18:07 +08:00
/// <summary>
/// 尝试将焦点移动到指定索引对应的列表项。
/// </summary>
/// <param name="index">目标数据索引。</param>
/// <param name="smooth">是否先以平滑滚动方式将目标项滚入可见区域。</param>
/// <param name="alignment">目标项滚动完成后的对齐方式。</param>
/// <returns>成功定位并应用焦点时返回 <see langword="true"/>;否则返回 <see langword="false"/>。</returns>
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;
}
/// <summary>
/// 按进入方向尝试将焦点移入当前列表。
/// </summary>
/// <param name="entryDirection">焦点进入列表时的方向。</param>
/// <returns>成功聚焦某个列表项时返回 <see langword="true"/>;否则返回 <see langword="false"/>。</returns>
2026-04-03 15:21:13 +08:00
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);
}
2026-04-03 15:21:13 +08:00
int step = entryDirection is MoveDirection.Up or MoveDirection.Left ? -1 : 1;
return TryFocusIndexRange(targetIndex, step, realCount, smooth, alignment);
}
/// <summary>
/// 解析指定持有者最终应被聚焦的目标对象。
/// </summary>
/// <param name="holder">目标视图持有者。</param>
/// <param name="target">返回解析得到的焦点对象。</param>
/// <returns>成功解析到可聚焦对象时返回 <see langword="true"/>;否则返回 <see langword="false"/>。</returns>
internal bool TryResolveFocusTarget(ViewHolder holder, out GameObject target)
{
target = null;
if (holder == null)
{
return false;
}
ItemInteractionProxy proxy = holder.GetComponent<ItemInteractionProxy>();
2026-04-03 15:21:13 +08:00
if (proxy != null)
{
2026-04-03 15:21:13 +08:00
return proxy.TryGetFocusTarget(out target);
}
2026-04-03 15:21:13 +08:00
Selectable selectable = holder.GetComponent<Selectable>();
if (!IsSelectableFocusable(selectable))
{
Selectable[] selectables = holder.GetComponentsInChildren<Selectable>(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<IMoveHandler>(holder.gameObject) ||
ExecuteEvents.CanHandleEvent<ISelectHandler>(holder.gameObject) ||
ExecuteEvents.CanHandleEvent<ISubmitHandler>(holder.gameObject))
{
target = holder.gameObject;
return true;
}
return false;
}
/// <summary>
/// 判断指定持有者是否已经完整处于当前视口内。
/// </summary>
/// <param name="holder">待检测的视图持有者。</param>
/// <returns>完整可见时返回 <see langword="true"/>;否则返回 <see langword="false"/>。</returns>
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
};
}
/// <summary>
/// 将 EventSystem 焦点切换到指定目标,并在下一帧做一次恢复校正。
/// </summary>
/// <param name="target">目标焦点对象。</param>
private void ApplyFocus(GameObject target)
{
if (target == null)
{
return;
}
EventSystem eventSystem = EventSystem.current;
if (eventSystem == null)
{
return;
}
2026-04-03 15:21:13 +08:00
if (ReferenceEquals(eventSystem.currentSelectedGameObject, target))
{
2026-04-03 15:21:13 +08:00
ScheduleFocusRecovery(target);
return;
}
if (eventSystem.alreadySelecting)
{
ScheduleFocusRecovery(target);
return;
}
2026-04-03 15:21:13 +08:00
eventSystem.SetSelectedGameObject(target);
ScheduleFocusRecovery(target);
}
private void ScheduleFocusRecovery(GameObject target)
{
if (focusRecoveryCoroutine != null)
{
StopCoroutine(focusRecoveryCoroutine);
}
focusRecoveryCoroutine = StartCoroutine(RecoverFocusNextFrame(target));
}
2026-04-03 15:21:13 +08:00
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();
}
/// <summary>
/// 在下一帧尝试恢复目标对象的焦点,避免布局刷新期间焦点丢失。
/// </summary>
/// <param name="target">需要恢复焦点的目标对象。</param>
/// <returns>用于协程调度的枚举器。</returns>
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);
}
/// <summary>
/// 重置视图池、滚动位置与当前索引状态。
2026-03-11 14:18:07 +08:00
/// </summary>
2026-04-01 11:58:08 +08:00
internal void Reset()
2025-12-26 14:22:46 +08:00
{
if (!EnsureMainThread(nameof(Reset)))
{
return;
}
2025-12-26 14:22:46 +08:00
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
}
startIndex = 0;
endIndex = -1;
currentIndex = -1;
ClearPendingFocusRequest();
2025-12-26 14:22:46 +08:00
}
#endregion
#region Public Methods - Layout
2026-03-11 14:18:07 +08:00
/// <summary>
/// 按当前滚动位置重新创建可见范围内的所有视图持有者。
2026-03-11 14:18:07 +08:00
/// </summary>
2026-04-01 11:58:08 +08:00
internal void Refresh()
2025-12-26 14:22:46 +08:00
{
if (!EnsureMainThread(nameof(Refresh)))
{
return;
}
2025-12-26 14:22:46 +08:00
ViewProvider.Clear();
if (layoutManager == null || RecyclerViewAdapter == null || RecyclerViewAdapter.GetItemCount() <= 0)
{
startIndex = 0;
endIndex = -1;
return;
}
2025-12-26 14:22:46 +08:00
startIndex = Mathf.Max(0, layoutManager.GetStartIndex());
2025-12-26 14:22:46 +08:00
endIndex = layoutManager.GetEndIndex();
if (endIndex < startIndex)
{
return;
}
2025-12-26 14:22:46 +08:00
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();
}
2026-03-11 14:18:07 +08:00
/// <summary>
/// 重新计算内容尺寸、滚动能力与对象池预热状态。
2026-03-11 14:18:07 +08:00
/// </summary>
2026-04-01 11:58:08 +08:00
internal void RequestLayout()
2025-12-26 14:22:46 +08:00
{
if (!EnsureMainThread(nameof(RequestLayout)))
{
return;
}
if (layoutManager == null)
{
UpdateScrollbarVisibility();
return;
}
2025-12-26 14:22:46 +08:00
layoutManager.SetContentSize();
if (scroller == null)
{
viewProvider?.PreparePool();
UpdateScrollbarVisibility();
return;
}
2025-12-26 14:22:46 +08:00
scroller.Direction = direction;
scroller.ViewSize = layoutManager.ViewportSize;
scroller.ContentSize = layoutManager.ContentSize;
scroller.Position = Mathf.Clamp(scroller.Position, 0, scroller.MaxPosition);
viewProvider?.PreparePool();
2025-12-26 14:22:46 +08:00
UpdateScrollerState();
2025-12-26 14:22:46 +08:00
UpdateScrollbarVisibility();
UpdateScrollbarValue(scroller.Position);
2025-12-26 14:22:46 +08:00
}
#endregion
#region Public Methods - Scrolling
2026-03-11 14:18:07 +08:00
/// <summary>
/// 获取当前滚动位置。
2026-03-11 14:18:07 +08:00
/// </summary>
/// <returns>当前滚动偏移量;未启用滚动器时返回 <c>0</c>。</returns>
2025-12-26 14:22:46 +08:00
public float GetScrollPosition()
{
return scroller != null ? scroller.Position : 0;
}
2026-03-11 14:18:07 +08:00
/// <summary>
/// 将列表滚动到指定索引对应的位置。
2026-03-11 14:18:07 +08:00
/// </summary>
/// <param name="index">目标数据索引。</param>
/// <param name="smooth">是否使用平滑滚动。</param>
2025-12-26 14:22:46 +08:00
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
}
2026-03-11 14:18:07 +08:00
/// <summary>
/// 将列表滚动到指定索引,并按给定对齐方式定位。
2026-03-11 14:18:07 +08:00
/// </summary>
/// <param name="index">目标数据索引。</param>
/// <param name="alignment">目标项滚动完成后的对齐方式。</param>
/// <param name="offset">在对齐基础上的额外偏移量。</param>
/// <param name="smooth">是否使用平滑滚动。</param>
/// <param name="duration">平滑滚动时长,单位为秒。</param>
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 (duration > 0 && smooth)
{
scroller.ScrollToDuration(targetPosition, duration);
2025-12-26 15:39:31 +08:00
}
else
{
scroller.ScrollTo(targetPosition, smooth);
}
if (!smooth)
{
Refresh();
}
UpdateCurrentIndex(index);
}
/// <summary>
/// 计算指定索引在目标对齐方式下应滚动到的位置。
/// </summary>
/// <param name="index">目标数据索引。</param>
/// <param name="alignment">目标项滚动完成后的对齐方式。</param>
/// <param name="offset">在对齐基础上的额外偏移量。</param>
/// <returns>计算得到的滚动位置,结果会被限制在合法范围内。</returns>
2025-12-26 15:39:31 +08:00
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;
// 计算目标项的原始位置,不在此阶段做范围限制。
2025-12-26 15:39:31 +08:00
float itemPosition = CalculateRawItemPosition(index);
float targetPosition = alignment switch
{
ScrollAlignment.Start => itemPosition,
ScrollAlignment.Center => itemPosition - (viewportLength - itemSize) / 2f,
ScrollAlignment.End => itemPosition - viewportLength + itemSize,
_ => itemPosition
};
// 叠加调用方传入的额外偏移量。
2025-12-26 15:39:31 +08:00
targetPosition += offset;
// 将结果限制在可滚动范围内。
2025-12-26 15:39:31 +08:00
return Mathf.Clamp(targetPosition, 0, scroller.MaxPosition);
}
/// <summary>
/// 计算指定索引对应项在内容区域中的原始起始位置。
/// </summary>
/// <param name="index">目标数据索引。</param>
/// <returns>未做边界限制的原始滚动位置。</returns>
2025-12-26 15:39:31 +08:00
private float CalculateRawItemPosition(int index)
{
// 根据滚动方向选择对应轴向的间距与内边距。
2025-12-26 15:39:31 +08:00
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;
// 直接基于索引、尺寸与间距推导原始位置。
2025-12-26 15:39:31 +08:00
return index * (itemSize + spacingValue) + paddingValue;
}
/// <summary>
/// 获取指定索引对应项在主滚动轴上的尺寸。
/// </summary>
/// <param name="index">目标数据索引。</param>
/// <returns>目标项在主滚动轴上的尺寸值。</returns>
2025-12-26 15:39:31 +08:00
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
/// <summary>
/// 响应滚动器位置变化,更新布局、滚动条与可见区间。
/// </summary>
/// <param name="position">当前滚动位置。</param>
2025-12-26 14:22:46 +08:00
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();
2026-04-01 14:43:37 +08:00
OnScrollValueChanged?.Invoke(position);
2025-06-03 17:08:27 +08:00
}
/// <summary>
/// 响应滚动器停止移动事件,处理吸附与挂起焦点请求。
/// </summary>
2025-03-12 20:59:12 +08:00
private void OnMoveStoped()
{
if (snap && SnapToNearestItem())
2025-03-12 20:59:12 +08:00
{
return;
2025-03-12 20:59:12 +08:00
}
TryProcessPendingFocusRequest();
2026-04-01 14:43:37 +08:00
OnScrollStopped?.Invoke();
}
/// <summary>
/// 响应滚动器拖拽状态变化。
/// </summary>
/// <param name="dragging">当前是否正在拖拽。</param>
private void OnScrollerDraggingChanged(bool dragging)
{
OnScrollDraggingChanged?.Invoke(dragging);
2025-03-12 20:59:12 +08:00
}
/// <summary>
/// 响应滚动条值变化并同步滚动器位置。
/// </summary>
/// <param name="ratio">滚动条归一化值。</param>
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
}
/// <summary>
/// 响应滚动条拖拽结束事件,并在需要时触发吸附。
/// </summary>
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
/// <summary>
/// 根据当前滚动位置同步滚动条显示值。
/// </summary>
/// <param name="position">当前滚动位置。</param>
2025-12-26 14:22:46 +08:00
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
}
}
/// <summary>
/// 根据当前滚动位置增量更新可见区间内的视图持有者。
/// </summary>
2025-12-26 14:22:46 +08:00
private void UpdateVisibleRange()
2025-04-01 15:21:02 +08:00
{
if (layoutManager == null || RecyclerViewAdapter == null || RecyclerViewAdapter.GetItemCount() <= 0)
{
return;
}
// 处理可见区间起始端的回收与补充。
2025-12-26 14:22:46 +08:00
if (layoutManager.IsFullInvisibleStart(startIndex))
{
viewProvider.RemoveViewHolder(startIndex);
startIndex += layoutManager.Unit;
}
else if (layoutManager.IsFullVisibleStart(startIndex))
{
if (startIndex == 0)
{
// TODO: 在滚动到列表起始端时补充刷新逻辑。
2025-12-26 14:22:46 +08:00
}
else
{
startIndex -= layoutManager.Unit;
viewProvider.CreateViewHolder(startIndex);
}
}
2025-04-01 15:21:02 +08:00
// 处理可见区间末端的回收与补充。
2025-12-26 14:22:46 +08:00
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: 在滚动到列表末端时补充加载更多逻辑。
2025-12-26 14:22:46 +08:00
}
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
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
}
}
/// <summary>
/// 根据当前状态更新滚动条的显示与交互能力。
/// </summary>
2025-12-26 14:22:46 +08:00
private void UpdateScrollbarVisibility()
2025-03-12 20:59:12 +08:00
{
if (scrollbar == null)
{
2025-12-26 14:22:46 +08:00
return;
}
2025-03-12 20:59:12 +08:00
2025-12-26 14:22:46 +08:00
bool shouldShow = ShouldShowScrollbar();
scrollbar.gameObject.SetActive(shouldShow);
scrollbar.interactable = 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
}
}
/// <summary>
/// 判断当前是否应显示滚动条。
/// </summary>
/// <returns>应显示滚动条时返回 <see langword="true"/>;否则返回 <see langword="false"/>。</returns>
2025-12-26 14:22:46 +08:00
private bool ShouldShowScrollbar()
2025-03-12 20:59:12 +08:00
{
if (!showScrollBar || !scroll || scrollbar == null || scroller == null || layoutManager == null || !SupportsOverflowCheck())
{
return false;
}
2025-12-26 14:22:46 +08:00
if (!HasValidScrollMetrics())
2025-05-30 10:32:08 +08:00
{
return false;
2025-05-30 10:32:08 +08:00
}
if (!ShouldLimitScrollToOverflow())
2025-03-12 20:59:12 +08:00
{
return true;
2025-03-12 20:59:12 +08:00
}
return HasScrollableContent();
2025-03-12 20:59:12 +08:00
}
/// <summary>
/// 根据列表方向设置滚动条方向。
/// </summary>
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
/// <summary>
/// 根据内容尺寸与视口尺寸更新滚动条手柄长度。
/// </summary>
2025-12-26 14:22:46 +08:00
private void ConfigureScrollbarSize()
{
float contentLength;
float viewLength;
float trackLength;
2025-12-26 14:22:46 +08:00
if (direction == Direction.Vertical)
2025-03-12 20:59:12 +08:00
{
contentLength = scroller.ContentSize.y;
viewLength = scroller.ViewSize.y;
trackLength = GetScrollbarTrackLength(true);
2025-12-26 14:22:46 +08:00
}
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);
}
/// <summary>
/// 获取滚动条轨道的像素长度。
/// </summary>
/// <param name="vertical">是否按垂直滚动条计算。</param>
/// <returns>滚动条轨道长度;无法获取时返回 <c>0</c>。</returns>
private float GetScrollbarTrackLength(bool vertical)
{
if (scrollbar == null)
{
return 0f;
}
RectTransform scrollbarRect = scrollbar.transform as RectTransform;
if (scrollbarRect == null)
{
return 0f;
2025-03-12 20:59:12 +08:00
}
Rect rect = scrollbarRect.rect;
return vertical ? rect.height : rect.width;
2025-03-12 20:59:12 +08:00
}
/// <summary>
/// 根据当前内容是否可滚动来更新滚动器启用状态。
/// </summary>
private void UpdateScrollerState()
{
if (scroller == null)
{
return;
}
scroller.enabled = scroll && (!ShouldLimitScrollToOverflow() || HasScrollableContent());
}
/// <summary>
/// 判断是否需要将滚动能力限制在内容溢出场景下才启用。
/// </summary>
/// <returns>需要仅在内容溢出时启用滚动返回 <see langword="true"/>;否则返回 <see langword="false"/>。</returns>
private bool ShouldLimitScrollToOverflow()
{
return showScrollBar && showScrollBarOnlyWhenScrollable && SupportsOverflowCheck();
}
/// <summary>
/// 判断当前内容尺寸是否超过视口尺寸。
/// </summary>
/// <returns>内容可滚动时返回 <see langword="true"/>;否则返回 <see langword="false"/>。</returns>
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;
}
/// <summary>
/// 判断当前方向是否支持溢出检测。
/// </summary>
/// <returns>支持垂直或水平溢出检测时返回 <see langword="true"/>;否则返回 <see langword="false"/>。</returns>
private bool SupportsOverflowCheck()
{
return direction == Direction.Vertical || direction == Direction.Horizontal;
}
/// <summary>
/// 判断当前布局是否已经具备有效的滚动尺寸信息。
/// </summary>
/// <returns>内容尺寸与视口尺寸均有效时返回 <see langword="true"/>;否则返回 <see langword="false"/>。</returns>
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;
}
/// <summary>
/// 将滚动位置吸附到最近的列表项。
/// </summary>
/// <returns>触发了新的吸附滚动时返回 <see langword="true"/>;否则返回 <see langword="false"/>。</returns>
private bool SnapToNearestItem()
2025-03-12 20:59:12 +08:00
{
2025-12-26 14:22:46 +08:00
int index = layoutManager.PositionToIndex(GetScrollPosition());
float targetPosition = layoutManager.IndexToPosition(index);
if (Mathf.Abs(targetPosition - GetScrollPosition()) <= 0.1f)
{
return false;
}
2025-12-26 14:22:46 +08:00
ScrollTo(index, true);
return true;
2025-03-12 20:59:12 +08:00
}
/// <summary>
/// 更新当前逻辑索引,并在变化时触发事件通知。
/// </summary>
/// <param name="index">新的候选索引。</param>
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
int itemCount = RecyclerViewAdapter.GetRealCount();
if (itemCount <= 0)
{
itemCount = RecyclerViewAdapter.GetItemCount();
}
if (itemCount <= 0)
{
if (currentIndex != -1)
{
currentIndex = -1;
2026-04-01 14:43:37 +08:00
OnFocusIndexChanged?.Invoke(currentIndex);
}
return;
}
2025-12-26 14:22:46 +08:00
index %= itemCount;
index = index < 0 ? itemCount + index : index;
2025-03-12 20:59:12 +08:00
if (currentIndex != index)
{
currentIndex = index;
2026-04-01 14:43:37 +08:00
OnFocusIndexChanged?.Invoke(currentIndex);
2025-03-12 20:59:12 +08:00
}
}
/// <summary>
/// 重新绑定当前可见区域内指定数据索引对应的所有持有者。
/// </summary>
/// <param name="dataIndex">要重绑的数据索引。</param>
/// <returns>实际完成重绑的持有者数量。</returns>
internal int RebindVisibleDataIndex(int dataIndex)
{
if (!EnsureMainThread(nameof(RebindVisibleDataIndex)) ||
RecyclerViewAdapter == null ||
!ViewProvider.TryGetViewHoldersByDataIndex(dataIndex, out IReadOnlyList<ViewHolder> 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;
}
/// <summary>
/// 重新绑定当前可见区域内指定数据区间对应的所有持有者。
/// </summary>
/// <param name="startDataIndex">起始数据索引。</param>
/// <param name="count">需要重绑的数据项数量。</param>
/// <returns>实际完成重绑的持有者总数。</returns>
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;
}
/// <summary>
/// 缓存一条等待滚动结束后执行的焦点请求。
/// </summary>
/// <param name="index">待聚焦的数据索引。</param>
/// <param name="alignment">目标对齐方式。</param>
private void QueueFocusRequest(int index, ScrollAlignment alignment)
{
hasPendingFocusRequest = true;
pendingFocusIndex = index;
pendingFocusAlignment = alignment;
}
/// <summary>
/// 清除当前缓存的焦点请求。
/// </summary>
private void ClearPendingFocusRequest()
{
hasPendingFocusRequest = false;
pendingFocusIndex = -1;
pendingFocusAlignment = ScrollAlignment.Center;
}
/// <summary>
/// 尝试执行当前缓存的焦点请求。
/// </summary>
private void TryProcessPendingFocusRequest()
{
if (!hasPendingFocusRequest)
{
return;
}
int index = pendingFocusIndex;
ScrollAlignment alignment = pendingFocusAlignment;
ClearPendingFocusRequest();
TryFocusIndex(index, false, alignment);
}
/// <summary>
/// 判断当前调用线程是否为 Unity 主线程。
/// </summary>
/// <returns>当前线程为主线程时返回 <see langword="true"/>;否则返回 <see langword="false"/>。</returns>
private static bool IsMainThread()
{
return mainThreadId < 0 || Thread.CurrentThread.ManagedThreadId == mainThreadId;
}
/// <summary>
/// 校验当前调用是否发生在 Unity 主线程上。
/// </summary>
/// <param name="caller">发起校验的调用方名称。</param>
/// <returns>位于主线程时返回 <see langword="true"/>;否则返回 <see langword="false"/>。</returns>
private bool EnsureMainThread(string caller)
{
if (IsMainThread())
{
return true;
}
Debug.LogError($"RecyclerView.{caller} must run on Unity main thread.");
return false;
}
2025-12-26 14:22:46 +08:00
#endregion
2025-03-12 20:59:12 +08:00
}
}