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)