This commit is contained in:
陈思海 2026-04-03 15:21:13 +08:00
parent 589da39636
commit 2177fd8334
7 changed files with 214 additions and 39 deletions

View File

@ -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))
{

View File

@ -24,6 +24,8 @@ namespace AlicizaX.UI
/// <param name="selected">是否处于选中状态。</param>
void UpdateSelection(bool selected);
void SyncSelection(bool selected);
/// <summary>
/// 清理当前渲染实例上的绑定状态。
/// </summary>
@ -76,6 +78,8 @@ namespace AlicizaX.UI
/// <param name="selected">是否处于选中状态。</param>
internal abstract void UpdateSelectionInternal(bool selected);
internal abstract void SyncSelectionInternal(bool selected);
/// <summary>
/// 清理当前绑定产生的临时状态。
/// </summary>
@ -100,6 +104,11 @@ namespace AlicizaX.UI
UpdateSelectionInternal(selected);
}
void IItemRender.SyncSelection(bool selected)
{
SyncSelectionInternal(selected);
}
/// <summary>
/// 由框架内部调用,清理当前渲染实例的绑定状态。
/// </summary>
@ -229,9 +238,12 @@ namespace AlicizaX.UI
/// <param name="selected">是否处于选中状态。</param>
internal override void UpdateSelectionInternal(bool selected)
{
EnsureHolder();
IsSelected = selected;
OnSelectionChanged(selected);
SetSelectionState(selected, true);
}
internal override void SyncSelectionInternal(bool selected)
{
SetSelectionState(selected, true);
}
/// <summary>
@ -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;
}
/// <summary>
@ -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);
}
/// <summary>
/// 每次当前持有者绑定到新的数据项时调用。
/// 仅用于执行数据驱动的界面刷新,例如文本、图片与状态更新。

View File

@ -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<Selectable>(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 =

View File

@ -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)
{
}
}
}

View File

@ -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)

View File

@ -686,7 +686,10 @@ namespace AlicizaX.UI
/// </summary>
/// <param name="entryDirection">焦点进入列表时的方向。</param>
/// <returns>成功聚焦某个列表项时返回 <see langword="true"/>;否则返回 <see langword="false"/>。</returns>
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);
}
/// <summary>
@ -728,14 +732,40 @@ namespace AlicizaX.UI
}
ItemInteractionProxy proxy = holder.GetComponent<ItemInteractionProxy>();
Selectable selectable = proxy != null ? proxy.GetSelectable() : holder.GetComponent<Selectable>();
if (selectable == null)
if (proxy != null)
{
selectable = holder.GetComponentInChildren<Selectable>(true);
return proxy.TryGetFocusTarget(out target);
}
target = selectable != null ? selectable.gameObject : holder.gameObject;
return target != null;
Selectable selectable = holder.GetComponent<Selectable>();
if (!IsSelectableFocusable(selectable))
{
Selectable[] selectables = holder.GetComponentsInChildren<Selectable>(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<IMoveHandler>(holder.gameObject) ||
ExecuteEvents.CanHandleEvent<ISelectHandler>(holder.gameObject) ||
ExecuteEvents.CanHandleEvent<ISubmitHandler>(holder.gameObject))
{
target = holder.gameObject;
return true;
}
return false;
}
/// <summary>
@ -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();
}
/// <summary>
/// 在下一帧尝试恢复目标对象的焦点,避免布局刷新期间焦点丢失。
/// </summary>

View File

@ -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)