2026-03-31 15:18:50 +08:00
|
|
|
using UnityEngine;
|
|
|
|
|
using UnityEngine.EventSystems;
|
|
|
|
|
using UnityEngine.UI;
|
2026-04-29 14:44:30 +08:00
|
|
|
using Cysharp.Text;
|
2026-03-31 15:18:50 +08:00
|
|
|
|
|
|
|
|
namespace AlicizaX.UI
|
|
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
public abstract partial class ViewHolder :
|
2026-03-31 15:18:50 +08:00
|
|
|
IPointerClickHandler,
|
|
|
|
|
IPointerEnterHandler,
|
|
|
|
|
IPointerExitHandler,
|
2026-04-13 15:45:34 +08:00
|
|
|
#if UX_NAVIGATION
|
2026-03-31 15:18:50 +08:00
|
|
|
ISelectHandler,
|
|
|
|
|
IDeselectHandler,
|
|
|
|
|
IMoveHandler,
|
2026-04-13 15:45:34 +08:00
|
|
|
#endif
|
2026-03-31 15:18:50 +08:00
|
|
|
IBeginDragHandler,
|
|
|
|
|
IDragHandler,
|
2026-04-13 15:45:34 +08:00
|
|
|
IEndDragHandler
|
|
|
|
|
#if UX_NAVIGATION
|
|
|
|
|
,
|
2026-03-31 15:18:50 +08:00
|
|
|
ISubmitHandler,
|
|
|
|
|
ICancelHandler
|
2026-04-13 15:45:34 +08:00
|
|
|
#endif
|
2026-03-31 15:18:50 +08:00
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
[SerializeField]
|
|
|
|
|
private ItemInteractionFlags itemInteractionFlags = ItemInteractionFlags.None;
|
|
|
|
|
|
|
|
|
|
private IItemInteractionHost interactionHost;
|
|
|
|
|
private ItemInteractionFlags activeInteractionFlags;
|
2026-03-31 15:18:50 +08:00
|
|
|
private RecyclerItemSelectable ownedSelectable;
|
|
|
|
|
private Scroller parentScroller;
|
2026-04-29 14:44:30 +08:00
|
|
|
private bool missingSelectableLogged;
|
2026-03-31 15:18:50 +08:00
|
|
|
|
2026-04-29 14:44:30 +08:00
|
|
|
public ItemInteractionFlags ItemInteractionFlags
|
2026-03-31 15:18:50 +08:00
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
get => itemInteractionFlags;
|
|
|
|
|
set => itemInteractionFlags = value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal void BindInteractionHost(IItemInteractionHost host)
|
|
|
|
|
{
|
|
|
|
|
interactionHost = host;
|
|
|
|
|
activeInteractionFlags = host?.InteractionFlags ?? itemInteractionFlags;
|
|
|
|
|
parentScroller = RecyclerView != null ? RecyclerView.Scroller : null;
|
2026-03-31 15:18:50 +08:00
|
|
|
EnsureFocusAnchor();
|
|
|
|
|
|
|
|
|
|
if (ownedSelectable != null)
|
|
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
bool requiresSelection = RequiresSelection(activeInteractionFlags);
|
2026-03-31 15:18:50 +08:00
|
|
|
ownedSelectable.interactable = requiresSelection;
|
|
|
|
|
ownedSelectable.enabled = requiresSelection;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
InvalidateNavigationScope();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 14:44:30 +08:00
|
|
|
internal void ClearInteractionHost()
|
2026-03-31 15:18:50 +08:00
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
interactionHost = null;
|
|
|
|
|
activeInteractionFlags = ItemInteractionFlags.None;
|
2026-03-31 15:18:50 +08:00
|
|
|
parentScroller = null;
|
|
|
|
|
|
|
|
|
|
if (ownedSelectable != null)
|
|
|
|
|
{
|
|
|
|
|
ownedSelectable.interactable = false;
|
|
|
|
|
ownedSelectable.enabled = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
InvalidateNavigationScope();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void OnPointerClick(PointerEventData eventData)
|
|
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
if ((activeInteractionFlags & ItemInteractionFlags.PointerClick) != 0)
|
2026-03-31 15:18:50 +08:00
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
interactionHost?.HandlePointerClick(eventData);
|
2026-03-31 15:18:50 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void OnPointerEnter(PointerEventData eventData)
|
|
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
if ((activeInteractionFlags & ItemInteractionFlags.PointerEnter) != 0)
|
2026-03-31 15:18:50 +08:00
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
interactionHost?.HandlePointerEnter(eventData);
|
2026-03-31 15:18:50 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void OnPointerExit(PointerEventData eventData)
|
|
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
if ((activeInteractionFlags & ItemInteractionFlags.PointerExit) != 0)
|
2026-03-31 15:18:50 +08:00
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
interactionHost?.HandlePointerExit(eventData);
|
2026-03-31 15:18:50 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void OnSelect(BaseEventData eventData)
|
|
|
|
|
{
|
2026-04-13 15:45:34 +08:00
|
|
|
#if UX_NAVIGATION
|
2026-04-28 20:52:06 +08:00
|
|
|
#if INPUTSYSTEM_SUPPORT
|
|
|
|
|
UXNavigationRuntime.NotifySelection(gameObject);
|
|
|
|
|
#endif
|
2026-04-29 14:44:30 +08:00
|
|
|
if ((activeInteractionFlags & ItemInteractionFlags.Select) != 0)
|
2026-03-31 15:18:50 +08:00
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
interactionHost?.HandleSelect(eventData);
|
2026-03-31 15:18:50 +08:00
|
|
|
}
|
2026-04-13 15:45:34 +08:00
|
|
|
#endif
|
2026-03-31 15:18:50 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void OnDeselect(BaseEventData eventData)
|
|
|
|
|
{
|
2026-04-13 15:45:34 +08:00
|
|
|
#if UX_NAVIGATION
|
2026-04-29 14:44:30 +08:00
|
|
|
if ((activeInteractionFlags & ItemInteractionFlags.Deselect) != 0)
|
2026-03-31 15:18:50 +08:00
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
interactionHost?.HandleDeselect(eventData);
|
2026-03-31 15:18:50 +08:00
|
|
|
}
|
2026-04-13 15:45:34 +08:00
|
|
|
#endif
|
2026-03-31 15:18:50 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void OnMove(AxisEventData eventData)
|
|
|
|
|
{
|
2026-04-13 15:45:34 +08:00
|
|
|
#if UX_NAVIGATION
|
2026-04-29 14:44:30 +08:00
|
|
|
if ((activeInteractionFlags & ItemInteractionFlags.Move) != 0)
|
2026-03-31 15:18:50 +08:00
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
interactionHost?.HandleMove(eventData);
|
2026-03-31 15:18:50 +08:00
|
|
|
}
|
2026-04-13 15:45:34 +08:00
|
|
|
#endif
|
2026-03-31 15:18:50 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void OnBeginDrag(PointerEventData eventData)
|
|
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
if ((activeInteractionFlags & ItemInteractionFlags.BeginDrag) != 0)
|
2026-03-31 15:18:50 +08:00
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
interactionHost?.HandleBeginDrag(eventData);
|
2026-03-31 15:18:50 +08:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
parentScroller?.OnBeginDrag(eventData);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void OnDrag(PointerEventData eventData)
|
|
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
if ((activeInteractionFlags & ItemInteractionFlags.Drag) != 0)
|
2026-03-31 15:18:50 +08:00
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
interactionHost?.HandleDrag(eventData);
|
2026-03-31 15:18:50 +08:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
parentScroller?.OnDrag(eventData);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void OnEndDrag(PointerEventData eventData)
|
|
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
if ((activeInteractionFlags & ItemInteractionFlags.EndDrag) != 0)
|
2026-03-31 15:18:50 +08:00
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
interactionHost?.HandleEndDrag(eventData);
|
2026-03-31 15:18:50 +08:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
parentScroller?.OnEndDrag(eventData);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void OnSubmit(BaseEventData eventData)
|
|
|
|
|
{
|
2026-04-13 15:45:34 +08:00
|
|
|
#if UX_NAVIGATION
|
2026-04-29 14:44:30 +08:00
|
|
|
if ((activeInteractionFlags & ItemInteractionFlags.Submit) != 0)
|
2026-03-31 15:18:50 +08:00
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
interactionHost?.HandleSubmit(eventData);
|
2026-03-31 15:18:50 +08:00
|
|
|
}
|
2026-04-13 15:45:34 +08:00
|
|
|
#endif
|
2026-03-31 15:18:50 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void OnCancel(BaseEventData eventData)
|
|
|
|
|
{
|
2026-04-13 15:45:34 +08:00
|
|
|
#if UX_NAVIGATION
|
2026-04-29 14:44:30 +08:00
|
|
|
if ((activeInteractionFlags & ItemInteractionFlags.Cancel) != 0)
|
2026-03-31 15:18:50 +08:00
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
interactionHost?.HandleCancel(eventData);
|
2026-03-31 15:18:50 +08:00
|
|
|
}
|
2026-04-13 15:45:34 +08:00
|
|
|
#endif
|
2026-03-31 15:18:50 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void EnsureFocusAnchor()
|
|
|
|
|
{
|
|
|
|
|
if (focusAnchor != null)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
focusAnchor = GetComponent<Selectable>();
|
2026-04-13 15:45:34 +08:00
|
|
|
#if !UX_NAVIGATION
|
|
|
|
|
if (focusAnchor is RecyclerItemSelectable)
|
|
|
|
|
{
|
|
|
|
|
focusAnchor = null;
|
|
|
|
|
}
|
|
|
|
|
#endif
|
2026-03-31 15:18:50 +08:00
|
|
|
if (focusAnchor != null)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 15:45:34 +08:00
|
|
|
#if UX_NAVIGATION
|
2026-03-31 15:18:50 +08:00
|
|
|
ownedSelectable = GetComponent<RecyclerItemSelectable>();
|
2026-04-29 14:44:30 +08:00
|
|
|
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
|
|
|
|
if (ownedSelectable == null && RequiresSelection(activeInteractionFlags) && !missingSelectableLogged)
|
2026-03-31 15:18:50 +08:00
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
missingSelectableLogged = true;
|
|
|
|
|
UnityEngine.Debug.LogError(ZString.Format("RecyclerItemSelectable is missing on '{0}'. Add it in prefab/editor setup.", GetHierarchyPath(transform)));
|
2026-03-31 15:18:50 +08:00
|
|
|
}
|
2026-04-29 14:44:30 +08:00
|
|
|
#endif
|
2026-03-31 15:18:50 +08:00
|
|
|
focusAnchor = ownedSelectable;
|
2026-04-13 15:45:34 +08:00
|
|
|
#endif
|
2026-03-31 15:18:50 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-29 14:44:30 +08:00
|
|
|
private bool TryGetInteractionFocusTarget(out GameObject target)
|
2026-04-03 15:21:13 +08:00
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
target = null;
|
2026-04-03 15:21:13 +08:00
|
|
|
EnsureFocusAnchor();
|
|
|
|
|
if (IsSelectableFocusable(focusAnchor))
|
|
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
target = focusAnchor.gameObject;
|
|
|
|
|
return target != null;
|
2026-04-03 15:21:13 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-29 14:44:30 +08:00
|
|
|
if (selectableCache.Count == 0)
|
2026-04-03 15:21:13 +08:00
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
RefreshInteractionCache();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < selectableCache.Count; i++)
|
|
|
|
|
{
|
|
|
|
|
Selectable candidate = selectableCache[i];
|
2026-04-03 15:21:13 +08:00
|
|
|
if (candidate == focusAnchor)
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 15:45:34 +08:00
|
|
|
#if !UX_NAVIGATION
|
|
|
|
|
if (candidate is RecyclerItemSelectable)
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
#endif
|
2026-04-03 15:21:13 +08:00
|
|
|
if (IsSelectableFocusable(candidate))
|
|
|
|
|
{
|
2026-04-29 14:44:30 +08:00
|
|
|
target = candidate.gameObject;
|
|
|
|
|
return target != null;
|
2026-04-03 15:21:13 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 15:18:50 +08:00
|
|
|
private static bool RequiresSelection(ItemInteractionFlags interactionFlags)
|
|
|
|
|
{
|
2026-04-13 15:45:34 +08:00
|
|
|
#if !UX_NAVIGATION
|
|
|
|
|
return false;
|
|
|
|
|
#else
|
2026-03-31 15:18:50 +08:00
|
|
|
const ItemInteractionFlags selectionFlags =
|
|
|
|
|
ItemInteractionFlags.Select |
|
|
|
|
|
ItemInteractionFlags.Deselect |
|
|
|
|
|
ItemInteractionFlags.Move |
|
|
|
|
|
ItemInteractionFlags.Submit |
|
|
|
|
|
ItemInteractionFlags.Cancel;
|
|
|
|
|
|
|
|
|
|
return (interactionFlags & selectionFlags) != 0;
|
2026-04-13 15:45:34 +08:00
|
|
|
#endif
|
2026-03-31 15:18:50 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-29 14:44:30 +08:00
|
|
|
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
|
|
|
|
private static string GetHierarchyPath(Transform target)
|
|
|
|
|
{
|
|
|
|
|
if (target == null)
|
|
|
|
|
{
|
|
|
|
|
return "<null>";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
string path = target.name;
|
|
|
|
|
Transform parent = target.parent;
|
|
|
|
|
while (parent != null)
|
|
|
|
|
{
|
|
|
|
|
path = ZString.Format("{0}/{1}", parent.name, path);
|
|
|
|
|
parent = parent.parent;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return path;
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
2026-03-31 15:18:50 +08:00
|
|
|
[System.Diagnostics.Conditional("INPUTSYSTEM_SUPPORT")]
|
|
|
|
|
private void InvalidateNavigationScope()
|
|
|
|
|
{
|
|
|
|
|
#if INPUTSYSTEM_SUPPORT && UX_NAVIGATION
|
|
|
|
|
var scope = GetComponentInParent<UnityEngine.UI.UXNavigationScope>(true);
|
|
|
|
|
scope?.InvalidateSelectableCache();
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|