using System;
using UnityEngine;
using UnityEngine.UI;
namespace AlicizaX.UI
{
///
/// RecyclerView 核心组件,用于高效显示大量列表数据
/// 通过视图回收和复用机制,只渲染可见区域的项目,大幅提升性能
/// 支持垂直/水平滚动、网格布局、循环滚动等多种布局模式
///
public class RecyclerView : MonoBehaviour
{
#region Serialized Fields - Layout Settings
[HideInInspector] [SerializeField] private Direction direction;
[HideInInspector] [SerializeField] private Alignment alignment;
[HideInInspector] [SerializeField] private Vector2 spacing;
[HideInInspector] [SerializeField] private Vector2 padding;
#endregion
#region Serialized Fields - Scroll Settings
[HideInInspector] [SerializeField] private bool scroll;
[HideInInspector] [SerializeField] private bool snap;
[HideInInspector] [SerializeField, Range(1f, 50f)]
private float scrollSpeed = 7f;
[HideInInspector] [SerializeField, Range(10f, 50f)]
private float wheelSpeed = 30f;
#endregion
#region Serialized Fields - Components
[HideInInspector] [SerializeField] private ViewHolder[] templates;
[HideInInspector] [SerializeField] private RectTransform content;
[HideInInspector] [SerializeField] private bool showScrollBar;
[HideInInspector] [SerializeField] private bool showScrollBarOnlyWhenScrollable;
[HideInInspector] [SerializeField] private Scrollbar scrollbar;
#endregion
#region Serialized Fields - Internal (Hidden in Inspector)
[HideInInspector] [SerializeField] private string _layoutManagerTypeName;
[SerializeReference] private LayoutManager layoutManager;
[HideInInspector] [SerializeField] private string _scrollerTypeName;
[HideInInspector] [SerializeReference] private Scroller scroller;
#endregion
#region Private Fields
private ViewProvider viewProvider;
private int startIndex;
private int endIndex;
private int currentIndex;
#endregion
#region Public Properties - Layout Settings
///
/// 获取或设置列表的滚动方向(垂直、水平或自定义)
///
public Direction Direction
{
get => direction;
set => direction = value;
}
///
/// 获取或设置列表项的对齐方式(起始、居中或结束)
///
public Alignment Alignment
{
get => alignment;
set => alignment = value;
}
///
/// 获取或设置列表项之间的间距(X轴和Y轴)
///
public Vector2 Spacing
{
get => spacing;
set => spacing = value;
}
///
/// 获取或设置列表内容的内边距(X轴和Y轴)
///
public Vector2 Padding
{
get => padding;
set => padding = value;
}
#endregion
#region Public Properties - Scroll Settings
///
/// 获取或设置是否启用滚动功能
/// 启用时会激活 Scroller 组件并显示滚动条(如果配置了)
///
public bool Scroll
{
get => scroll;
set
{
if (scroll == value) return;
scroll = value;
// 启/停 scroller(如果存在)
if (scroller != null)
{
// 如果 Scroller 是 MonoBehaviour,可以启/停组件;否则回退到设置一个 Position/flags
scroller.enabled = scroll;
// 将当前的滚动相关配置下发到 scroller,保持一致性
scroller.ScrollSpeed = scrollSpeed;
scroller.WheelSpeed = wheelSpeed;
scroller.Snap = snap;
}
// 更新 scrollbar 显示(只有在 showScrollBar 为 true 时才显示)
if (scrollbar != null)
{
scrollbar.gameObject.SetActive(showScrollBar && scroll);
}
// 如果启用/禁用滚动后需要调整布局或滚动条大小,刷新布局
RequestLayout();
}
}
///
/// 获取或设置是否启用吸附功能
/// 启用时滚动停止后会自动对齐到最近的列表项
/// 注意:此功能依赖于 Scroll 属性,只有在滚动启用时才生效
///
public bool Snap
{
get => snap;
set
{
// Snap 依赖于 Scroll(与原逻辑保持一致)
bool newSnap = value & scroll;
if (snap == newSnap) return;
snap = newSnap;
if (scroller != null)
{
scroller.Snap = snap;
}
// 如果开启了 snap,可以选做:立即对齐到最近项
// if (snap && scroller != null) SnapToNearestItem();
}
}
///
/// 获取或设置滚动速度(范围:1-50)
/// 值越大,滚动响应越快
///
public float ScrollSpeed
{
get => scrollSpeed;
set
{
if (Mathf.Approximately(scrollSpeed, value)) return;
scrollSpeed = value;
if (scroller != null)
{
scroller.ScrollSpeed = scrollSpeed;
}
}
}
///
/// 获取或设置鼠标滚轮的滚动速度(范围:10-50)
/// 值越大,滚轮滚动的距离越大
///
public float WheelSpeed
{
get => wheelSpeed;
set
{
if (Mathf.Approximately(wheelSpeed, value)) return;
wheelSpeed = value;
if (scroller != null)
{
scroller.WheelSpeed = wheelSpeed;
}
}
}
///
/// 是否仅在内容超出可视区域时显示滚动条并允许滚动
///
public bool ShowScrollBarOnlyWhenScrollable
{
get => showScrollBarOnlyWhenScrollable;
set
{
if (showScrollBarOnlyWhenScrollable == value) return;
showScrollBarOnlyWhenScrollable = value;
RequestLayout();
}
}
#endregion
#region Public Properties - Components
///
/// 获取或设置 ViewHolder 模板数组
/// 用于创建和复用列表项视图
///
public ViewHolder[] Templates
{
get => templates;
set => templates = value;
}
///
/// 获取内容容器的 RectTransform
/// 所有列表项都会作为此容器的子对象
///
public RectTransform Content
{
get
{
if (content == null)
{
content = transform.GetChild(0).GetComponent();
}
return content;
}
}
///
/// 获取滚动条组件
///
public Scrollbar Scrollbar => scrollbar;
///
/// 获取滚动控制器组件
///
public Scroller Scroller => scroller;
///
/// 获取视图提供器
/// 负责创建、回收和管理 ViewHolder 实例
/// 根据模板数量自动选择 SimpleViewProvider 或 MixedViewProvider
///
public ViewProvider ViewProvider
{
get
{
if (viewProvider == null)
{
viewProvider = templates.Length > 1
? new MixedViewProvider(this, templates)
: new SimpleViewProvider(this, templates);
}
return viewProvider;
}
}
#endregion
#region Public Properties - State
///
/// 获取或设置当前绑定的适配器
/// 适配器负责提供数据和创建 ViewHolder
///
public IAdapter RecyclerViewAdapter { get; set; }
///
/// 获取或设置当前显示的列表项索引
///
public int CurrentIndex
{
get => currentIndex;
set => currentIndex = value;
}
#endregion
#region Events
///
/// 当前索引改变时触发的事件
/// 参数为新的索引值
///
public Action OnIndexChanged;
///
/// 滚动位置改变时触发的事件
///
public Action OnScrollValueChanged;
#endregion
#region Unity Lifecycle
private void Awake()
{
InitializeTemplates();
ConfigureScroller();
ConfigureScrollbar();
}
#endregion
#region Initialization
private void InitializeTemplates()
{
if (templates == null) return;
for (int i = 0; i < templates.Length; i++)
{
if (templates[i] != null)
{
templates[i].gameObject.SetActive(false);
}
}
}
private void ConfigureScroller()
{
if (scroller == null) return;
scroller.ScrollSpeed = scrollSpeed;
scroller.WheelSpeed = wheelSpeed;
scroller.Snap = snap;
scroller.OnValueChanged.AddListener(OnScrollChanged);
scroller.OnMoveStoped.AddListener(OnMoveStoped);
UpdateScrollerState();
}
private void ConfigureScrollbar()
{
if (!showScrollBar || scrollbar == null) return;
scrollbar.onValueChanged.AddListener(OnScrollbarChanged);
var scrollbarEx = scrollbar.gameObject.GetComponent();
if (scrollbarEx == null)
{
scrollbarEx = scrollbar.gameObject.AddComponent();
}
scrollbarEx.OnDragEnd = OnScrollbarDragEnd;
UpdateScrollbarVisibility();
}
#endregion
#region Public Methods - Setup
///
/// 设置数据适配器并初始化布局管理器
///
/// 要绑定的适配器实例
public void SetAdapter(IAdapter adapter)
{
if (adapter == null)
{
Debug.LogError("Adapter cannot be null");
return;
}
RecyclerViewAdapter = adapter;
ViewProvider.Adapter = adapter;
ViewProvider.LayoutManager = layoutManager;
layoutManager.RecyclerView = this;
layoutManager.Adapter = adapter;
layoutManager.ViewProvider = viewProvider;
layoutManager.Direction = direction;
layoutManager.Alignment = alignment;
layoutManager.Spacing = spacing;
layoutManager.Padding = padding;
}
///
/// 重置列表状态
/// 清空所有 ViewHolder 并将滚动位置重置为起始位置
///
public void Reset()
{
viewProvider?.Reset();
if (scroller != null)
{
scroller.Position = 0;
}
if (scrollbar != null)
{
scrollbar.SetValueWithoutNotify(0);
}
}
#endregion
#region Public Methods - Layout
///
/// 刷新列表显示
/// 清空当前所有 ViewHolder 并根据当前滚动位置重新创建可见项
///
public void Refresh()
{
ViewProvider.Clear();
startIndex = layoutManager.GetStartIndex();
endIndex = layoutManager.GetEndIndex();
for (int i = startIndex; i <= endIndex; i += layoutManager.Unit)
{
ViewProvider.CreateViewHolder(i);
}
layoutManager.DoItemAnimation();
}
///
/// 请求重新布局
/// 重新计算内容大小、视口大小,并更新滚动条显示状态
///
public void RequestLayout()
{
if (layoutManager == null)
{
UpdateScrollbarVisibility();
return;
}
layoutManager.SetContentSize();
if (scroller == null)
{
UpdateScrollbarVisibility();
return;
}
scroller.Direction = direction;
scroller.ViewSize = layoutManager.ViewportSize;
scroller.ContentSize = layoutManager.ContentSize;
scroller.Position = Mathf.Clamp(scroller.Position, 0, scroller.MaxPosition);
UpdateScrollerState();
UpdateScrollbarVisibility();
UpdateScrollbarValue(scroller.Position);
}
#endregion
#region Public Methods - Scrolling
///
/// 获取当前的滚动位置
///
/// 当前滚动位置值,如果没有 Scroller 则返回 0
public float GetScrollPosition()
{
return scroller != null ? scroller.Position : 0;
}
///
/// 滚动到指定索引的列表项
///
/// 目标列表项的索引
/// 是否使用平滑滚动动画
public void ScrollTo(int index, bool smooth = false)
{
if (!scroll || scroller == null) return;
scroller.ScrollTo(layoutManager.IndexToPosition(index), smooth);
if (!smooth)
{
Refresh();
}
UpdateCurrentIndex(index);
}
///
/// 滚动到指定索引的列表项,并使用指定的对齐方式
///
/// 目标列表项的索引
/// 对齐方式(起始、居中或结束)
/// 额外的偏移量
/// 是否使用平滑滚动动画
/// 滚动动画持续时间(秒)
public void ScrollToWithAlignment(int index, ScrollAlignment alignment, float offset = 0f, bool smooth = false, float duration = 0.3f)
{
if (!scroll || scroller == null) return;
float targetPosition = CalculateScrollPositionWithAlignment(index, alignment, offset);
if (UGListExtensions.DebugScrollTo)
{
Debug.Log($"[RecyclerView] ScrollToWithAlignment: index={index}, alignment={alignment}, offset={offset}, targetPosition={targetPosition}, maxPosition={scroller.MaxPosition}");
}
if (duration > 0 && smooth)
{
scroller.ScrollTo(targetPosition, true);
}
else
{
scroller.ScrollTo(targetPosition, smooth);
}
if (!smooth)
{
Refresh();
}
UpdateCurrentIndex(index);
}
private float CalculateScrollPositionWithAlignment(int index, ScrollAlignment alignment, float offset)
{
if (RecyclerViewAdapter == null || index < 0 || index >= RecyclerViewAdapter.GetItemCount())
{
return scroller.Position;
}
float itemSize = GetItemSize(index);
float viewportLength = direction == Direction.Vertical ? layoutManager.ViewportSize.y : layoutManager.ViewportSize.x;
float contentLength = direction == Direction.Vertical ? layoutManager.ContentSize.y : layoutManager.ContentSize.x;
// Calculate the raw position of the item (without any clamping)
float itemPosition = CalculateRawItemPosition(index);
float targetPosition = alignment switch
{
ScrollAlignment.Start => itemPosition,
ScrollAlignment.Center => itemPosition - (viewportLength - itemSize) / 2f,
ScrollAlignment.End => itemPosition - viewportLength + itemSize,
_ => itemPosition
};
// Apply custom offset
targetPosition += offset;
if (UGListExtensions.DebugScrollTo)
{
Debug.Log($"[RecyclerView] CalculateScrollPosition: index={index}, itemPosition={itemPosition}, itemSize={itemSize}, viewportLength={viewportLength}, contentLength={contentLength}, targetPosition={targetPosition}, maxPosition={scroller.MaxPosition}");
}
// Clamp to valid scroll range
return Mathf.Clamp(targetPosition, 0, scroller.MaxPosition);
}
private float CalculateRawItemPosition(int index)
{
// Get spacing based on direction
Vector2 spacing = layoutManager.Spacing;
Vector2 padding = layoutManager.Padding;
float itemSize = GetItemSize(index);
float spacingValue = direction == Direction.Vertical ? spacing.y : spacing.x;
float paddingValue = direction == Direction.Vertical ? padding.y : padding.x;
// Calculate raw position without clamping
return index * (itemSize + spacingValue) + paddingValue;
}
private float GetItemSize(int index)
{
Vector2 itemSize = ViewProvider.CalculateViewSize(index);
return direction == Direction.Vertical ? itemSize.y : itemSize.x;
}
#endregion
#region Private Methods - Scroll Callbacks
private void OnScrollChanged(float position)
{
layoutManager.UpdateLayout();
UpdateScrollbarValue(position);
UpdateVisibleRange();
layoutManager.DoItemAnimation();
OnScrollValueChanged?.Invoke();
}
private void OnMoveStoped()
{
if (snap)
{
SnapToNearestItem();
}
}
private void OnScrollbarChanged(float ratio)
{
if (scroller != null)
{
scroller.ScrollToRatio(ratio);
}
}
private void OnScrollbarDragEnd()
{
if (scroller == null) return;
if (scroller.Position < scroller.MaxPosition && snap)
{
SnapToNearestItem();
}
}
#endregion
#region Private Methods - Scroll Helpers
private void UpdateScrollbarValue(float position)
{
if (scrollbar != null && scroller != null)
{
float ratio = scroller.MaxPosition > 0 ? position / scroller.MaxPosition : 0;
scrollbar.SetValueWithoutNotify(ratio);
}
}
private void UpdateVisibleRange()
{
// Handle start index
if (layoutManager.IsFullInvisibleStart(startIndex))
{
viewProvider.RemoveViewHolder(startIndex);
startIndex += layoutManager.Unit;
}
else if (layoutManager.IsFullVisibleStart(startIndex))
{
if (startIndex == 0)
{
// TODO: Implement refresh logic
}
else
{
startIndex -= layoutManager.Unit;
viewProvider.CreateViewHolder(startIndex);
}
}
// Handle end index
if (layoutManager.IsFullInvisibleEnd(endIndex))
{
viewProvider.RemoveViewHolder(endIndex);
endIndex -= layoutManager.Unit;
}
else if (layoutManager.IsFullVisibleEnd(endIndex))
{
if (endIndex >= viewProvider.GetItemCount() - layoutManager.Unit)
{
// TODO: Implement load more logic
}
else
{
endIndex += layoutManager.Unit;
viewProvider.CreateViewHolder(endIndex);
}
}
// Refresh if out of visible range
if (!layoutManager.IsVisible(startIndex) || !layoutManager.IsVisible(endIndex))
{
Refresh();
}
}
private void UpdateScrollbarVisibility()
{
if (scrollbar == null)
{
return;
}
bool shouldShow = ShouldShowScrollbar();
scrollbar.gameObject.SetActive(shouldShow);
scrollbar.interactable = shouldShow;
if (shouldShow)
{
ConfigureScrollbarDirection();
ConfigureScrollbarSize();
}
}
private bool ShouldShowScrollbar()
{
if (!showScrollBar || !scroll || scrollbar == null || scroller == null || layoutManager == null || !SupportsOverflowCheck())
{
return false;
}
if (!HasValidScrollMetrics())
{
return false;
}
if (!ShouldLimitScrollToOverflow())
{
return true;
}
return HasScrollableContent();
}
private void ConfigureScrollbarDirection()
{
scrollbar.direction = direction == Direction.Vertical
? Scrollbar.Direction.TopToBottom
: Scrollbar.Direction.LeftToRight;
}
private void ConfigureScrollbarSize()
{
if (direction == Direction.Vertical)
{
scrollbar.size = scroller.ViewSize.y / scroller.ContentSize.y;
}
else
{
scrollbar.size = scroller.ViewSize.x / scroller.ContentSize.x;
}
}
private void UpdateScrollerState()
{
if (scroller == null)
{
return;
}
scroller.enabled = scroll && (!ShouldLimitScrollToOverflow() || HasScrollableContent());
}
private bool ShouldLimitScrollToOverflow()
{
return showScrollBar && showScrollBarOnlyWhenScrollable && SupportsOverflowCheck();
}
private bool HasScrollableContent()
{
if (layoutManager == null)
{
return false;
}
if (direction == Direction.Vertical)
{
return layoutManager.ContentSize.y > layoutManager.ViewportSize.y;
}
if (direction == Direction.Horizontal)
{
return layoutManager.ContentSize.x > layoutManager.ViewportSize.x;
}
return false;
}
private bool SupportsOverflowCheck()
{
return direction == Direction.Vertical || direction == Direction.Horizontal;
}
private bool HasValidScrollMetrics()
{
if (direction == Direction.Vertical)
{
return layoutManager.ContentSize.y > 0f && layoutManager.ViewportSize.y > 0f;
}
if (direction == Direction.Horizontal)
{
return layoutManager.ContentSize.x > 0f && layoutManager.ViewportSize.x > 0f;
}
return false;
}
private void SnapToNearestItem()
{
int index = layoutManager.PositionToIndex(GetScrollPosition());
ScrollTo(index, true);
}
private void UpdateCurrentIndex(int index)
{
if (RecyclerViewAdapter == null) return;
int itemCount = RecyclerViewAdapter.GetItemCount();
index %= itemCount;
index = index < 0 ? itemCount + index : index;
if (currentIndex != index)
{
currentIndex = index;
OnIndexChanged?.Invoke(currentIndex);
}
}
#endregion
}
}