From 2177fd83346542d9eb813358ac6e156c9a7c7f38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=80=9D=E6=B5=B7?= <1464576565@qq.com> Date: Fri, 3 Apr 2026 15:21:13 +0800 Subject: [PATCH] fix --- Runtime/RecyclerView/Adapter/Adapter.cs | 5 +- Runtime/RecyclerView/Adapter/ItemRender.cs | 53 ++++++++--- .../Interaction/ItemInteractionProxy.cs | 48 ++++++++++ .../Interaction/RecyclerItemSelectable.cs | 5 ++ .../RecyclerNavigationController.cs | 46 ++++++---- Runtime/RecyclerView/RecyclerView.cs | 89 +++++++++++++++++-- Runtime/RecyclerView/UGList.cs | 7 +- 7 files changed, 214 insertions(+), 39 deletions(-) diff --git a/Runtime/RecyclerView/Adapter/Adapter.cs b/Runtime/RecyclerView/Adapter/Adapter.cs index 5e12675..4b588ed 100644 --- a/Runtime/RecyclerView/Adapter/Adapter.cs +++ b/Runtime/RecyclerView/Adapter/Adapter.cs @@ -83,7 +83,8 @@ namespace AlicizaX.UI itemRender.Bind(data, index); } - itemRender.UpdateSelection(index == choiceIndex); + bool selected = index == choiceIndex; + itemRender.SyncSelection(selected); return; } @@ -98,7 +99,6 @@ namespace AlicizaX.UI if (TryGetItemRender(viewHolder, out var itemRender)) { - itemRender.UpdateSelection(false); itemRender.Unbind(); } } @@ -373,6 +373,7 @@ namespace AlicizaX.UI } if (index == choiceIndex) return; + int previousChoice = choiceIndex; if (choiceIndex != -1 && TryGetViewHolder(choiceIndex, out var oldHolder)) { diff --git a/Runtime/RecyclerView/Adapter/ItemRender.cs b/Runtime/RecyclerView/Adapter/ItemRender.cs index 0c0dab5..7afc627 100644 --- a/Runtime/RecyclerView/Adapter/ItemRender.cs +++ b/Runtime/RecyclerView/Adapter/ItemRender.cs @@ -24,6 +24,8 @@ namespace AlicizaX.UI /// 是否处于选中状态。 void UpdateSelection(bool selected); + void SyncSelection(bool selected); + /// /// 清理当前渲染实例上的绑定状态。 /// @@ -76,6 +78,8 @@ namespace AlicizaX.UI /// 是否处于选中状态。 internal abstract void UpdateSelectionInternal(bool selected); + internal abstract void SyncSelectionInternal(bool selected); + /// /// 清理当前绑定产生的临时状态。 /// @@ -100,6 +104,11 @@ namespace AlicizaX.UI UpdateSelectionInternal(selected); } + void IItemRender.SyncSelection(bool selected) + { + SyncSelectionInternal(selected); + } + /// /// 由框架内部调用,清理当前渲染实例的绑定状态。 /// @@ -229,9 +238,12 @@ namespace AlicizaX.UI /// 是否处于选中状态。 internal override void UpdateSelectionInternal(bool selected) { - EnsureHolder(); - IsSelected = selected; - OnSelectionChanged(selected); + SetSelectionState(selected, true); + } + + internal override void SyncSelectionInternal(bool selected) + { + SetSelectionState(selected, true); } /// @@ -241,12 +253,7 @@ namespace AlicizaX.UI { if (Holder != null) { - if (IsSelected) - { - IsSelected = false; - OnSelectionChanged(false); - } - + ClearSelectionState(); OnClear(); if (interactionProxy != null) { @@ -262,7 +269,6 @@ namespace AlicizaX.UI CurrentIndex = -1; CurrentLayoutIndex = -1; CurrentBindingVersion = 0; - IsSelected = false; } /// @@ -295,6 +301,33 @@ namespace AlicizaX.UI OnBind(itemData, index); } + private void SetSelectionState(bool selected, bool notify) + { + EnsureHolder(); + bool previousSelected = IsSelected; + if (previousSelected == selected) + { + return; + } + + IsSelected = selected; + if (notify) + { + OnSelectionChanged(IsSelected); + } + } + + private void ClearSelectionState() + { + if (!IsSelected) + { + return; + } + + IsSelected = false; + OnSelectionChanged(false); + } + /// /// 每次当前持有者绑定到新的数据项时调用。 /// 仅用于执行数据驱动的界面刷新,例如文本、图片与状态更新。 diff --git a/Runtime/RecyclerView/Interaction/ItemInteractionProxy.cs b/Runtime/RecyclerView/Interaction/ItemInteractionProxy.cs index 37cc2f0..e519a7f 100644 --- a/Runtime/RecyclerView/Interaction/ItemInteractionProxy.cs +++ b/Runtime/RecyclerView/Interaction/ItemInteractionProxy.cs @@ -62,6 +62,18 @@ namespace AlicizaX.UI return focusAnchor; } + internal bool TryGetFocusTarget(out GameObject target) + { + target = null; + if (!TryGetFocusableSelectable(out Selectable selectable)) + { + return false; + } + + target = selectable.gameObject; + return target != null; + } + public void OnPointerClick(PointerEventData eventData) { if ((flags & ItemInteractionFlags.PointerClick) != 0) @@ -181,6 +193,42 @@ namespace AlicizaX.UI focusAnchor = ownedSelectable; } + private bool TryGetFocusableSelectable(out Selectable selectable) + { + EnsureFocusAnchor(); + if (IsSelectableFocusable(focusAnchor)) + { + selectable = focusAnchor; + return true; + } + + Selectable[] selectables = GetComponentsInChildren(true); + for (int i = 0; i < selectables.Length; i++) + { + Selectable candidate = selectables[i]; + if (candidate == focusAnchor) + { + continue; + } + + if (IsSelectableFocusable(candidate)) + { + selectable = candidate; + return true; + } + } + + selectable = null; + return false; + } + + private static bool IsSelectableFocusable(Selectable selectable) + { + return selectable != null && + selectable.IsActive() && + selectable.IsInteractable(); + } + private static bool RequiresSelection(ItemInteractionFlags interactionFlags) { const ItemInteractionFlags selectionFlags = diff --git a/Runtime/RecyclerView/Interaction/RecyclerItemSelectable.cs b/Runtime/RecyclerView/Interaction/RecyclerItemSelectable.cs index 074a285..7ef7d49 100644 --- a/Runtime/RecyclerView/Interaction/RecyclerItemSelectable.cs +++ b/Runtime/RecyclerView/Interaction/RecyclerItemSelectable.cs @@ -1,3 +1,4 @@ +using UnityEngine.EventSystems; using UnityEngine.UI; namespace AlicizaX.UI @@ -12,5 +13,9 @@ namespace AlicizaX.UI disabledNavigation.mode = Navigation.Mode.None; navigation = disabledNavigation; } + + public override void OnMove(AxisEventData eventData) + { + } } } diff --git a/Runtime/RecyclerView/Navigation/RecyclerNavigationController.cs b/Runtime/RecyclerView/Navigation/RecyclerNavigationController.cs index bbe0a92..fb83ca7 100644 --- a/Runtime/RecyclerView/Navigation/RecyclerNavigationController.cs +++ b/Runtime/RecyclerView/Navigation/RecyclerNavigationController.cs @@ -36,23 +36,37 @@ namespace AlicizaX.UI bool isLoopSource = itemCount != realCount; bool allowWrap = isLoopSource && options.Wrap; - int nextLayoutIndex = currentHolder.Index + step; - if (allowWrap) - { - nextLayoutIndex = WrapIndex(nextLayoutIndex, realCount); - } - else - { - nextLayoutIndex = Mathf.Clamp(nextLayoutIndex, 0, itemCount - 1); - } - - if (!allowWrap && nextLayoutIndex == currentHolder.Index) - { - return false; - } - + int originalIndex = currentHolder.Index; + int currentIndex = currentHolder.Index; + int nextLayoutIndex = currentIndex; + int maxAttempts = allowWrap ? realCount : itemCount; ScrollAlignment alignment = ResolveAlignment(direction, options.Alignment); - return recyclerView.TryFocusIndex(nextLayoutIndex, options.SmoothScroll, alignment); + for (int attempt = 0; attempt < maxAttempts; attempt++) + { + nextLayoutIndex += step; + if (allowWrap) + { + nextLayoutIndex = WrapIndex(nextLayoutIndex, realCount); + } + else + { + nextLayoutIndex = Mathf.Clamp(nextLayoutIndex, 0, itemCount - 1); + if (nextLayoutIndex == currentIndex) + { + break; + } + } + + if (recyclerView.TryFocusIndex(nextLayoutIndex, options.SmoothScroll, alignment)) + { + return true; + } + + currentIndex = nextLayoutIndex; + } + + recyclerView.TryFocusIndex(originalIndex, false, alignment); + return false; } private int GetStep(MoveDirection direction) diff --git a/Runtime/RecyclerView/RecyclerView.cs b/Runtime/RecyclerView/RecyclerView.cs index 96f7987..f65812d 100644 --- a/Runtime/RecyclerView/RecyclerView.cs +++ b/Runtime/RecyclerView/RecyclerView.cs @@ -686,7 +686,10 @@ namespace AlicizaX.UI /// /// 焦点进入列表时的方向。 /// 成功聚焦某个列表项时返回 ;否则返回 - public bool TryFocusEntry(MoveDirection entryDirection) + public bool TryFocusEntry( + MoveDirection entryDirection, + bool smooth = false, + ScrollAlignment alignment = ScrollAlignment.Center) { if (RecyclerViewAdapter == null) { @@ -710,7 +713,8 @@ namespace AlicizaX.UI : Mathf.Clamp(CurrentIndex, 0, realCount - 1); } - return TryFocusIndex(targetIndex); + int step = entryDirection is MoveDirection.Up or MoveDirection.Left ? -1 : 1; + return TryFocusIndexRange(targetIndex, step, realCount, smooth, alignment); } /// @@ -728,14 +732,40 @@ namespace AlicizaX.UI } ItemInteractionProxy proxy = holder.GetComponent(); - Selectable selectable = proxy != null ? proxy.GetSelectable() : holder.GetComponent(); - if (selectable == null) + if (proxy != null) { - selectable = holder.GetComponentInChildren(true); + return proxy.TryGetFocusTarget(out target); } - target = selectable != null ? selectable.gameObject : holder.gameObject; - return target != null; + Selectable selectable = holder.GetComponent(); + if (!IsSelectableFocusable(selectable)) + { + Selectable[] selectables = holder.GetComponentsInChildren(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(holder.gameObject) || + ExecuteEvents.CanHandleEvent(holder.gameObject) || + ExecuteEvents.CanHandleEvent(holder.gameObject)) + { + target = holder.gameObject; + return true; + } + + return false; } /// @@ -795,11 +825,24 @@ namespace AlicizaX.UI return; } - if (!ReferenceEquals(eventSystem.currentSelectedGameObject, target)) + if (ReferenceEquals(eventSystem.currentSelectedGameObject, target)) { - eventSystem.SetSelectedGameObject(target); + ScheduleFocusRecovery(target); + return; } + if (eventSystem.alreadySelecting) + { + ScheduleFocusRecovery(target); + return; + } + + eventSystem.SetSelectedGameObject(target); + ScheduleFocusRecovery(target); + } + + private void ScheduleFocusRecovery(GameObject target) + { if (focusRecoveryCoroutine != null) { StopCoroutine(focusRecoveryCoroutine); @@ -808,6 +851,34 @@ namespace AlicizaX.UI focusRecoveryCoroutine = StartCoroutine(RecoverFocusNextFrame(target)); } + 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(); + } + /// /// 在下一帧尝试恢复目标对象的焦点,避免布局刷新期间焦点丢失。 /// diff --git a/Runtime/RecyclerView/UGList.cs b/Runtime/RecyclerView/UGList.cs index 7638c04..7cd6d49 100644 --- a/Runtime/RecyclerView/UGList.cs +++ b/Runtime/RecyclerView/UGList.cs @@ -195,9 +195,12 @@ namespace AlicizaX.UI return index >= 0 && TryFocus(index, smooth, alignment); } - public bool TryFocusEntry(MoveDirection entryDirection) + public bool TryFocusEntry( + MoveDirection entryDirection, + bool smooth = false, + ScrollAlignment alignment = ScrollAlignment.Center) { - return _recyclerView != null && _recyclerView.TryFocusEntry(entryDirection); + return _recyclerView != null && _recyclerView.TryFocusEntry(entryDirection, smooth, alignment); } public void ScrollToIndex(int index, bool smooth = false)