827 lines
25 KiB
C#
827 lines
25 KiB
C#
using System;
|
||
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
|
||
namespace AlicizaX.UI
|
||
{
|
||
/// <summary>
|
||
/// RecyclerView 核心组件,用于高效显示大量列表数据
|
||
/// 通过视图回收和复用机制,只渲染可见区域的项目,大幅提升性能
|
||
/// 支持垂直/水平滚动、网格布局、循环滚动等多种布局模式
|
||
/// </summary>
|
||
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
|
||
|
||
/// <summary>
|
||
/// 获取或设置列表的滚动方向(垂直、水平或自定义)
|
||
/// </summary>
|
||
public Direction Direction
|
||
{
|
||
get => direction;
|
||
set => direction = value;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取或设置列表项的对齐方式(起始、居中或结束)
|
||
/// </summary>
|
||
public Alignment Alignment
|
||
{
|
||
get => alignment;
|
||
set => alignment = value;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取或设置列表项之间的间距(X轴和Y轴)
|
||
/// </summary>
|
||
public Vector2 Spacing
|
||
{
|
||
get => spacing;
|
||
set => spacing = value;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取或设置列表内容的内边距(X轴和Y轴)
|
||
/// </summary>
|
||
public Vector2 Padding
|
||
{
|
||
get => padding;
|
||
set => padding = value;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Public Properties - Scroll Settings
|
||
|
||
/// <summary>
|
||
/// 获取或设置是否启用滚动功能
|
||
/// 启用时会激活 Scroller 组件并显示滚动条(如果配置了)
|
||
/// </summary>
|
||
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();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取或设置是否启用吸附功能
|
||
/// 启用时滚动停止后会自动对齐到最近的列表项
|
||
/// 注意:此功能依赖于 Scroll 属性,只有在滚动启用时才生效
|
||
/// </summary>
|
||
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();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取或设置滚动速度(范围:1-50)
|
||
/// 值越大,滚动响应越快
|
||
/// </summary>
|
||
public float ScrollSpeed
|
||
{
|
||
get => scrollSpeed;
|
||
set
|
||
{
|
||
if (Mathf.Approximately(scrollSpeed, value)) return;
|
||
scrollSpeed = value;
|
||
|
||
if (scroller != null)
|
||
{
|
||
scroller.ScrollSpeed = scrollSpeed;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取或设置鼠标滚轮的滚动速度(范围:10-50)
|
||
/// 值越大,滚轮滚动的距离越大
|
||
/// </summary>
|
||
public float WheelSpeed
|
||
{
|
||
get => wheelSpeed;
|
||
set
|
||
{
|
||
if (Mathf.Approximately(wheelSpeed, value)) return;
|
||
wheelSpeed = value;
|
||
|
||
if (scroller != null)
|
||
{
|
||
scroller.WheelSpeed = wheelSpeed;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 是否仅在内容超出可视区域时显示滚动条并允许滚动
|
||
/// </summary>
|
||
public bool ShowScrollBarOnlyWhenScrollable
|
||
{
|
||
get => showScrollBarOnlyWhenScrollable;
|
||
set
|
||
{
|
||
if (showScrollBarOnlyWhenScrollable == value) return;
|
||
showScrollBarOnlyWhenScrollable = value;
|
||
RequestLayout();
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
|
||
#region Public Properties - Components
|
||
|
||
/// <summary>
|
||
/// 获取或设置 ViewHolder 模板数组
|
||
/// 用于创建和复用列表项视图
|
||
/// </summary>
|
||
public ViewHolder[] Templates
|
||
{
|
||
get => templates;
|
||
set => templates = value;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取内容容器的 RectTransform
|
||
/// 所有列表项都会作为此容器的子对象
|
||
/// </summary>
|
||
public RectTransform Content
|
||
{
|
||
get
|
||
{
|
||
if (content == null)
|
||
{
|
||
content = transform.GetChild(0).GetComponent<RectTransform>();
|
||
}
|
||
|
||
return content;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取滚动条组件
|
||
/// </summary>
|
||
public Scrollbar Scrollbar => scrollbar;
|
||
|
||
/// <summary>
|
||
/// 获取滚动控制器组件
|
||
/// </summary>
|
||
public Scroller Scroller => scroller;
|
||
|
||
/// <summary>
|
||
/// 获取视图提供器
|
||
/// 负责创建、回收和管理 ViewHolder 实例
|
||
/// 根据模板数量自动选择 SimpleViewProvider 或 MixedViewProvider
|
||
/// </summary>
|
||
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
|
||
|
||
/// <summary>
|
||
/// 获取或设置当前绑定的适配器
|
||
/// 适配器负责提供数据和创建 ViewHolder
|
||
/// </summary>
|
||
public IAdapter RecyclerViewAdapter { get; set; }
|
||
|
||
/// <summary>
|
||
/// 获取或设置当前显示的列表项索引
|
||
/// </summary>
|
||
public int CurrentIndex
|
||
{
|
||
get => currentIndex;
|
||
set => currentIndex = value;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Events
|
||
|
||
/// <summary>
|
||
/// 当前索引改变时触发的事件
|
||
/// 参数为新的索引值
|
||
/// </summary>
|
||
public Action<int> OnIndexChanged;
|
||
|
||
/// <summary>
|
||
/// 滚动位置改变时触发的事件
|
||
/// </summary>
|
||
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<ScrollbarEx>();
|
||
if (scrollbarEx == null)
|
||
{
|
||
scrollbarEx = scrollbar.gameObject.AddComponent<ScrollbarEx>();
|
||
}
|
||
|
||
scrollbarEx.OnDragEnd = OnScrollbarDragEnd;
|
||
UpdateScrollbarVisibility();
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Public Methods - Setup
|
||
|
||
/// <summary>
|
||
/// 设置数据适配器并初始化布局管理器
|
||
/// </summary>
|
||
/// <param name="adapter">要绑定的适配器实例</param>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 重置列表状态
|
||
/// 清空所有 ViewHolder 并将滚动位置重置为起始位置
|
||
/// </summary>
|
||
public void Reset()
|
||
{
|
||
viewProvider?.Reset();
|
||
|
||
if (scroller != null)
|
||
{
|
||
scroller.Position = 0;
|
||
}
|
||
|
||
if (scrollbar != null)
|
||
{
|
||
scrollbar.SetValueWithoutNotify(0);
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Public Methods - Layout
|
||
|
||
/// <summary>
|
||
/// 刷新列表显示
|
||
/// 清空当前所有 ViewHolder 并根据当前滚动位置重新创建可见项
|
||
/// </summary>
|
||
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();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 请求重新布局
|
||
/// 重新计算内容大小、视口大小,并更新滚动条显示状态
|
||
/// </summary>
|
||
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
|
||
|
||
/// <summary>
|
||
/// 获取当前的滚动位置
|
||
/// </summary>
|
||
/// <returns>当前滚动位置值,如果没有 Scroller 则返回 0</returns>
|
||
public float GetScrollPosition()
|
||
{
|
||
return scroller != null ? scroller.Position : 0;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 滚动到指定索引的列表项
|
||
/// </summary>
|
||
/// <param name="index">目标列表项的索引</param>
|
||
/// <param name="smooth">是否使用平滑滚动动画</param>
|
||
public void ScrollTo(int index, bool smooth = false)
|
||
{
|
||
if (!scroll || scroller == null) return;
|
||
|
||
scroller.ScrollTo(layoutManager.IndexToPosition(index), smooth);
|
||
|
||
if (!smooth)
|
||
{
|
||
Refresh();
|
||
}
|
||
|
||
UpdateCurrentIndex(index);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 滚动到指定索引的列表项,并使用指定的对齐方式
|
||
/// </summary>
|
||
/// <param name="index">目标列表项的索引</param>
|
||
/// <param name="alignment">对齐方式(起始、居中或结束)</param>
|
||
/// <param name="offset">额外的偏移量</param>
|
||
/// <param name="smooth">是否使用平滑滚动动画</param>
|
||
/// <param name="duration">滚动动画持续时间(秒)</param>
|
||
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
|
||
}
|
||
}
|