diff --git a/Runtime/RecyclerView/Adapter/Adapter.cs b/Runtime/RecyclerView/Adapter/Adapter.cs index e3f83a3..5e12675 100644 --- a/Runtime/RecyclerView/Adapter/Adapter.cs +++ b/Runtime/RecyclerView/Adapter/Adapter.cs @@ -37,6 +37,8 @@ namespace AlicizaX.UI set => SetChoiceIndex(value); } + internal Action OnChoiceIndexChanged; + public Adapter(RecyclerView recyclerView) : this(recyclerView, new List()) { } @@ -383,6 +385,8 @@ namespace AlicizaX.UI { UpdateSelectionState(newHolder, true); } + + OnChoiceIndexChanged?.Invoke(choiceIndex); } protected virtual bool TryGetBindData(int index, out T data) diff --git a/Runtime/RecyclerView/Adapter/GroupAdapter.cs b/Runtime/RecyclerView/Adapter/GroupAdapter.cs index 09dd9d5..f8140e8 100644 --- a/Runtime/RecyclerView/Adapter/GroupAdapter.cs +++ b/Runtime/RecyclerView/Adapter/GroupAdapter.cs @@ -26,6 +26,11 @@ namespace AlicizaX.UI return showList.Count; } + public override int GetRealCount() + { + return showList.Count; + } + public override string GetViewName(int index) { return index >= 0 && index < showList.Count @@ -60,10 +65,10 @@ namespace AlicizaX.UI continue; } - Collapse(i); + CollapseInternal(i); if (group.Expanded) { - Expand(i); + ExpandInternal(i); i += CountItemsForType(group.Type); } } @@ -93,6 +98,47 @@ namespace AlicizaX.UI } public void Expand(int index) + { + SetExpanded(index, true); + } + + public void Collapse(int index) + { + SetExpanded(index, false); + } + + public bool SetExpanded(int index, bool expanded) + { + if (!TryGetDisplayData(index, out TData data) || !IsGroupIndex(index)) + { + return false; + } + + data.Expanded = expanded; + NotifyDataChanged(); + return true; + } + + public bool IsGroupIndex(int index) + { + return index >= 0 && + index < showList.Count && + string.Equals(showList[index].TemplateName, groupViewName, StringComparison.Ordinal); + } + + public bool TryGetDisplayData(int index, out TData data) + { + if (list == null || index < 0 || index >= showList.Count) + { + data = default; + return false; + } + + data = showList[index]; + return true; + } + + private void ExpandInternal(int index) { if (list == null || index < 0 || index >= showList.Count) { @@ -110,7 +156,7 @@ namespace AlicizaX.UI } } - public void Collapse(int index) + private void CollapseInternal(int index) { if (index < 0 || index >= showList.Count) { @@ -155,10 +201,9 @@ namespace AlicizaX.UI } TData data = showList[index]; - if (data.TemplateName == groupViewName) + if (IsGroupIndex(index)) { - data.Expanded = !data.Expanded; - NotifyDataChanged(); + SetExpanded(index, !data.Expanded); return; } diff --git a/Runtime/RecyclerView/RecyclerView.cs b/Runtime/RecyclerView/RecyclerView.cs index 5f5c0e7..96f7987 100644 --- a/Runtime/RecyclerView/RecyclerView.cs +++ b/Runtime/RecyclerView/RecyclerView.cs @@ -413,7 +413,7 @@ namespace AlicizaX.UI /// /// 获取当前记录的内部逻辑索引。 - /// 仅供框架内部的导航与布局逻辑使用;业务层请改用 维护自身状态, + /// 仅供框架内部的导航与布局逻辑使用;业务层请改用 维护自身状态, /// 或使用适配器上的 ChoiceIndex 表示业务选中项。 /// internal int CurrentIndex => currentIndex; @@ -425,7 +425,22 @@ namespace AlicizaX.UI /// /// 当当前逻辑索引发生变化时触发。 /// - public Action OnIndexChanged; + public Action OnFocusIndexChanged; + + /// + /// 当滚动位置发生变化时触发。 + /// + public Action OnScrollValueChanged; + + /// + /// 当滚动停止时触发。 + /// + public Action OnScrollStopped; + + /// + /// 当拖拽状态变化时触发。 + /// + public Action OnScrollDraggingChanged; #endregion @@ -527,6 +542,7 @@ namespace AlicizaX.UI scroller.Snap = snap; scroller.OnValueChanged.AddListener(OnScrollChanged); scroller.OnMoveStoped.AddListener(OnMoveStoped); + scroller.OnDragging.AddListener(OnScrollerDraggingChanged); UpdateScrollerState(); } @@ -1060,6 +1076,7 @@ namespace AlicizaX.UI UpdateScrollbarValue(position); UpdateVisibleRange(); layoutManager.DoItemAnimation(); + OnScrollValueChanged?.Invoke(position); } /// @@ -1073,6 +1090,16 @@ namespace AlicizaX.UI } TryProcessPendingFocusRequest(); + OnScrollStopped?.Invoke(); + } + + /// + /// 响应滚动器拖拽状态变化。 + /// + /// 当前是否正在拖拽。 + private void OnScrollerDraggingChanged(bool dragging) + { + OnScrollDraggingChanged?.Invoke(dragging); } /// @@ -1395,7 +1422,7 @@ namespace AlicizaX.UI if (currentIndex != -1) { currentIndex = -1; - OnIndexChanged?.Invoke(currentIndex); + OnFocusIndexChanged?.Invoke(currentIndex); } return; @@ -1407,7 +1434,7 @@ namespace AlicizaX.UI if (currentIndex != index) { currentIndex = index; - OnIndexChanged?.Invoke(currentIndex); + OnFocusIndexChanged?.Invoke(currentIndex); } } diff --git a/Runtime/RecyclerView/UGList.cs b/Runtime/RecyclerView/UGList.cs index f706af0..923a03f 100644 --- a/Runtime/RecyclerView/UGList.cs +++ b/Runtime/RecyclerView/UGList.cs @@ -1,36 +1,19 @@ using System; using System.Collections.Generic; +using UnityEngine.EventSystems; namespace AlicizaX.UI { - /// - /// 封装 RecyclerView 与 Adapter 的通用列表基类。 - /// - /// 列表数据类型。 - /// 适配器类型。 - public abstract class UGListBase where TAdapter : Adapter where TData : ISimpleViewData + public abstract class UGListBase + where TAdapter : Adapter + where TData : ISimpleViewData { - /// - /// 关联的 RecyclerView 实例。 - /// protected readonly RecyclerView _recyclerView; - - /// - /// 当前列表使用的适配器实例。 - /// protected readonly TAdapter _adapter; - /// - /// 获取当前绑定的 RecyclerView。 - /// - public RecyclerView RecyclerView => _recyclerView; + private List _datas; - /// - /// 初始化列表封装并将适配器绑定到 RecyclerView。 - /// - /// 目标 RecyclerView。 - /// 用于驱动列表渲染的适配器。 - public UGListBase(RecyclerView recyclerView, TAdapter adapter) + protected UGListBase(RecyclerView recyclerView, TAdapter adapter) { _recyclerView = recyclerView; _adapter = adapter; @@ -41,60 +24,10 @@ namespace AlicizaX.UI } } - /// - /// 获取当前列表使用的适配器。 - /// + public RecyclerView RecyclerView => _recyclerView; + public TAdapter Adapter => _adapter; - /// - /// 注册指定视图类型对应的 ItemRender。 - /// - /// ItemRender 类型。 - /// 视图名称;为空时表示默认视图。 - public void RegisterItemRender(string viewName = "") where TItemRender : ItemRenderBase - { - _adapter.RegisterItemRender(viewName); - } - - /// - /// 按运行时类型注册指定视图对应的 ItemRender。 - /// - /// ItemRender 的运行时类型。 - /// 视图名称;为空时表示默认视图。 - public void RegisterItemRender(Type itemRenderType, string viewName = "") - { - _adapter.RegisterItemRender(itemRenderType, viewName); - } - - /// - /// 注销指定视图名称对应的 ItemRender 注册。 - /// - /// 视图名称;为空时表示默认视图。 - /// 是否成功移除对应注册。 - public bool UnregisterItemRender(string viewName = "") - { - return _adapter.UnregisterItemRender(viewName); - } - - /// - /// 清空当前列表的全部 ItemRender 注册信息。 - /// - public void ClearItemRenderRegistrations() - { - _adapter.ClearItemRenderRegistrations(); - } - - /// - /// 当前持有的数据集合引用。 - /// - private List _datas; - - /// - /// 获取或设置当前列表数据。 - /// - /// - /// 设置数据时会同步调用适配器刷新列表内容。 - /// public List Data { get => _datas; @@ -104,112 +37,288 @@ namespace AlicizaX.UI _adapter.SetList(_datas); } } + + public int DataCount => _datas?.Count ?? 0; + + public int FocusIndex => _recyclerView != null ? _recyclerView.CurrentIndex : -1; + + public int ChoiceIndex + { + get => _adapter != null ? _adapter.ChoiceIndex : -1; + set + { + if (_adapter != null) + { + _adapter.ChoiceIndex = value; + } + } + } + + public bool HasChoice => ChoiceIndex >= 0; + + public float ScrollPosition => _recyclerView != null ? _recyclerView.GetScrollPosition() : 0f; + + public event Action OnFocusIndexChanged + { + add + { + if (_recyclerView != null) + { + _recyclerView.OnFocusIndexChanged += value; + } + } + remove + { + if (_recyclerView != null) + { + _recyclerView.OnFocusIndexChanged -= value; + } + } + } + + public event Action OnChoiceIndexChanged + { + add + { + if (_adapter != null) + { + _adapter.OnChoiceIndexChanged += value; + } + } + remove + { + if (_adapter != null) + { + _adapter.OnChoiceIndexChanged -= value; + } + } + } + + public event Action ScrollValueChanged + { + add + { + if (_recyclerView != null) + { + _recyclerView.OnScrollValueChanged += value; + } + } + remove + { + if (_recyclerView != null) + { + _recyclerView.OnScrollValueChanged -= value; + } + } + } + + public event Action ScrollStopped + { + add + { + if (_recyclerView != null) + { + _recyclerView.OnScrollStopped += value; + } + } + remove + { + if (_recyclerView != null) + { + _recyclerView.OnScrollStopped -= value; + } + } + } + + public event Action ScrollDraggingChanged + { + add + { + if (_recyclerView != null) + { + _recyclerView.OnScrollDraggingChanged += value; + } + } + remove + { + if (_recyclerView != null) + { + _recyclerView.OnScrollDraggingChanged -= value; + } + } + } + + public void RegisterItemRender(string viewName = "") where TItemRender : ItemRenderBase + { + _adapter.RegisterItemRender(viewName); + } + + public void RegisterItemRender(Type itemRenderType, string viewName = "") + { + _adapter.RegisterItemRender(itemRenderType, viewName); + } + + public bool UnregisterItemRender(string viewName = "") + { + return _adapter.UnregisterItemRender(viewName); + } + + public void ClearItemRenderRegistrations() + { + _adapter.ClearItemRenderRegistrations(); + } + + public void ClearChoice() + { + ChoiceIndex = -1; + } + + public bool TryFocus(int index, bool smooth = false, ScrollAlignment alignment = ScrollAlignment.Center) + { + return _recyclerView != null && _recyclerView.TryFocusIndex(index, smooth, alignment); + } + + public bool TryFocusChoice(bool smooth = false, ScrollAlignment alignment = ScrollAlignment.Center) + { + int index = ChoiceIndex; + return index >= 0 && TryFocus(index, smooth, alignment); + } + + public bool TryFocusEntry(MoveDirection entryDirection) + { + return _recyclerView != null && _recyclerView.TryFocusEntry(entryDirection); + } + + public void ScrollToIndex(int index, bool smooth = false) + { + _recyclerView?.ScrollTo(index, smooth); + } + + public void ScrollTo( + int index, + ScrollAlignment alignment = ScrollAlignment.Start, + float offset = 0f, + bool smooth = false, + float duration = 0.3f) + { + _recyclerView?.ScrollToWithAlignment(index, alignment, offset, smooth, duration); + } + + public void ScrollToChoice( + ScrollAlignment alignment = ScrollAlignment.Center, + float offset = 0f, + bool smooth = false, + float duration = 0.3f) + { + int index = ChoiceIndex; + if (index >= 0) + { + ScrollTo(index, alignment, offset, smooth, duration); + } + } + + public void ScrollToFocus( + ScrollAlignment alignment = ScrollAlignment.Center, + float offset = 0f, + bool smooth = false, + float duration = 0.3f) + { + int index = FocusIndex; + if (index >= 0) + { + ScrollTo(index, alignment, offset, smooth, duration); + } + } + + public void ScrollToStart(int index, float offset = 0f, bool smooth = false, float duration = 0.3f) + { + ScrollTo(index, ScrollAlignment.Start, offset, smooth, duration); + } + + public void ScrollToCenter(int index, float offset = 0f, bool smooth = false, float duration = 0.3f) + { + ScrollTo(index, ScrollAlignment.Center, offset, smooth, duration); + } + + public void ScrollToEnd(int index, float offset = 0f, bool smooth = false, float duration = 0.3f) + { + ScrollTo(index, ScrollAlignment.End, offset, smooth, duration); + } } - /// - /// 提供单模板列表的便捷封装。 - /// - /// 列表数据类型。 public class UGList : UGListBase> where TData : ISimpleViewData { - /// - /// 初始化单模板列表。 - /// - /// 目标 RecyclerView。 public UGList(RecyclerView recyclerView) : base(recyclerView, new Adapter(recyclerView)) { } } - /// - /// 提供分组列表的便捷封装。 - /// - /// 分组列表数据类型。 public class UGGroupList : UGListBase> where TData : class, IGroupViewData, new() { - /// - /// 初始化分组列表。 - /// - /// 目标 RecyclerView。 - /// 分组头使用的模板名称。 public UGGroupList(RecyclerView recyclerView, string groupViewName) : base(recyclerView, new GroupAdapter(recyclerView, groupViewName)) { } + + public bool IsGroupIndex(int index) + { + return _adapter.IsGroupIndex(index); + } + + public bool TryGetDisplayData(int index, out TData data) + { + return _adapter.TryGetDisplayData(index, out data); + } + + public bool SetExpanded(int index, bool expanded) + { + return _adapter.SetExpanded(index, expanded); + } + + public void Expand(int index) + { + _adapter.Expand(index); + } + + public void Collapse(int index) + { + _adapter.Collapse(index); + } + + public void Activate(int index) + { + _adapter.Activate(index); + } } - /// - /// 提供循环列表的便捷封装。 - /// - /// 循环列表数据类型。 public class UGLoopList : UGListBase> where TData : ISimpleViewData, new() { - /// - /// 初始化循环列表。 - /// - /// 目标 RecyclerView。 public UGLoopList(RecyclerView recyclerView) : base(recyclerView, new LoopAdapter(recyclerView)) { } } - /// - /// 提供多模板列表的便捷封装。 - /// - /// 多模板列表数据类型。 public class UGMixedList : UGListBase> where TData : IMixedViewData { - /// - /// 初始化多模板列表。 - /// - /// 目标 RecyclerView。 public UGMixedList(RecyclerView recyclerView) : base(recyclerView, new MixedAdapter(recyclerView)) { } } - /// - /// 提供常用 UGList 类型的快速创建方法。 - /// public static class UGListCreateHelper { - /// - /// 创建单模板列表封装。 - /// - /// 列表数据类型。 - /// 目标 RecyclerView。 - /// 创建后的单模板列表实例。 public static UGList Create(RecyclerView recyclerView) where TData : ISimpleViewData => new UGList(recyclerView); - /// - /// 创建分组列表封装。 - /// - /// 分组列表数据类型。 - /// 目标 RecyclerView。 - /// 分组头使用的模板名称。 - /// 创建后的分组列表实例。 public static UGGroupList CreateGroup(RecyclerView recyclerView, string groupViewName) where TData : class, IGroupViewData, new() => new UGGroupList(recyclerView, groupViewName); - /// - /// 创建循环列表封装。 - /// - /// 循环列表数据类型。 - /// 目标 RecyclerView。 - /// 创建后的循环列表实例。 public static UGLoopList CreateLoop(RecyclerView recyclerView) where TData : ISimpleViewData, new() => new UGLoopList(recyclerView); - /// - /// 创建多模板列表封装。 - /// - /// 多模板列表数据类型。 - /// 目标 RecyclerView。 - /// 创建后的多模板列表实例。 public static UGMixedList CreateMixed(RecyclerView recyclerView) where TData : IMixedViewData => new UGMixedList(recyclerView); } diff --git a/Runtime/RecyclerView/UGList.cs.meta b/Runtime/RecyclerView/UGList.cs.meta index 7cfff98..5fff2c5 100644 --- a/Runtime/RecyclerView/UGList.cs.meta +++ b/Runtime/RecyclerView/UGList.cs.meta @@ -1,3 +1,11 @@ fileFormatVersion: 2 guid: fd094e3cb2194cb193221ea9436a169a -timeCreated: 1766729620 \ No newline at end of file +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/UGListExtensions.cs b/Runtime/RecyclerView/UGListExtensions.cs index c7150db..5f138c5 100644 --- a/Runtime/RecyclerView/UGListExtensions.cs +++ b/Runtime/RecyclerView/UGListExtensions.cs @@ -8,6 +8,7 @@ namespace AlicizaX.UI /// public static class UGListExtensions { + public static bool DebugScrollTo { get; set; } = false; /// /// 将列表滚动到指定索引,并按给定对齐方式定位。 @@ -36,7 +37,7 @@ namespace AlicizaX.UI return; } - ugList.RecyclerView.ScrollToWithAlignment(index, alignment, offset, smooth, duration); + ugList.ScrollTo(index, alignment, offset, smooth, duration); } /// @@ -58,7 +59,7 @@ namespace AlicizaX.UI where TAdapter : Adapter where TData : ISimpleViewData { - ugList.ScrollTo(index, ScrollAlignment.Start, offset, smooth, duration); + ugList.ScrollToStart(index, offset, smooth, duration); } /// @@ -80,7 +81,7 @@ namespace AlicizaX.UI where TAdapter : Adapter where TData : ISimpleViewData { - ugList.ScrollTo(index, ScrollAlignment.Center, offset, smooth, duration); + ugList.ScrollToCenter(index, offset, smooth, duration); } /// @@ -102,7 +103,7 @@ namespace AlicizaX.UI where TAdapter : Adapter where TData : ISimpleViewData { - ugList.ScrollTo(index, ScrollAlignment.End, offset, smooth, duration); + ugList.ScrollToEnd(index, offset, smooth, duration); } } }