2026-03-31 15:18:50 +08:00
|
|
|
|
using System;
|
|
|
|
|
|
using System.Collections;
|
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using System.Threading;
|
2025-03-12 20:59:12 +08:00
|
|
|
|
using UnityEngine;
|
2026-03-31 15:18:50 +08:00
|
|
|
|
using UnityEngine.EventSystems;
|
2025-03-12 20:59:12 +08:00
|
|
|
|
using UnityEngine.UI;
|
|
|
|
|
|
|
2025-11-20 15:40:38 +08:00
|
|
|
|
namespace AlicizaX.UI
|
2025-03-12 20:59:12 +08:00
|
|
|
|
{
|
2026-03-11 14:18:07 +08:00
|
|
|
|
/// <summary>
|
2026-03-31 15:18:50 +08:00
|
|
|
|
/// RecyclerView 的核心组件,负责适配器绑定、布局刷新、滚动控制与焦点导航。
|
2026-03-11 14:18:07 +08:00
|
|
|
|
/// </summary>
|
2025-03-12 20:59:12 +08:00
|
|
|
|
public class RecyclerView : MonoBehaviour
|
|
|
|
|
|
{
|
2026-03-31 15:18:50 +08:00
|
|
|
|
/// <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
|
|
|
|
|
|
|
2026-03-31 15:18:50 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 列表的主滚动方向。
|
|
|
|
|
|
/// </summary>
|
2025-12-26 14:22:46 +08:00
|
|
|
|
[HideInInspector] [SerializeField] private Direction direction;
|
2026-03-31 15:18:50 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 列表项在交叉轴上的对齐方式。
|
|
|
|
|
|
/// </summary>
|
2025-12-26 14:22:46 +08:00
|
|
|
|
[HideInInspector] [SerializeField] private Alignment alignment;
|
2026-03-31 15:18:50 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 列表项之间的间距。
|
|
|
|
|
|
/// </summary>
|
2025-12-26 14:22:46 +08:00
|
|
|
|
[HideInInspector] [SerializeField] private Vector2 spacing;
|
2026-03-31 15:18:50 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 列表内容区域的内边距。
|
|
|
|
|
|
/// </summary>
|
2025-12-26 14:22:46 +08:00
|
|
|
|
[HideInInspector] [SerializeField] private Vector2 padding;
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region Serialized Fields - Scroll Settings
|
|
|
|
|
|
|
2026-03-31 15:18:50 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 是否启用滚动能力。
|
|
|
|
|
|
/// </summary>
|
2025-12-26 14:22:46 +08:00
|
|
|
|
[HideInInspector] [SerializeField] private bool scroll;
|
2026-03-31 15:18:50 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 是否在停止滚动后自动吸附到最近项。
|
|
|
|
|
|
/// </summary>
|
2025-12-26 14:22:46 +08:00
|
|
|
|
[HideInInspector] [SerializeField] private bool snap;
|
|
|
|
|
|
|
2026-03-31 15:18:50 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 平滑滚动时的速度系数。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[HideInInspector] [SerializeField, Range(0.5f, 50f)]
|
2025-12-26 14:22:46 +08:00
|
|
|
|
private float scrollSpeed = 7f;
|
|
|
|
|
|
|
2026-03-31 15:18:50 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 鼠标滚轮滚动时的速度系数。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[HideInInspector] [SerializeField, Range(1f, 50f)]
|
2025-12-26 14:22:46 +08:00
|
|
|
|
private float wheelSpeed = 30f;
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region Serialized Fields - Components
|
|
|
|
|
|
|
2026-03-31 15:18:50 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 可用于创建列表项的模板集合。
|
|
|
|
|
|
/// </summary>
|
2025-12-26 14:22:46 +08:00
|
|
|
|
[HideInInspector] [SerializeField] private ViewHolder[] templates;
|
2026-03-31 15:18:50 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 承载所有列表项的内容节点。
|
|
|
|
|
|
/// </summary>
|
2025-12-26 14:22:46 +08:00
|
|
|
|
[HideInInspector] [SerializeField] private RectTransform content;
|
2026-03-31 15:18:50 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 是否显示滚动条。
|
|
|
|
|
|
/// </summary>
|
2025-12-26 14:22:46 +08:00
|
|
|
|
[HideInInspector] [SerializeField] private bool showScrollBar;
|
2026-03-31 15:18:50 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 是否仅在内容可滚动时显示滚动条。
|
|
|
|
|
|
/// </summary>
|
2026-03-27 13:37:18 +08:00
|
|
|
|
[HideInInspector] [SerializeField] private bool showScrollBarOnlyWhenScrollable;
|
2026-03-31 15:18:50 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 与当前列表关联的滚动条组件。
|
|
|
|
|
|
/// </summary>
|
2025-12-26 14:22:46 +08:00
|
|
|
|
[HideInInspector] [SerializeField] private Scrollbar scrollbar;
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region Serialized Fields - Internal (Hidden in Inspector)
|
|
|
|
|
|
|
2026-03-31 15:18:50 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 序列化保存的布局管理器类型名称。
|
|
|
|
|
|
/// </summary>
|
2025-12-26 14:22:46 +08:00
|
|
|
|
[HideInInspector] [SerializeField] private string _layoutManagerTypeName;
|
2026-03-31 15:18:50 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 当前使用的布局管理器实例。
|
|
|
|
|
|
/// </summary>
|
2025-12-26 14:22:46 +08:00
|
|
|
|
[SerializeReference] private LayoutManager layoutManager;
|
2026-03-31 15:18:50 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 序列化保存的滚动器类型名称。
|
|
|
|
|
|
/// </summary>
|
2025-12-26 14:22:46 +08:00
|
|
|
|
[HideInInspector] [SerializeField] private string _scrollerTypeName;
|
2026-03-31 15:18:50 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 当前使用的滚动器实例。
|
|
|
|
|
|
/// </summary>
|
2025-12-26 14:22:46 +08:00
|
|
|
|
[HideInInspector] [SerializeReference] private Scroller scroller;
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region Private Fields
|
|
|
|
|
|
|
2026-03-31 15:18:50 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 负责创建、回收与查询视图持有者的提供器。
|
|
|
|
|
|
/// </summary>
|
2025-12-26 14:22:46 +08:00
|
|
|
|
private ViewProvider viewProvider;
|
2026-03-31 15:18:50 +08:00
|
|
|
|
|
|
|
|
|
|
/// <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;
|
2026-03-31 15:18:50 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 当前可见区间的结束布局索引。
|
|
|
|
|
|
/// </summary>
|
2025-12-26 14:22:46 +08:00
|
|
|
|
private int endIndex;
|
2026-03-31 15:18:50 +08:00
|
|
|
|
|
|
|
|
|
|
/// <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-31 15:18:50 +08:00
|
|
|
|
/// 获取或设置列表的主滚动方向。
|
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-31 15:18:50 +08:00
|
|
|
|
/// 获取或设置列表项在交叉轴上的对齐方式。
|
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-31 15:18:50 +08:00
|
|
|
|
/// 获取或设置列表项之间的间距。
|
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-31 15:18:50 +08:00
|
|
|
|
/// 获取或设置列表内容区域的内边距。
|
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-31 15:18:50 +08:00
|
|
|
|
/// 获取或设置是否启用滚动能力。
|
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-31 15:18:50 +08:00
|
|
|
|
/// 获取或设置是否在停止滚动后自动吸附到最近项。
|
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
|
|
|
|
|
2026-03-31 15:18:50 +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-31 15:18:50 +08:00
|
|
|
|
/// 获取或设置平滑滚动速度系数。
|
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-31 15:18:50 +08:00
|
|
|
|
/// 获取或设置鼠标滚轮滚动速度系数。
|
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
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-27 13:37:18 +08:00
|
|
|
|
/// <summary>
|
2026-03-31 15:18:50 +08:00
|
|
|
|
/// 获取或设置是否仅在内容可滚动时显示滚动条。
|
2026-03-27 13:37:18 +08:00
|
|
|
|
/// </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-31 15:18:50 +08:00
|
|
|
|
/// 获取或设置用于创建列表项的模板集合。
|
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-31 15:18:50 +08:00
|
|
|
|
/// 获取内容节点;未显式指定时会尝试从首个子节点推断。
|
2026-03-11 14:18:07 +08:00
|
|
|
|
/// </summary>
|
2025-12-26 14:22:46 +08:00
|
|
|
|
public RectTransform Content
|
|
|
|
|
|
{
|
|
|
|
|
|
get
|
|
|
|
|
|
{
|
|
|
|
|
|
if (content == null)
|
|
|
|
|
|
{
|
2026-03-31 15:18:50 +08:00
|
|
|
|
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>();
|
2026-03-31 15:18:50 +08:00
|
|
|
|
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-31 15:18:50 +08:00
|
|
|
|
/// 获取当前绑定的滚动条组件。
|
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-31 15:18:50 +08:00
|
|
|
|
/// 获取当前绑定的滚动器实例。
|
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-31 15:18:50 +08:00
|
|
|
|
/// 获取视图提供器;首次访问时根据模板数量自动创建。
|
2026-03-11 14:18:07 +08:00
|
|
|
|
/// </summary>
|
2025-12-26 14:22:46 +08:00
|
|
|
|
public ViewProvider ViewProvider
|
|
|
|
|
|
{
|
|
|
|
|
|
get
|
|
|
|
|
|
{
|
|
|
|
|
|
if (viewProvider == null)
|
|
|
|
|
|
{
|
2026-03-31 15:18:50 +08:00
|
|
|
|
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
|
|
|
|
|
2026-03-31 15:18:50 +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-31 15:18:50 +08:00
|
|
|
|
/// 获取或设置当前绑定的适配器实例。
|
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-31 15:18:50 +08:00
|
|
|
|
/// 当当前逻辑索引发生变化时触发。
|
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
|
|
|
|
|
|
|
2026-03-31 15:18:50 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 初始化模板、滚动器、滚动条与导航桥接组件。
|
|
|
|
|
|
/// </summary>
|
2025-12-26 14:22:46 +08:00
|
|
|
|
private void Awake()
|
2025-03-12 20:59:12 +08:00
|
|
|
|
{
|
2026-03-31 15:18:50 +08:00
|
|
|
|
if (mainThreadId < 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
mainThreadId = Thread.CurrentThread.ManagedThreadId;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
InitializeTemplates();
|
|
|
|
|
|
ConfigureScroller();
|
|
|
|
|
|
ConfigureScrollbar();
|
2026-03-31 15:18:50 +08:00
|
|
|
|
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
|
|
|
|
|
2026-03-31 15:18:50 +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
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 15:18:50 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 确保当前对象挂载用于导航事件桥接的组件。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private void EnsureNavigationBridge()
|
|
|
|
|
|
{
|
2026-04-13 15:45:34 +08:00
|
|
|
|
#if UX_NAVIGATION
|
2026-03-31 15:18:50 +08:00
|
|
|
|
if (GetComponent<RecyclerNavigationBridge>() == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
gameObject.AddComponent<RecyclerNavigationBridge>();
|
|
|
|
|
|
}
|
2026-04-13 15:45:34 +08:00
|
|
|
|
#endif
|
2026-03-31 15:18:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <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);
|
2026-03-27 13:37:18 +08:00
|
|
|
|
UpdateScrollerState();
|
2025-12-26 14:22:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 15:18:50 +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;
|
2026-03-27 13:37:18 +08:00
|
|
|
|
UpdateScrollbarVisibility();
|
2025-12-26 14:22:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region Public Methods - Setup
|
|
|
|
|
|
|
2026-03-11 14:18:07 +08:00
|
|
|
|
/// <summary>
|
2026-03-31 15:18:50 +08:00
|
|
|
|
/// 绑定新的适配器,并重建 RecyclerView 与布局管理器之间的关联关系。
|
2026-03-11 14:18:07 +08:00
|
|
|
|
/// </summary>
|
2026-03-31 15:18:50 +08:00
|
|
|
|
/// <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
|
|
|
|
{
|
2026-03-31 15:18:50 +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
|
|
|
|
|
2026-03-31 15:18:50 +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;
|
2026-03-31 15:18:50 +08:00
|
|
|
|
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)
|
2026-03-31 15:18:50 +08:00
|
|
|
|
{
|
|
|
|
|
|
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>
|
2026-03-31 15:18:50 +08:00
|
|
|
|
/// 尝试将焦点移动到指定索引对应的列表项。
|
|
|
|
|
|
/// </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)
|
2026-03-31 15:18:50 +08:00
|
|
|
|
{
|
|
|
|
|
|
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);
|
2026-03-31 15:18:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <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-03-31 15:18:50 +08:00
|
|
|
|
{
|
2026-04-03 15:21:13 +08:00
|
|
|
|
return proxy.TryGetFocusTarget(out target);
|
2026-03-31 15:18:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
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;
|
2026-03-31 15:18:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <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-03-31 15:18:50 +08:00
|
|
|
|
{
|
2026-04-03 15:21:13 +08:00
|
|
|
|
ScheduleFocusRecovery(target);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (eventSystem.alreadySelecting)
|
|
|
|
|
|
{
|
|
|
|
|
|
ScheduleFocusRecovery(target);
|
|
|
|
|
|
return;
|
2026-03-31 15:18:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 15:21:13 +08:00
|
|
|
|
eventSystem.SetSelectedGameObject(target);
|
|
|
|
|
|
ScheduleFocusRecovery(target);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void ScheduleFocusRecovery(GameObject target)
|
|
|
|
|
|
{
|
2026-03-31 15:18:50 +08:00
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 15:18:50 +08:00
|
|
|
|
/// <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
|
|
|
|
{
|
2026-03-31 15:18:50 +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
|
|
|
|
}
|
2026-03-31 15:18:50 +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-31 15:18:50 +08:00
|
|
|
|
/// 按当前滚动位置重新创建可见范围内的所有视图持有者。
|
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
|
|
|
|
{
|
2026-03-31 15:18:50 +08:00
|
|
|
|
if (!EnsureMainThread(nameof(Refresh)))
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
ViewProvider.Clear();
|
2026-03-31 15:18:50 +08:00
|
|
|
|
if (layoutManager == null || RecyclerViewAdapter == null || RecyclerViewAdapter.GetItemCount() <= 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
startIndex = 0;
|
|
|
|
|
|
endIndex = -1;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-12-26 14:22:46 +08:00
|
|
|
|
|
2026-03-31 15:18:50 +08:00
|
|
|
|
startIndex = Mathf.Max(0, layoutManager.GetStartIndex());
|
2025-12-26 14:22:46 +08:00
|
|
|
|
endIndex = layoutManager.GetEndIndex();
|
2026-03-31 15:18:50 +08:00
|
|
|
|
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-31 15:18:50 +08:00
|
|
|
|
/// 重新计算内容尺寸、滚动能力与对象池预热状态。
|
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
|
|
|
|
{
|
2026-03-31 15:18:50 +08:00
|
|
|
|
if (!EnsureMainThread(nameof(RequestLayout)))
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-27 13:37:18 +08:00
|
|
|
|
if (layoutManager == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
UpdateScrollbarVisibility();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
layoutManager.SetContentSize();
|
|
|
|
|
|
|
2026-03-27 13:37:18 +08:00
|
|
|
|
if (scroller == null)
|
|
|
|
|
|
{
|
2026-03-31 15:18:50 +08:00
|
|
|
|
viewProvider?.PreparePool();
|
2026-03-27 13:37:18 +08:00
|
|
|
|
UpdateScrollbarVisibility();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-12-26 14:22:46 +08:00
|
|
|
|
|
|
|
|
|
|
scroller.Direction = direction;
|
|
|
|
|
|
scroller.ViewSize = layoutManager.ViewportSize;
|
|
|
|
|
|
scroller.ContentSize = layoutManager.ContentSize;
|
2026-03-27 13:37:18 +08:00
|
|
|
|
scroller.Position = Mathf.Clamp(scroller.Position, 0, scroller.MaxPosition);
|
2026-03-31 15:18:50 +08:00
|
|
|
|
viewProvider?.PreparePool();
|
2025-12-26 14:22:46 +08:00
|
|
|
|
|
2026-03-27 13:37:18 +08:00
|
|
|
|
UpdateScrollerState();
|
2025-12-26 14:22:46 +08:00
|
|
|
|
UpdateScrollbarVisibility();
|
2026-03-27 13:37:18 +08:00
|
|
|
|
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-31 15:18:50 +08:00
|
|
|
|
/// 获取当前滚动位置。
|
2026-03-11 14:18:07 +08:00
|
|
|
|
/// </summary>
|
2026-03-31 15:18:50 +08:00
|
|
|
|
/// <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-31 15:18:50 +08:00
|
|
|
|
/// 将列表滚动到指定索引对应的位置。
|
2026-03-11 14:18:07 +08:00
|
|
|
|
/// </summary>
|
2026-03-31 15:18:50 +08:00
|
|
|
|
/// <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-31 15:18:50 +08:00
|
|
|
|
/// 将列表滚动到指定索引,并按给定对齐方式定位。
|
2026-03-11 14:18:07 +08:00
|
|
|
|
/// </summary>
|
2026-03-31 15:18:50 +08:00
|
|
|
|
/// <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)
|
|
|
|
|
|
{
|
2026-03-31 15:18:50 +08:00
|
|
|
|
scroller.ScrollToDuration(targetPosition, duration);
|
2025-12-26 15:39:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
scroller.ScrollTo(targetPosition, smooth);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!smooth)
|
|
|
|
|
|
{
|
|
|
|
|
|
Refresh();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
UpdateCurrentIndex(index);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 15:18:50 +08:00
|
|
|
|
/// <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;
|
|
|
|
|
|
|
2026-03-31 15:18:50 +08:00
|
|
|
|
// 计算目标项的原始位置,不在此阶段做范围限制。
|
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
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-31 15:18:50 +08:00
|
|
|
|
// 叠加调用方传入的额外偏移量。
|
2025-12-26 15:39:31 +08:00
|
|
|
|
targetPosition += offset;
|
|
|
|
|
|
|
2026-03-31 15:18:50 +08:00
|
|
|
|
// 将结果限制在可滚动范围内。
|
2025-12-26 15:39:31 +08:00
|
|
|
|
return Mathf.Clamp(targetPosition, 0, scroller.MaxPosition);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 15:18:50 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 计算指定索引对应项在内容区域中的原始起始位置。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="index">目标数据索引。</param>
|
|
|
|
|
|
/// <returns>未做边界限制的原始滚动位置。</returns>
|
2025-12-26 15:39:31 +08:00
|
|
|
|
private float CalculateRawItemPosition(int index)
|
|
|
|
|
|
{
|
2026-03-31 15:18:50 +08:00
|
|
|
|
// 根据滚动方向选择对应轴向的间距与内边距。
|
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;
|
|
|
|
|
|
|
2026-03-31 15:18:50 +08:00
|
|
|
|
// 直接基于索引、尺寸与间距推导原始位置。
|
2025-12-26 15:39:31 +08:00
|
|
|
|
return index * (itemSize + spacingValue) + paddingValue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 15:18:50 +08:00
|
|
|
|
/// <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
|
|
|
|
|
|
|
2026-03-31 15:18:50 +08:00
|
|
|
|
/// <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
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 15:18:50 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 响应滚动器停止移动事件,处理吸附与挂起焦点请求。
|
|
|
|
|
|
/// </summary>
|
2025-03-12 20:59:12 +08:00
|
|
|
|
private void OnMoveStoped()
|
|
|
|
|
|
{
|
2026-03-31 15:18:50 +08:00
|
|
|
|
if (snap && SnapToNearestItem())
|
2025-03-12 20:59:12 +08:00
|
|
|
|
{
|
2026-03-31 15:18:50 +08:00
|
|
|
|
return;
|
2025-03-12 20:59:12 +08:00
|
|
|
|
}
|
2026-03-31 15:18:50 +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
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 15:18:50 +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
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 15:18:50 +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
|
|
|
|
|
|
|
2026-03-31 15:18:50 +08:00
|
|
|
|
/// <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
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 15:18:50 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 根据当前滚动位置增量更新可见区间内的视图持有者。
|
|
|
|
|
|
/// </summary>
|
2025-12-26 14:22:46 +08:00
|
|
|
|
private void UpdateVisibleRange()
|
2025-04-01 15:21:02 +08:00
|
|
|
|
{
|
2026-03-31 15:18:50 +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)
|
|
|
|
|
|
{
|
2026-03-31 15:18:50 +08:00
|
|
|
|
// TODO: 在滚动到列表起始端时补充刷新逻辑。
|
2025-12-26 14:22:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
startIndex -= layoutManager.Unit;
|
|
|
|
|
|
viewProvider.CreateViewHolder(startIndex);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-04-01 15:21:02 +08:00
|
|
|
|
|
2026-03-31 15:18:50 +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)
|
|
|
|
|
|
{
|
2026-03-31 15:18:50 +08:00
|
|
|
|
// 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
|
|
|
|
|
2026-03-31 15:18:50 +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
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 15:18:50 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 根据当前状态更新滚动条的显示与交互能力。
|
|
|
|
|
|
/// </summary>
|
2025-12-26 14:22:46 +08:00
|
|
|
|
private void UpdateScrollbarVisibility()
|
2025-03-12 20:59:12 +08:00
|
|
|
|
{
|
2026-03-27 13:37:18 +08:00
|
|
|
|
if (scrollbar == null)
|
|
|
|
|
|
{
|
2025-12-26 14:22:46 +08:00
|
|
|
|
return;
|
2026-03-27 13:37:18 +08:00
|
|
|
|
}
|
2025-03-12 20:59:12 +08:00
|
|
|
|
|
2025-12-26 14:22:46 +08:00
|
|
|
|
bool shouldShow = ShouldShowScrollbar();
|
|
|
|
|
|
scrollbar.gameObject.SetActive(shouldShow);
|
2026-03-27 13:37:18 +08:00
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 15:18:50 +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
|
|
|
|
{
|
2026-03-27 13:37:18 +08:00
|
|
|
|
if (!showScrollBar || !scroll || scrollbar == null || scroller == null || layoutManager == null || !SupportsOverflowCheck())
|
|
|
|
|
|
{
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2025-12-26 14:22:46 +08:00
|
|
|
|
|
2026-03-27 13:37:18 +08:00
|
|
|
|
if (!HasValidScrollMetrics())
|
2025-05-30 10:32:08 +08:00
|
|
|
|
{
|
2026-03-27 13:37:18 +08:00
|
|
|
|
return false;
|
2025-05-30 10:32:08 +08:00
|
|
|
|
}
|
2026-03-27 13:37:18 +08:00
|
|
|
|
|
|
|
|
|
|
if (!ShouldLimitScrollToOverflow())
|
2025-03-12 20:59:12 +08:00
|
|
|
|
{
|
2026-03-27 13:37:18 +08:00
|
|
|
|
return true;
|
2025-03-12 20:59:12 +08:00
|
|
|
|
}
|
2026-03-27 13:37:18 +08:00
|
|
|
|
|
|
|
|
|
|
return HasScrollableContent();
|
2025-03-12 20:59:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 15:18:50 +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
|
|
|
|
|
2026-03-31 15:18:50 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 根据内容尺寸与视口尺寸更新滚动条手柄长度。
|
|
|
|
|
|
/// </summary>
|
2025-12-26 14:22:46 +08:00
|
|
|
|
private void ConfigureScrollbarSize()
|
|
|
|
|
|
{
|
2026-03-31 15:18:50 +08:00
|
|
|
|
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
|
|
|
|
{
|
2026-03-31 15:18:50 +08:00
|
|
|
|
contentLength = scroller.ContentSize.y;
|
|
|
|
|
|
viewLength = scroller.ViewSize.y;
|
|
|
|
|
|
trackLength = GetScrollbarTrackLength(true);
|
2025-12-26 14:22:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2026-03-31 15:18:50 +08:00
|
|
|
|
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
|
|
|
|
}
|
2026-03-31 15:18:50 +08:00
|
|
|
|
|
|
|
|
|
|
Rect rect = scrollbarRect.rect;
|
|
|
|
|
|
return vertical ? rect.height : rect.width;
|
2025-03-12 20:59:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 15:18:50 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 根据当前内容是否可滚动来更新滚动器启用状态。
|
|
|
|
|
|
/// </summary>
|
2026-03-27 13:37:18 +08:00
|
|
|
|
private void UpdateScrollerState()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (scroller == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
scroller.enabled = scroll && (!ShouldLimitScrollToOverflow() || HasScrollableContent());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 15:18:50 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 判断是否需要将滚动能力限制在内容溢出场景下才启用。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <returns>需要仅在内容溢出时启用滚动返回 <see langword="true"/>;否则返回 <see langword="false"/>。</returns>
|
2026-03-27 13:37:18 +08:00
|
|
|
|
private bool ShouldLimitScrollToOverflow()
|
|
|
|
|
|
{
|
|
|
|
|
|
return showScrollBar && showScrollBarOnlyWhenScrollable && SupportsOverflowCheck();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 15:18:50 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 判断当前内容尺寸是否超过视口尺寸。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <returns>内容可滚动时返回 <see langword="true"/>;否则返回 <see langword="false"/>。</returns>
|
2026-03-27 13:37:18 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 15:18:50 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 判断当前方向是否支持溢出检测。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <returns>支持垂直或水平溢出检测时返回 <see langword="true"/>;否则返回 <see langword="false"/>。</returns>
|
2026-03-27 13:37:18 +08:00
|
|
|
|
private bool SupportsOverflowCheck()
|
|
|
|
|
|
{
|
|
|
|
|
|
return direction == Direction.Vertical || direction == Direction.Horizontal;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 15:18:50 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 判断当前布局是否已经具备有效的滚动尺寸信息。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <returns>内容尺寸与视口尺寸均有效时返回 <see langword="true"/>;否则返回 <see langword="false"/>。</returns>
|
2026-03-27 13:37:18 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 15:18:50 +08:00
|
|
|
|
/// <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());
|
2026-03-31 15:18:50 +08:00
|
|
|
|
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);
|
2026-03-31 15:18:50 +08:00
|
|
|
|
return true;
|
2025-03-12 20:59:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 15:18:50 +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
|
|
|
|
|
2026-03-31 15:18:50 +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);
|
2026-03-31 15:18:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 15:18:50 +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
|
|
|
|
}
|
|
|
|
|
|
}
|