[Opt] 优化虚拟列表 优化手柄导航功能

This commit is contained in:
陈思海 2026-04-29 14:44:30 +08:00
parent 7d5ae32361
commit 1ba8c9a1c8
26 changed files with 2280 additions and 960 deletions

View File

@ -66,22 +66,12 @@ namespace AlicizaX.UI.Editor
private void OnEnable() private void OnEnable()
{ {
// 先绑定所有 SerializedProperty
InitializeLayoutManagerProperties(); InitializeLayoutManagerProperties();
InitializeScrollerProperties(); InitializeScrollerProperties();
InitializeBaseProperties(); InitializeBaseProperties();
InitializeTemplateProperties(); InitializeTemplateProperties();
// 确保序列化对象是最新的
serializedObject.Update(); serializedObject.Update();
// 如果 layoutManager 的 managedReferenceValue 丢失但有记录的 typeName则尝试恢复实例
RestoreLayoutManagerFromTypeNameIfMissing();
// 如果 scroller 组件丢失但有记录的 typeName则尝试恢复组件到目标 GameObject 上
RestoreScrollerFromTypeNameIfMissing();
// 应用修改(若有)
serializedObject.ApplyModifiedProperties(); serializedObject.ApplyModifiedProperties();
} }
@ -134,6 +124,7 @@ namespace AlicizaX.UI.Editor
serializedObject.Update(); serializedObject.Update();
bool isPlaying = Application.isPlaying; bool isPlaying = Application.isPlaying;
DrawMissingReferenceRepairSection(isPlaying);
DrawLayoutManagerSection(isPlaying); DrawLayoutManagerSection(isPlaying);
DrawBaseSettingsSection(isPlaying); DrawBaseSettingsSection(isPlaying);
DrawScrollerSection(isPlaying); DrawScrollerSection(isPlaying);
@ -144,6 +135,71 @@ namespace AlicizaX.UI.Editor
#endregion #endregion
private void DrawMissingReferenceRepairSection(bool isPlaying)
{
if (isPlaying)
{
return;
}
bool missingLayout = _layoutManager != null &&
_layoutManager.managedReferenceValue == null &&
_layoutManagerTypeName != null &&
!string.IsNullOrEmpty(_layoutManagerTypeName.stringValue);
bool missingScroller = _scroller != null &&
_scroller.objectReferenceValue == null &&
_scrollerTypeName != null &&
!string.IsNullOrEmpty(_scrollerTypeName.stringValue);
RecyclerView recyclerView = target as RecyclerView;
bool missingScrollbarEx = recyclerView != null &&
recyclerView.Scrollbar != null &&
recyclerView.Scrollbar.GetComponent<ScrollbarEx>() == null;
#if UX_NAVIGATION
bool missingNavigationBridge = recyclerView != null &&
recyclerView.GetComponent<RecyclerNavigationBridge>() == null;
#else
bool missingNavigationBridge = false;
#endif
bool templatesNeedRecyclerSelectable = TemplatesNeedRecyclerItemSelectable();
if (!missingLayout && !missingScroller && !missingScrollbarEx && !missingNavigationBridge && !templatesNeedRecyclerSelectable)
{
return;
}
EditorGUILayout.BeginVertical("box");
EditorGUILayout.LabelField("Missing References", EditorStyles.boldLabel);
if (missingLayout && GUILayout.Button("Restore Layout Manager"))
{
RestoreLayoutManagerFromTypeNameIfMissing();
serializedObject.ApplyModifiedProperties();
}
if (missingScroller && GUILayout.Button("Restore Scroller Component"))
{
RestoreScrollerFromTypeNameIfMissing();
serializedObject.ApplyModifiedProperties();
}
if (missingScrollbarEx && GUILayout.Button("Add ScrollbarEx"))
{
Undo.AddComponent<ScrollbarEx>(recyclerView.Scrollbar.gameObject);
EditorUtility.SetDirty(recyclerView.Scrollbar);
}
if (missingNavigationBridge && GUILayout.Button("Add RecyclerNavigationBridge"))
{
Undo.AddComponent<RecyclerNavigationBridge>(recyclerView.gameObject);
EditorUtility.SetDirty(recyclerView);
}
if (templatesNeedRecyclerSelectable && GUILayout.Button("Add RecyclerItemSelectable To Templates"))
{
AddComponentToTemplates<RecyclerItemSelectable>();
}
EditorGUILayout.EndVertical();
}
#region Layout Manager Section #region Layout Manager Section
private void DrawLayoutManagerSection(bool isPlaying) private void DrawLayoutManagerSection(bool isPlaying)
@ -697,6 +753,106 @@ namespace AlicizaX.UI.Editor
return false; return false;
} }
private bool TemplatesNeedComponent<TComponent>() where TComponent : Component
{
if (_templates == null || !_templates.isArray)
{
return false;
}
for (int i = 0; i < _templates.arraySize; i++)
{
SerializedProperty item = _templates.GetArrayElementAtIndex(i);
GameObject template = GetTemplateGameObject(item.objectReferenceValue);
if (template != null && template.GetComponent<TComponent>() == null)
{
return true;
}
}
return false;
}
private bool TemplatesNeedRecyclerItemSelectable()
{
#if UX_NAVIGATION
if (_templates == null || !_templates.isArray)
{
return false;
}
for (int i = 0; i < _templates.arraySize; i++)
{
SerializedProperty item = _templates.GetArrayElementAtIndex(i);
GameObject template = GetTemplateGameObject(item.objectReferenceValue);
if (template == null)
{
continue;
}
ViewHolder holder = template.GetComponent<ViewHolder>();
if (holder != null && RequiresSelection(holder.ItemInteractionFlags) && template.GetComponent<RecyclerItemSelectable>() == null)
{
return true;
}
}
return false;
#else
return false;
#endif
}
private static bool RequiresSelection(ItemInteractionFlags interactionFlags)
{
const ItemInteractionFlags selectionFlags =
ItemInteractionFlags.Select |
ItemInteractionFlags.Deselect |
ItemInteractionFlags.Move |
ItemInteractionFlags.Submit |
ItemInteractionFlags.Cancel;
return (interactionFlags & selectionFlags) != 0;
}
private void AddComponentToTemplates<TComponent>() where TComponent : Component
{
if (_templates == null || !_templates.isArray)
{
return;
}
for (int i = 0; i < _templates.arraySize; i++)
{
SerializedProperty item = _templates.GetArrayElementAtIndex(i);
GameObject template = GetTemplateGameObject(item.objectReferenceValue);
if (template == null || template.GetComponent<TComponent>() != null)
{
continue;
}
Undo.AddComponent<TComponent>(template);
EditorUtility.SetDirty(template);
}
serializedObject.ApplyModifiedProperties();
}
private static GameObject GetTemplateGameObject(Object templateReference)
{
if (templateReference is GameObject gameObject)
{
return gameObject;
}
if (templateReference is Component component)
{
return component.gameObject;
}
return null;
}
#endregion #endregion
#region Helper Methods #region Helper Methods

View File

@ -1,4 +1,4 @@
#if INPUTSYSTEM_SUPPORT && UX_NAVIGATION #if INPUTSYSTEM_SUPPORT && UX_NAVIGATION
using System.Collections.Generic; using System.Collections.Generic;
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
@ -9,6 +9,7 @@ namespace AlicizaX.UI.Extension.Editor
[CustomEditor(typeof(UXNavigationScope))] [CustomEditor(typeof(UXNavigationScope))]
public sealed class UXNavigationScopeEditor : UnityEditor.Editor public sealed class UXNavigationScopeEditor : UnityEditor.Editor
{ {
private readonly List<Selectable> _selectableBuffer = new List<Selectable>(64);
private SerializedProperty _defaultSelectable; private SerializedProperty _defaultSelectable;
private SerializedProperty _bakedSelectables; private SerializedProperty _bakedSelectables;
private SerializedProperty _runtimeSelectableCapacity; private SerializedProperty _runtimeSelectableCapacity;
@ -55,7 +56,7 @@ namespace AlicizaX.UI.Extension.Editor
{ {
using (new EditorGUILayout.HorizontalScope()) using (new EditorGUILayout.HorizontalScope())
{ {
if (GUILayout.Button("收集 Selectable")) if (GUILayout.Button("收集本 Scope Selectable"))
{ {
BakeSelectables(); BakeSelectables();
} }
@ -70,6 +71,19 @@ namespace AlicizaX.UI.Extension.Editor
SortBakedSelectables(); SortBakedSelectables();
} }
} }
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("烘焙当前 Prefab 所有 Scope"))
{
BakeAllScopesInRoot();
}
if (GUILayout.Button("校验当前 Prefab 所有 Scope"))
{
ValidateAllScopesInRoot();
}
}
} }
private void DrawDiagnostics() private void DrawDiagnostics()
@ -105,32 +119,22 @@ namespace AlicizaX.UI.Extension.Editor
EditorGUILayout.HelpBox("烘焙列表存在跨 Scope 引用。", MessageType.Error); EditorGUILayout.HelpBox("烘焙列表存在跨 Scope 引用。", MessageType.Error);
return; return;
} }
for (int j = i + 1; j < _bakedSelectables.arraySize; j++)
{
if (_bakedSelectables.GetArrayElementAtIndex(j).objectReferenceValue == selectable)
{
EditorGUILayout.HelpBox("烘焙列表存在重复引用。", MessageType.Error);
return;
}
}
} }
} }
private void BakeSelectables() private void BakeSelectables()
{ {
UXNavigationScope scope = (UXNavigationScope)target; UXNavigationScope scope = (UXNavigationScope)target;
Selectable[] allSelectables = scope.GetComponentsInChildren<Selectable>(true); BakeScope(scope, serializedObject, _bakedSelectables, _selectableBuffer);
List<Selectable> ownedSelectables = new List<Selectable>(allSelectables.Length);
for (int i = 0; i < allSelectables.Length; i++)
{
Selectable selectable = allSelectables[i];
if (selectable != null && selectable.GetComponentInParent<UXNavigationScope>(true) == scope)
{
ownedSelectables.Add(selectable);
}
}
Undo.RecordObject(scope, "Bake UX Navigation Selectables");
_bakedSelectables.arraySize = ownedSelectables.Count;
for (int i = 0; i < ownedSelectables.Count; i++)
{
_bakedSelectables.GetArrayElementAtIndex(i).objectReferenceValue = ownedSelectables[i];
}
serializedObject.ApplyModifiedProperties();
EditorUtility.SetDirty(scope);
} }
private void RemoveNullEntries() private void RemoveNullEntries()
@ -152,10 +156,85 @@ namespace AlicizaX.UI.Extension.Editor
private void SortBakedSelectables() private void SortBakedSelectables()
{ {
UXNavigationScope scope = (UXNavigationScope)target; UXNavigationScope scope = (UXNavigationScope)target;
List<Selectable> selectables = new List<Selectable>(_bakedSelectables.arraySize); SortScope(scope, serializedObject, _bakedSelectables);
for (int i = 0; i < _bakedSelectables.arraySize; i++) }
private void BakeAllScopesInRoot()
{ {
Selectable selectable = _bakedSelectables.GetArrayElementAtIndex(i).objectReferenceValue as Selectable; GameObject root = GetRootGameObject(((UXNavigationScope)target).gameObject);
UXNavigationScope[] scopes = root.GetComponentsInChildren<UXNavigationScope>(true);
for (int i = 0; i < scopes.Length; i++)
{
UXNavigationScope scope = scopes[i];
SerializedObject scopeObject = new SerializedObject(scope);
SerializedProperty bakedSelectables = scopeObject.FindProperty("_bakedSelectables");
BakeScope(scope, scopeObject, bakedSelectables);
}
}
private void ValidateAllScopesInRoot()
{
GameObject root = GetRootGameObject(((UXNavigationScope)target).gameObject);
UXNavigationScope[] scopes = root.GetComponentsInChildren<UXNavigationScope>(true);
int errorCount = 0;
for (int i = 0; i < scopes.Length; i++)
{
if (!ValidateScope(scopes[i]))
{
errorCount++;
}
}
if (errorCount == 0)
{
Debug.Log("UXNavigationScope validation passed.", root);
}
else
{
Debug.LogErrorFormat(root, "UXNavigationScope validation failed. Error count: {0}", errorCount);
}
}
private static readonly List<Selectable> StaticSelectableBuffer = new List<Selectable>(64);
private static void BakeScope(UXNavigationScope scope, SerializedObject scopeObject, SerializedProperty bakedSelectables)
{
BakeScope(scope, scopeObject, bakedSelectables, StaticSelectableBuffer);
}
private static void BakeScope(UXNavigationScope scope, SerializedObject scopeObject, SerializedProperty bakedSelectables, List<Selectable> scopeEditorBuffer)
{
Selectable[] allSelectables = scope.GetComponentsInChildren<Selectable>(true);
List<Selectable> ownedSelectables = scopeEditorBuffer;
ownedSelectables.Clear();
for (int i = 0; i < allSelectables.Length; i++)
{
Selectable selectable = allSelectables[i];
if (selectable != null && selectable.GetComponentInParent<UXNavigationScope>(true) == scope)
{
ownedSelectables.Add(selectable);
}
}
ownedSelectables.Sort(CompareSiblingPath);
Undo.RecordObject(scope, "Bake UX Navigation Selectables");
bakedSelectables.arraySize = ownedSelectables.Count;
for (int i = 0; i < ownedSelectables.Count; i++)
{
bakedSelectables.GetArrayElementAtIndex(i).objectReferenceValue = ownedSelectables[i];
}
scopeObject.ApplyModifiedProperties();
EditorUtility.SetDirty(scope);
}
private static void SortScope(UXNavigationScope scope, SerializedObject scopeObject, SerializedProperty bakedSelectables)
{
List<Selectable> selectables = StaticSelectableBuffer;
selectables.Clear();
for (int i = 0; i < bakedSelectables.arraySize; i++)
{
Selectable selectable = bakedSelectables.GetArrayElementAtIndex(i).objectReferenceValue as Selectable;
if (selectable != null) if (selectable != null)
{ {
selectables.Add(selectable); selectables.Add(selectable);
@ -164,16 +243,70 @@ namespace AlicizaX.UI.Extension.Editor
selectables.Sort(CompareSiblingPath); selectables.Sort(CompareSiblingPath);
Undo.RecordObject(scope, "Sort UX Navigation Selectables"); Undo.RecordObject(scope, "Sort UX Navigation Selectables");
_bakedSelectables.arraySize = selectables.Count; bakedSelectables.arraySize = selectables.Count;
for (int i = 0; i < selectables.Count; i++) for (int i = 0; i < selectables.Count; i++)
{ {
_bakedSelectables.GetArrayElementAtIndex(i).objectReferenceValue = selectables[i]; bakedSelectables.GetArrayElementAtIndex(i).objectReferenceValue = selectables[i];
} }
serializedObject.ApplyModifiedProperties(); scopeObject.ApplyModifiedProperties();
EditorUtility.SetDirty(scope); EditorUtility.SetDirty(scope);
} }
private static bool ValidateScope(UXNavigationScope scope)
{
SerializedObject scopeObject = new SerializedObject(scope);
SerializedProperty defaultSelectableProperty = scopeObject.FindProperty("_defaultSelectable");
SerializedProperty bakedSelectables = scopeObject.FindProperty("_bakedSelectables");
Selectable defaultSelectable = defaultSelectableProperty.objectReferenceValue as Selectable;
bool valid = true;
if (defaultSelectable != null && defaultSelectable.GetComponentInParent<UXNavigationScope>(true) != scope)
{
Debug.LogError("UXNavigationScope default selectable crosses scope.", scope);
valid = false;
}
for (int i = 0; i < bakedSelectables.arraySize; i++)
{
Selectable selectable = bakedSelectables.GetArrayElementAtIndex(i).objectReferenceValue as Selectable;
if (selectable == null)
{
Debug.LogWarning("UXNavigationScope baked selectables contain null entry.", scope);
valid = false;
continue;
}
if (selectable.GetComponentInParent<UXNavigationScope>(true) != scope)
{
Debug.LogError("UXNavigationScope baked selectable crosses scope.", selectable);
valid = false;
}
for (int j = i + 1; j < bakedSelectables.arraySize; j++)
{
if (bakedSelectables.GetArrayElementAtIndex(j).objectReferenceValue == selectable)
{
Debug.LogError("UXNavigationScope baked selectables contain duplicate entry.", selectable);
valid = false;
}
}
}
return valid;
}
private static GameObject GetRootGameObject(GameObject gameObject)
{
Transform current = gameObject.transform;
while (current.parent != null)
{
current = current.parent;
}
return current.gameObject;
}
private static int CompareSiblingPath(Selectable left, Selectable right) private static int CompareSiblingPath(Selectable left, Selectable right)
{ {
if (left == right) if (left == right)
@ -181,22 +314,67 @@ namespace AlicizaX.UI.Extension.Editor
return 0; return 0;
} }
string leftPath = GetSiblingPath(left.transform); Transform leftTransform = left != null ? left.transform : null;
string rightPath = GetSiblingPath(right.transform); Transform rightTransform = right != null ? right.transform : null;
return string.CompareOrdinal(leftPath, rightPath); return CompareSiblingPath(leftTransform, rightTransform);
} }
private static string GetSiblingPath(Transform transform) private static int CompareSiblingPath(Transform left, Transform right)
{ {
if (transform == null) if (left == right)
{ {
return string.Empty; return 0;
} }
return transform.parent == null if (left == null)
? transform.GetSiblingIndex().ToString("D4") {
: GetSiblingPath(transform.parent) + "/" + transform.GetSiblingIndex().ToString("D4"); return -1;
}
if (right == null)
{
return 1;
}
int leftDepth = GetDepth(left);
int rightDepth = GetDepth(right);
Transform leftCursor = left;
Transform rightCursor = right;
while (leftDepth > rightDepth)
{
leftCursor = leftCursor.parent;
leftDepth--;
}
while (rightDepth > leftDepth)
{
rightCursor = rightCursor.parent;
rightDepth--;
}
while (leftCursor.parent != rightCursor.parent)
{
leftCursor = leftCursor.parent;
rightCursor = rightCursor.parent;
}
return leftCursor.GetSiblingIndex().CompareTo(rightCursor.GetSiblingIndex());
}
private static int GetDepth(Transform transform)
{
int depth = 0;
Transform current = transform;
while (current != null)
{
depth++;
current = current.parent;
}
return depth;
} }
} }
} }
#endif #endif

View File

@ -8,27 +8,18 @@ namespace AlicizaX.UI
void ReleaseAllItemRenders(); void ReleaseAllItemRenders();
} }
public class Adapter<T> : IAdapter, IItemRenderCacheOwner where T : ISimpleViewData internal interface IItemRenderPrewarmer
{ {
private sealed class ItemRenderEntry void PrewarmItemRender(ViewHolder viewHolder, string viewName);
{
public ItemRenderEntry(string viewName, IItemRender itemRender)
{
ViewName = viewName;
ItemRender = itemRender;
}
public string ViewName { get; }
public IItemRender ItemRender { get; }
} }
public class Adapter<T> : IAdapter, IItemRenderCacheOwner, IItemRenderPrewarmer where T : ISimpleViewData
{
protected RecyclerView recyclerView; protected RecyclerView recyclerView;
protected List<T> list; protected List<T> list;
protected int choiceIndex = -1; protected int choiceIndex = -1;
private readonly Dictionary<string, ItemRenderResolver.ItemRenderDefinition> itemRenderDefinitions = new(StringComparer.Ordinal); private readonly Dictionary<string, ItemRenderResolver.ItemRenderDefinition> itemRenderDefinitions = new(StringComparer.Ordinal);
private readonly Dictionary<ViewHolder, ItemRenderEntry> itemRenders = new();
private ItemRenderResolver.ItemRenderDefinition defaultItemRenderDefinition; private ItemRenderResolver.ItemRenderDefinition defaultItemRenderDefinition;
public int ChoiceIndex public int ChoiceIndex
@ -84,9 +75,9 @@ namespace AlicizaX.UI
return; return;
} }
string resolvedViewName = string.IsNullOrEmpty(viewName) ? "<default>" : viewName; #if UNITY_EDITOR || DEVELOPMENT_BUILD
throw new InvalidOperationException( UnityEngine.Debug.LogError("RecyclerView item render is missing.");
$"RecyclerView item render is missing for view '{resolvedViewName}'. Holder='{viewHolder.GetType().Name}', Adapter='{GetType().Name}'."); #endif
} }
public virtual void OnRecycleViewHolder(ViewHolder viewHolder) public virtual void OnRecycleViewHolder(ViewHolder viewHolder)
@ -230,6 +221,41 @@ namespace AlicizaX.UI
return removed; return removed;
} }
public bool UnregisterItemRender(Type itemRenderType)
{
if (itemRenderType == null)
{
return false;
}
bool removed = false;
if (defaultItemRenderDefinition != null && defaultItemRenderDefinition.ItemRenderType == itemRenderType)
{
defaultItemRenderDefinition = null;
ReleaseCachedItemRenders(string.Empty);
removed = true;
}
string removedViewName = null;
foreach (var pair in itemRenderDefinitions)
{
if (pair.Value != null && pair.Value.ItemRenderType == itemRenderType)
{
removedViewName = pair.Key;
break;
}
}
if (removedViewName != null)
{
itemRenderDefinitions.Remove(removedViewName);
ReleaseCachedItemRenders(removedViewName);
removed = true;
}
return removed;
}
public void ClearItemRenderRegistrations() public void ClearItemRenderRegistrations()
{ {
ReleaseAllItemRenders(); ReleaseAllItemRenders();
@ -403,61 +429,47 @@ namespace AlicizaX.UI
return viewHolder != null; return viewHolder != null;
} }
private bool TryGetItemRenderDefinition(string viewName, out ItemRenderResolver.ItemRenderDefinition definition) private static bool TryGetItemRender(ViewHolder viewHolder, out IItemRender itemRender)
{ {
if (!string.IsNullOrEmpty(viewName) && itemRenderDefinitions.TryGetValue(viewName, out definition)) itemRender = viewHolder != null ? viewHolder.CachedItemRender : null;
{ return itemRender != null;
return true;
} }
definition = defaultItemRenderDefinition; private static void ReleaseItemRender(ViewHolder viewHolder)
return definition != null; {
IItemRender itemRender = viewHolder != null ? viewHolder.CachedItemRender : null;
if (itemRender == null)
{
return;
}
itemRender.Unbind();
if (itemRender is ItemRenderBase itemRenderBase)
{
itemRenderBase.Detach();
}
viewHolder.CachedItemRender = null;
viewHolder.CachedItemRenderViewName = null;
} }
private void UpdateSelectionState(ViewHolder viewHolder, bool selected) private void UpdateSelectionState(ViewHolder viewHolder, bool selected)
{ {
if (viewHolder == null) if (TryGetItemRender(viewHolder, out IItemRender itemRender))
{
return;
}
if (TryGetItemRender(viewHolder, out var itemRender))
{ {
itemRender.UpdateSelection(selected); itemRender.UpdateSelection(selected);
return; }
} }
string viewName = string.IsNullOrEmpty(viewHolder.Name) ? "<default>" : viewHolder.Name; private bool TryGetItemRenderDefinition(string viewName, out ItemRenderResolver.ItemRenderDefinition definition)
throw new InvalidOperationException( {
$"RecyclerView item render is missing for selection update. View='{viewName}', Holder='{viewHolder.GetType().Name}', Adapter='{GetType().Name}'."); if (!string.IsNullOrEmpty(viewName) && itemRenderDefinitions.TryGetValue(viewName, out definition))
{
return definition != null;
} }
private bool TryGetItemRender(ViewHolder viewHolder, out IItemRender itemRender) definition = defaultItemRenderDefinition;
{ return definition != null;
if (viewHolder != null &&
itemRenders.TryGetValue(viewHolder, out var entry) &&
entry.ItemRender != null)
{
itemRender = entry.ItemRender;
return true;
}
itemRender = null;
return false;
}
private static void ReleaseItemRender(ItemRenderEntry entry)
{
if (entry?.ItemRender == null)
{
return;
}
entry.ItemRender.Unbind();
if (entry.ItemRender is ItemRenderBase itemRender)
{
itemRender.Detach();
}
} }
private bool TryGetOrCreateItemRender(ViewHolder viewHolder, string viewName, out IItemRender itemRender) private bool TryGetOrCreateItemRender(ViewHolder viewHolder, string viewName, out IItemRender itemRender)
@ -468,17 +480,15 @@ namespace AlicizaX.UI
return false; return false;
} }
if (itemRenders.TryGetValue(viewHolder, out var entry)) if (viewHolder.CachedItemRender != null)
{ {
if (entry.ItemRender != null && string.Equals(entry.ViewName, viewName, StringComparison.Ordinal)) if (string.Equals(viewHolder.CachedItemRenderViewName, viewName, StringComparison.Ordinal))
{ {
itemRender = entry.ItemRender; itemRender = viewHolder.CachedItemRender;
return true; return true;
} }
ReleaseItemRender(entry); ReleaseItemRender(viewHolder);
viewHolder.Destroyed -= OnViewHolderDestroyed;
itemRenders.Remove(viewHolder);
} }
if (!TryGetItemRenderDefinition(viewName, out var definition)) if (!TryGetItemRenderDefinition(viewName, out var definition))
@ -488,8 +498,13 @@ namespace AlicizaX.UI
} }
itemRender = definition.Create(viewHolder, recyclerView, this, SetChoiceIndex); itemRender = definition.Create(viewHolder, recyclerView, this, SetChoiceIndex);
itemRenders[viewHolder] = new ItemRenderEntry(viewName, itemRender); if (itemRender == null)
viewHolder.Destroyed += OnViewHolderDestroyed; {
return false;
}
viewHolder.CachedItemRender = itemRender;
viewHolder.CachedItemRenderViewName = viewName;
return true; return true;
} }
@ -498,64 +513,40 @@ namespace AlicizaX.UI
ReleaseAllItemRenders(); ReleaseAllItemRenders();
} }
private void ReleaseAllItemRenders() void IItemRenderPrewarmer.PrewarmItemRender(ViewHolder viewHolder, string viewName)
{ {
foreach (var pair in itemRenders) TryGetOrCreateItemRender(viewHolder, viewName, out _);
{
ReleaseItemRender(pair.Value);
if (pair.Key != null)
{
pair.Key.Destroyed -= OnViewHolderDestroyed;
}
} }
itemRenders.Clear(); private void ReleaseAllItemRenders()
{
if (recyclerView?.ViewProvider == null)
{
return;
}
for (int i = 0; i < recyclerView.ViewProvider.VisibleCount; i++)
{
ReleaseItemRender(recyclerView.ViewProvider.GetVisibleViewHolder(i));
}
} }
private void ReleaseCachedItemRenders(string viewName) private void ReleaseCachedItemRenders(string viewName)
{ {
if (itemRenders.Count == 0) if (recyclerView?.ViewProvider == null)
{ {
return; return;
} }
List<ViewHolder> viewHoldersToRemove = null; for (int i = 0; i < recyclerView.ViewProvider.VisibleCount; i++)
foreach (var pair in itemRenders)
{ {
if (!string.Equals(pair.Value.ViewName, viewName, StringComparison.Ordinal)) ViewHolder viewHolder = recyclerView.ViewProvider.GetVisibleViewHolder(i);
if (viewHolder == null || !string.Equals(viewHolder.CachedItemRenderViewName, viewName, StringComparison.Ordinal))
{ {
continue; continue;
} }
ReleaseItemRender(pair.Value); ReleaseItemRender(viewHolder);
pair.Key.Destroyed -= OnViewHolderDestroyed;
viewHoldersToRemove ??= new List<ViewHolder>();
viewHoldersToRemove.Add(pair.Key);
}
if (viewHoldersToRemove == null)
{
return;
}
for (int i = 0; i < viewHoldersToRemove.Count; i++)
{
itemRenders.Remove(viewHoldersToRemove[i]);
}
}
private void OnViewHolderDestroyed(ViewHolder viewHolder)
{
if (viewHolder == null)
{
return;
}
viewHolder.Destroyed -= OnViewHolderDestroyed;
if (itemRenders.TryGetValue(viewHolder, out var entry))
{
ReleaseItemRender(entry);
itemRenders.Remove(viewHolder);
} }
} }

View File

@ -7,6 +7,7 @@ namespace AlicizaX.UI
{ {
private readonly List<TData> showList = new(); private readonly List<TData> showList = new();
private readonly string groupViewName; private readonly string groupViewName;
private readonly Dictionary<int, TData> groupsByType = new();
public GroupAdapter(RecyclerView recyclerView, string groupViewName) : base(recyclerView) public GroupAdapter(RecyclerView recyclerView, string groupViewName) : base(recyclerView)
{ {
@ -42,12 +43,16 @@ namespace AlicizaX.UI
{ {
if (string.IsNullOrEmpty(groupViewName)) if (string.IsNullOrEmpty(groupViewName))
{ {
throw new InvalidOperationException("GroupAdapter requires a non-empty groupViewName."); #if UNITY_EDITOR || DEVELOPMENT_BUILD
UnityEngine.Debug.LogError("GroupAdapter requires a non-empty groupViewName.");
#endif
return;
} }
if (list == null) if (list == null)
{ {
showList.Clear(); showList.Clear();
groupsByType.Clear();
base.NotifyDataChanged(); base.NotifyDataChanged();
return; return;
} }
@ -80,22 +85,25 @@ namespace AlicizaX.UI
public override void SetList(List<TData> list) public override void SetList(List<TData> list)
{ {
showList.Clear(); showList.Clear();
groupsByType.Clear();
base.SetList(list); base.SetList(list);
} }
private void CreateGroup(int type) private void CreateGroup(int type)
{ {
var groupData = showList.Find(data => data.Type == type && data.TemplateName == groupViewName); if (groupsByType.ContainsKey(type))
if (groupData == null)
{ {
groupData = new TData return;
}
TData groupData = new TData
{ {
TemplateName = groupViewName, TemplateName = groupViewName,
Type = type Type = type
}; };
groupsByType[type] = groupData;
showList.Add(groupData); showList.Add(groupData);
} }
}
public void Expand(int index) public void Expand(int index)
{ {

View File

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Reflection; using System.Reflection;
using Cysharp.Text;
using UnityEngine.EventSystems; using UnityEngine.EventSystems;
namespace AlicizaX.UI namespace AlicizaX.UI
@ -105,8 +106,6 @@ namespace AlicizaX.UI
/// <summary> /// <summary>
/// 当前持有者上的交互代理组件。 /// 当前持有者上的交互代理组件。
/// </summary> /// </summary>
private ItemInteractionProxy interactionProxy;
/// <summary> /// <summary>
/// 当前项被选中时的回调委托。 /// 当前项被选中时的回调委托。
/// </summary> /// </summary>
@ -170,7 +169,7 @@ namespace AlicizaX.UI
/// <summary> /// <summary>
/// 获取当前渲染项支持的交互能力。 /// 获取当前渲染项支持的交互能力。
/// </summary> /// </summary>
public virtual ItemInteractionFlags InteractionFlags => ItemInteractionFlags.None; public virtual ItemInteractionFlags InteractionFlags => Holder != null ? Holder.ItemInteractionFlags : ItemInteractionFlags.None;
/// <summary> /// <summary>
/// 由框架交互代理读取当前渲染项的交互能力。 /// 由框架交互代理读取当前渲染项的交互能力。
@ -216,12 +215,9 @@ namespace AlicizaX.UI
{ {
ClearSelectionState(); ClearSelectionState();
OnClear(); OnClear();
if (interactionProxy != null) Holder.ClearInteractionHost();
{
interactionProxy.Clear();
interactionBindingActive = false; interactionBindingActive = false;
cachedInteractionFlags = ItemInteractionFlags.None; cachedInteractionFlags = ItemInteractionFlags.None;
}
Holder.DataIndex = -1; Holder.DataIndex = -1;
} }
@ -258,7 +254,7 @@ namespace AlicizaX.UI
CurrentLayoutIndex = Holder.Index; CurrentLayoutIndex = Holder.Index;
Holder.DataIndex = index; Holder.DataIndex = index;
CurrentBindingVersion = Holder.BindingVersion; CurrentBindingVersion = Holder.BindingVersion;
BindInteractionProxyIfNeeded(); BindInteractionHostIfNeeded();
OnBind(itemData, index); OnBind(itemData, index);
} }
@ -363,7 +359,7 @@ namespace AlicizaX.UI
if (viewHolder is not THolder holder) if (viewHolder is not THolder holder)
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
$"RecyclerView item render '{GetType().FullName}' expects holder '{typeof(THolder).FullName}', but got '{viewHolder.GetType().FullName}'."); ZString.Format("RecyclerView item render '{0}' expects holder '{1}', but got '{2}'.", GetType().FullName, typeof(THolder).FullName, viewHolder.GetType().FullName));
} }
Holder = holder; Holder = holder;
@ -372,11 +368,6 @@ namespace AlicizaX.UI
this.selectionHandler = selectionHandler; this.selectionHandler = selectionHandler;
interactionBindingActive = false; interactionBindingActive = false;
cachedInteractionFlags = ItemInteractionFlags.None; cachedInteractionFlags = ItemInteractionFlags.None;
interactionProxy = Holder.GetComponent<ItemInteractionProxy>();
if (interactionProxy == null)
{
interactionProxy = Holder.gameObject.AddComponent<ItemInteractionProxy>();
}
OnHolderAttached(); OnHolderAttached();
} }
@ -391,8 +382,7 @@ namespace AlicizaX.UI
} }
OnHolderDetached(); OnHolderDetached();
interactionProxy?.Clear(); Holder.ClearInteractionHost();
interactionProxy = null;
selectionHandler = null; selectionHandler = null;
Holder = null; Holder = null;
RecyclerView = null; RecyclerView = null;
@ -414,7 +404,7 @@ namespace AlicizaX.UI
if (Holder == null) if (Holder == null)
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
$"RecyclerView item render '{GetType().FullName}' has not been initialized with a holder."); ZString.Format("RecyclerView item render '{0}' has not been initialized with a holder.", GetType().FullName));
} }
} }
@ -435,20 +425,15 @@ namespace AlicizaX.UI
/// <summary> /// <summary>
/// 在需要时将当前渲染实例绑定到交互代理。 /// 在需要时将当前渲染实例绑定到交互代理。
/// </summary> /// </summary>
private void BindInteractionProxyIfNeeded() private void BindInteractionHostIfNeeded()
{ {
if (interactionProxy == null)
{
return;
}
ItemInteractionFlags interactionFlags = InteractionFlags; ItemInteractionFlags interactionFlags = InteractionFlags;
if (interactionBindingActive && cachedInteractionFlags == interactionFlags) if (interactionBindingActive && cachedInteractionFlags == interactionFlags)
{ {
return; return;
} }
interactionProxy.Bind(this); Holder.BindInteractionHost(this);
cachedInteractionFlags = interactionFlags; cachedInteractionFlags = interactionFlags;
interactionBindingActive = true; interactionBindingActive = true;
} }
@ -698,19 +683,23 @@ namespace AlicizaX.UI
{ {
if (viewHolder == null) if (viewHolder == null)
{ {
throw new ArgumentNullException(nameof(viewHolder)); return null;
} }
if (!HolderType.IsInstanceOfType(viewHolder)) if (!HolderType.IsInstanceOfType(viewHolder))
{ {
throw new InvalidOperationException( #if UNITY_EDITOR || DEVELOPMENT_BUILD
$"RecyclerView item render '{ItemRenderType.FullName}' expects holder '{HolderType.FullName}', but got '{viewHolder.GetType().FullName}'."); UnityEngine.Debug.LogError(ZString.Format("RecyclerView item render '{0}' expects holder '{1}', but got '{2}'.", ItemRenderType.FullName, HolderType.FullName, viewHolder.GetType().FullName));
#endif
return null;
} }
if (createInstance() is not ItemRenderBase itemRender) if (createInstance() is not ItemRenderBase itemRender)
{ {
throw new InvalidOperationException( #if UNITY_EDITOR || DEVELOPMENT_BUILD
$"RecyclerView item render '{ItemRenderType.FullName}' could not be created."); UnityEngine.Debug.LogError(ZString.Format("RecyclerView item render '{0}' could not be created.", ItemRenderType.FullName));
#endif
return null;
} }
itemRender.Attach(viewHolder, recyclerView, adapter, selectionHandler); itemRender.Attach(viewHolder, recyclerView, adapter, selectionHandler);
@ -758,13 +747,13 @@ namespace AlicizaX.UI
!typeof(IItemRender).IsAssignableFrom(itemRenderType)) !typeof(IItemRender).IsAssignableFrom(itemRenderType))
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
$"RecyclerView item render type '{itemRenderType.FullName}' is invalid."); ZString.Format("RecyclerView item render type '{0}' is invalid.", itemRenderType.FullName));
} }
if (!TryGetHolderType(itemRenderType, out Type holderType)) if (!TryGetHolderType(itemRenderType, out Type holderType))
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
$"RecyclerView item render '{itemRenderType.FullName}' must inherit from ItemRender<TData, THolder>."); ZString.Format("RecyclerView item render '{0}' must inherit from ItemRender<TData, THolder>.", itemRenderType.FullName));
} }
ConstructorInfo constructor = itemRenderType.GetConstructor( ConstructorInfo constructor = itemRenderType.GetConstructor(
@ -776,7 +765,7 @@ namespace AlicizaX.UI
if (constructor == null) if (constructor == null)
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
$"RecyclerView item render '{itemRenderType.FullName}' must have a parameterless constructor."); ZString.Format("RecyclerView item render '{0}' must have a parameterless constructor.", itemRenderType.FullName));
} }
return new ItemRenderDefinition(itemRenderType, holderType, CreateFactory(constructor)); return new ItemRenderDefinition(itemRenderType, holderType, CreateFactory(constructor));

View File

@ -107,9 +107,15 @@ namespace AlicizaX.UI
public override void DoItemAnimation() public override void DoItemAnimation()
{ {
var viewHolders = viewProvider.ViewHolders; int visibleCount = viewProvider.VisibleCount;
for (int i = 0; i < viewHolders.Count; i++) for (int i = 0; i < visibleCount; i++)
{ {
ViewHolder viewHolder = viewProvider.GetVisibleViewHolder(i);
if (viewHolder == null)
{
continue;
}
float angle = i * intervalAngle + initalAngle; float angle = i * intervalAngle + initalAngle;
angle = circleDirection == CircleDirection.Positive ? angle + ScrollPosition : angle - ScrollPosition; angle = circleDirection == CircleDirection.Positive ? angle + ScrollPosition : angle - ScrollPosition;
float delta = (angle - initalAngle) % 360; float delta = (angle - initalAngle) % 360;
@ -118,7 +124,7 @@ namespace AlicizaX.UI
float scale = delta < intervalAngle ? (1.4f - delta / intervalAngle) : 1; float scale = delta < intervalAngle ? (1.4f - delta / intervalAngle) : 1;
scale = Mathf.Max(scale, 1); scale = Mathf.Max(scale, 1);
viewHolders[i].RectTransform.localScale = Vector3.one * scale; viewHolder.RectTransform.localScale = Vector3.one * scale;
} }
} }
} }

View File

@ -104,8 +104,15 @@ namespace AlicizaX.UI
public void UpdateLayout() public void UpdateLayout()
{ {
foreach (var viewHolder in viewProvider.ViewHolders) int visibleCount = viewProvider.VisibleCount;
for (int i = 0; i < visibleCount; i++)
{ {
ViewHolder viewHolder = viewProvider.GetVisibleViewHolder(i);
if (viewHolder == null)
{
continue;
}
Layout(viewHolder, viewHolder.Index); Layout(viewHolder, viewHolder.Index);
} }
} }

View File

@ -94,14 +94,20 @@ namespace AlicizaX.UI
public override void DoItemAnimation() public override void DoItemAnimation()
{ {
var viewHolders = viewProvider.ViewHolders; int visibleCount = viewProvider.VisibleCount;
for (int i = 0; i < viewHolders.Count; i++) for (int i = 0; i < visibleCount; i++)
{ {
float viewPos = direction == Direction.Vertical ? -viewHolders[i].RectTransform.anchoredPosition.y : viewHolders[i].RectTransform.anchoredPosition.x; ViewHolder viewHolder = viewProvider.GetVisibleViewHolder(i);
if (viewHolder == null)
{
continue;
}
float viewPos = direction == Direction.Vertical ? -viewHolder.RectTransform.anchoredPosition.y : viewHolder.RectTransform.anchoredPosition.x;
float scale = 1 - Mathf.Min(Mathf.Abs(viewPos) * 0.0006f, 1f); float scale = 1 - Mathf.Min(Mathf.Abs(viewPos) * 0.0006f, 1f);
scale = Mathf.Max(scale, minScale); scale = Mathf.Max(scale, minScale);
viewHolders[i].RectTransform.localScale = Vector3.one * scale; viewHolder.RectTransform.localScale = Vector3.one * scale;
} }
} }
} }

View File

@ -8,6 +8,7 @@ namespace AlicizaX.UI
private const int DEFAULT_MAX_SIZE_PER_TYPE = 10; private const int DEFAULT_MAX_SIZE_PER_TYPE = 10;
private readonly Dictionary<string, Stack<T>> entries; private readonly Dictionary<string, Stack<T>> entries;
private readonly Dictionary<T, string> activeEntries;
private readonly Dictionary<string, int> typeSize; private readonly Dictionary<string, int> typeSize;
private readonly Dictionary<string, int> activeCountByType; private readonly Dictionary<string, int> activeCountByType;
private readonly Dictionary<string, int> peakActiveByType; private readonly Dictionary<string, int> peakActiveByType;
@ -17,6 +18,7 @@ namespace AlicizaX.UI
private int hitCount; private int hitCount;
private int missCount; private int missCount;
private int destroyCount; private int destroyCount;
private bool disposed;
public MixedObjectPool(IMixedObjectFactory<T> factory) : this(factory, DEFAULT_MAX_SIZE_PER_TYPE) public MixedObjectPool(IMixedObjectFactory<T> factory) : this(factory, DEFAULT_MAX_SIZE_PER_TYPE)
{ {
@ -33,6 +35,7 @@ namespace AlicizaX.UI
} }
entries = new Dictionary<string, Stack<T>>(StringComparer.Ordinal); entries = new Dictionary<string, Stack<T>>(StringComparer.Ordinal);
activeEntries = new Dictionary<T, string>();
typeSize = new Dictionary<string, int>(StringComparer.Ordinal); typeSize = new Dictionary<string, int>(StringComparer.Ordinal);
activeCountByType = new Dictionary<string, int>(StringComparer.Ordinal); activeCountByType = new Dictionary<string, int>(StringComparer.Ordinal);
peakActiveByType = new Dictionary<string, int>(StringComparer.Ordinal); peakActiveByType = new Dictionary<string, int>(StringComparer.Ordinal);
@ -40,18 +43,26 @@ namespace AlicizaX.UI
public T Allocate(string typeName) public T Allocate(string typeName)
{ {
if (disposed)
{
return null;
}
Stack<T> stack = GetOrCreateStack(typeName); Stack<T> stack = GetOrCreateStack(typeName);
if (stack.Count > 0) if (stack.Count > 0)
{ {
T obj = stack.Pop(); T obj = stack.Pop();
hitCount++; hitCount++;
activeEntries[obj] = typeName;
TrackAllocate(typeName); TrackAllocate(typeName);
return obj; return obj;
} }
missCount++; missCount++;
T created = factory.Create(typeName);
activeEntries[created] = typeName;
TrackAllocate(typeName); TrackAllocate(typeName);
return factory.Create(typeName); return created;
} }
public void Free(string typeName, T obj) public void Free(string typeName, T obj)
@ -60,6 +71,16 @@ namespace AlicizaX.UI
if (!factory.Validate(typeName, obj)) if (!factory.Validate(typeName, obj))
{ {
activeEntries.Remove(obj);
factory.Destroy(typeName, obj);
destroyCount++;
TrackFree(typeName);
return;
}
if (disposed)
{
activeEntries.Remove(obj);
factory.Destroy(typeName, obj); factory.Destroy(typeName, obj);
destroyCount++; destroyCount++;
TrackFree(typeName); TrackFree(typeName);
@ -70,6 +91,7 @@ namespace AlicizaX.UI
Stack<T> stack = GetOrCreateStack(typeName); Stack<T> stack = GetOrCreateStack(typeName);
factory.Reset(typeName, obj); factory.Reset(typeName, obj);
activeEntries.Remove(obj);
TrackFree(typeName); TrackFree(typeName);
if (stack.Count >= maxSize) if (stack.Count >= maxSize)
@ -99,6 +121,11 @@ namespace AlicizaX.UI
public void EnsureCapacity(string typeName, int value) public void EnsureCapacity(string typeName, int value)
{ {
if (disposed)
{
return;
}
if (value <= 0) if (value <= 0)
{ {
throw new ArgumentOutOfRangeException(nameof(value)); throw new ArgumentOutOfRangeException(nameof(value));
@ -113,6 +140,11 @@ namespace AlicizaX.UI
public void Warm(string typeName, int count) public void Warm(string typeName, int count)
{ {
if (disposed)
{
return;
}
if (count <= 0) if (count <= 0)
{ {
return; return;
@ -169,8 +201,14 @@ namespace AlicizaX.UI
peakActiveByType.Clear(); peakActiveByType.Clear();
} }
public void ClearInactive()
{
Clear();
}
public void Dispose() public void Dispose()
{ {
disposed = true;
Clear(); Clear();
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }

View File

@ -6,6 +6,7 @@ namespace AlicizaX.UI
public class ObjectPool<T> : IObjectPool<T> where T : class public class ObjectPool<T> : IObjectPool<T> where T : class
{ {
private readonly Stack<T> entries; private readonly Stack<T> entries;
private readonly HashSet<T> activeEntries;
private readonly int initialSize; private readonly int initialSize;
private int maxSize; private int maxSize;
protected readonly IObjectFactory<T> factory; protected readonly IObjectFactory<T> factory;
@ -15,6 +16,7 @@ namespace AlicizaX.UI
private int missCount; private int missCount;
private int destroyCount; private int destroyCount;
private int peakActive; private int peakActive;
private bool disposed;
public ObjectPool(IObjectFactory<T> factory) : this(factory, Environment.ProcessorCount * 2) public ObjectPool(IObjectFactory<T> factory) : this(factory, Environment.ProcessorCount * 2)
{ {
@ -36,6 +38,7 @@ namespace AlicizaX.UI
} }
entries = new Stack<T>(maxSize); entries = new Stack<T>(maxSize);
activeEntries = new HashSet<T>();
Warm(initialSize); Warm(initialSize);
} }
@ -59,6 +62,11 @@ namespace AlicizaX.UI
public virtual T Allocate() public virtual T Allocate()
{ {
if (disposed)
{
return null;
}
T value; T value;
if (entries.Count > 0) if (entries.Count > 0)
{ {
@ -67,6 +75,7 @@ namespace AlicizaX.UI
{ {
hitCount++; hitCount++;
activeCount++; activeCount++;
activeEntries.Add(value);
if (activeCount > peakActive) if (activeCount > peakActive)
{ {
peakActive = activeCount; peakActive = activeCount;
@ -79,6 +88,7 @@ namespace AlicizaX.UI
value = factory.Create(); value = factory.Create();
totalCount++; totalCount++;
activeCount++; activeCount++;
activeEntries.Add(value);
if (activeCount > peakActive) if (activeCount > peakActive)
{ {
peakActive = activeCount; peakActive = activeCount;
@ -93,6 +103,7 @@ namespace AlicizaX.UI
if (!factory.Validate(obj)) if (!factory.Validate(obj))
{ {
activeEntries.Remove(obj);
factory.Destroy(obj); factory.Destroy(obj);
destroyCount++; destroyCount++;
if (totalCount > 0) if (totalCount > 0)
@ -108,12 +119,29 @@ namespace AlicizaX.UI
return; return;
} }
if (disposed)
{
activeEntries.Remove(obj);
factory.Destroy(obj);
destroyCount++;
if (totalCount > 0)
{
totalCount--;
}
if (activeCount > 0)
{
activeCount--;
}
return;
}
factory.Reset(obj); factory.Reset(obj);
if (activeCount > 0) if (activeCount > 0)
{ {
activeCount--; activeCount--;
} }
activeEntries.Remove(obj);
if (entries.Count < maxSize) if (entries.Count < maxSize)
{ {
@ -151,17 +179,29 @@ namespace AlicizaX.UI
} }
} }
activeCount = activeEntries.Count;
totalCount = activeCount; totalCount = activeCount;
} }
public void ClearInactive()
{
Clear();
}
public void Dispose() public void Dispose()
{ {
disposed = true;
Clear(); Clear();
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
public void EnsureCapacity(int value) public void EnsureCapacity(int value)
{ {
if (disposed)
{
return;
}
if (value <= 0) if (value <= 0)
{ {
throw new ArgumentOutOfRangeException(nameof(value)); throw new ArgumentOutOfRangeException(nameof(value));
@ -175,6 +215,11 @@ namespace AlicizaX.UI
public void Warm(int count) public void Warm(int count)
{ {
if (disposed)
{
return;
}
if (count <= 0) if (count <= 0)
{ {
return; return;

View File

@ -16,6 +16,11 @@ namespace AlicizaX.UI
public T Create() public T Create()
{ {
T obj = Object.Instantiate(template, parent); T obj = Object.Instantiate(template, parent);
if (obj is ViewHolder viewHolder)
{
viewHolder.RefreshInteractionCache();
}
return obj; return obj;
} }

View File

@ -50,6 +50,11 @@ namespace AlicizaX.UI
obj.transform.localScale = Vector3.one; obj.transform.localScale = Vector3.one;
} }
if (obj is ViewHolder viewHolder)
{
viewHolder.RefreshInteractionCache();
}
return obj; return obj;
} }

File diff suppressed because it is too large Load Diff

View File

@ -7,8 +7,9 @@ namespace AlicizaX.UI
{ {
private Vector2 centerPosition; private Vector2 centerPosition;
private void Awake() protected override void Awake()
{ {
base.Awake();
RectTransform rectTransform = GetComponent<RectTransform>(); RectTransform rectTransform = GetComponent<RectTransform>();
Vector2 position = transform.position; Vector2 position = transform.position;
Vector2 size = rectTransform.sizeDelta; Vector2 size = rectTransform.sizeDelta;

View File

@ -14,11 +14,13 @@ namespace AlicizaX.UI
private bool dragging; private bool dragging;
private bool hovering; private bool hovering;
private float targetHandleScale = 1f;
private void Awake() private void Awake()
{ {
scrollbar = GetComponent<Scrollbar>(); scrollbar = GetComponent<Scrollbar>();
handle = scrollbar.handleRect; handle = scrollbar.handleRect;
targetHandleScale = GetCurrentHandleScale();
} }
public void OnBeginDrag(PointerEventData eventData) public void OnBeginDrag(PointerEventData eventData)
@ -31,23 +33,7 @@ namespace AlicizaX.UI
dragging = false; dragging = false;
if (!hovering) if (!hovering)
{ {
if (scrollbar.direction == Scrollbar.Direction.TopToBottom || SetHandleScale(1f);
scrollbar.direction == Scrollbar.Direction.BottomToTop)
{
#if PRIMETWEEN_SUPPORT
PrimeTween.Tween.ScaleX(handle, 1f, 0.2f);
#else
handle.localScale = new Vector3(1,handle.localScale.y,handle.localScale.z);
#endif
}
else
{
#if PRIMETWEEN_SUPPORT
PrimeTween.Tween.ScaleY(handle, 1f, 0.2f);
#else
handle.localScale = new Vector3(handle.localScale.x,1,handle.localScale.z);
#endif
}
} }
OnDragEnd?.Invoke(); OnDragEnd?.Invoke();
@ -56,24 +42,7 @@ namespace AlicizaX.UI
public void OnPointerEnter(PointerEventData eventData) public void OnPointerEnter(PointerEventData eventData)
{ {
hovering = true; hovering = true;
if (scrollbar.direction == Scrollbar.Direction.TopToBottom || SetHandleScale(2f);
scrollbar.direction == Scrollbar.Direction.BottomToTop)
{
#if PRIMETWEEN_SUPPORT
PrimeTween.Tween.ScaleX(handle, 2f, 0.2f);
#else
handle.localScale = new Vector3(2,handle.localScale.y,handle.localScale.z);
#endif
}
else
{
#if PRIMETWEEN_SUPPORT
PrimeTween.Tween.ScaleY(handle, 2f, 0.2f);
#else
handle.localScale = new Vector3(handle.localScale.x, 2, handle.localScale.z);
#endif
}
} }
public void OnPointerExit(PointerEventData eventData) public void OnPointerExit(PointerEventData eventData)
@ -81,24 +50,58 @@ namespace AlicizaX.UI
hovering = false; hovering = false;
if (!dragging) if (!dragging)
{ {
if (scrollbar.direction == Scrollbar.Direction.TopToBottom || SetHandleScale(1f);
scrollbar.direction == Scrollbar.Direction.BottomToTop) }
}
private void SetHandleScale(float target)
{ {
if (handle == null || Mathf.Approximately(targetHandleScale, target))
{
return;
}
targetHandleScale = target;
bool vertical = IsVerticalScrollbar();
#if PRIMETWEEN_SUPPORT #if PRIMETWEEN_SUPPORT
PrimeTween.Tween.ScaleX(handle, 1f, 0.2f); if (vertical)
#else {
handle.localScale = new Vector3(1,handle.localScale.y,handle.localScale.z); PrimeTween.Tween.ScaleX(handle, target, 0.2f);
#endif
} }
else else
{ {
#if PRIMETWEEN_SUPPORT PrimeTween.Tween.ScaleY(handle, target, 0.2f);
PrimeTween.Tween.ScaleY(handle, 1f, 0.2f); }
#else #else
handle.localScale = new Vector3(handle.localScale.x,1,handle.localScale.z); Vector3 scale = handle.localScale;
if (vertical)
{
scale.x = target;
}
else
{
scale.y = target;
}
handle.localScale = scale;
#endif #endif
} }
private float GetCurrentHandleScale()
{
if (handle == null)
{
return 1f;
} }
return IsVerticalScrollbar() ? handle.localScale.x : handle.localScale.y;
}
private bool IsVerticalScrollbar()
{
return scrollbar != null &&
(scrollbar.direction == Scrollbar.Direction.TopToBottom ||
scrollbar.direction == Scrollbar.Direction.BottomToTop);
} }
} }
} }

View File

@ -1,4 +1,3 @@
using System.Collections;
using UnityEngine; using UnityEngine;
using UnityEngine.EventSystems; using UnityEngine.EventSystems;
@ -6,16 +5,27 @@ namespace AlicizaX.UI
{ {
public class Scroller : MonoBehaviour, IScroller, IBeginDragHandler, IEndDragHandler, IDragHandler, IScrollHandler public class Scroller : MonoBehaviour, IScroller, IBeginDragHandler, IEndDragHandler, IDragHandler, IScrollHandler
{ {
private static readonly WaitForEndOfFrame EndOfFrameYield = new(); protected enum MotionState
private Coroutine movementCoroutine; {
Idle,
Smooth,
Duration,
Inertia
}
protected float position; protected float position;
public float Position { get => position; set => position = value; }
public float Position
{
get => position;
set => position = value;
}
protected float velocity; protected float velocity;
public float Velocity => velocity; public float Velocity => velocity;
protected Direction direction; protected Direction direction;
public Direction Direction public Direction Direction
{ {
get => direction; get => direction;
@ -23,6 +33,7 @@ namespace AlicizaX.UI
} }
protected Vector2 contentSize; protected Vector2 contentSize;
public Vector2 ContentSize public Vector2 ContentSize
{ {
get => contentSize; get => contentSize;
@ -30,6 +41,7 @@ namespace AlicizaX.UI
} }
protected Vector2 viewSize; protected Vector2 viewSize;
public Vector2 ViewSize public Vector2 ViewSize
{ {
get => viewSize; get => viewSize;
@ -37,6 +49,7 @@ namespace AlicizaX.UI
} }
protected float scrollSpeed = 1f; protected float scrollSpeed = 1f;
public float ScrollSpeed public float ScrollSpeed
{ {
get => scrollSpeed; get => scrollSpeed;
@ -44,6 +57,7 @@ namespace AlicizaX.UI
} }
protected float wheelSpeed = 30f; protected float wheelSpeed = 30f;
public float WheelSpeed public float WheelSpeed
{ {
get => wheelSpeed; get => wheelSpeed;
@ -51,6 +65,7 @@ namespace AlicizaX.UI
} }
protected bool snap; protected bool snap;
public bool Snap public bool Snap
{ {
get => snap; get => snap;
@ -61,20 +76,72 @@ namespace AlicizaX.UI
protected MoveStopEvent moveStopEvent = new(); protected MoveStopEvent moveStopEvent = new();
protected DraggingEvent draggingEvent = new(); protected DraggingEvent draggingEvent = new();
public float MaxPosition => direction == Direction.Vertical ? private MotionState motionState;
Mathf.Max(contentSize.y - viewSize.y, 0) : private float motionStartPosition;
Mathf.Max(contentSize.x - viewSize.x, 0); private float motionTargetPosition;
private float motionElapsed;
private float motionDuration;
private float motionSpeed;
private float inertiaVelocity;
public float MaxPosition => direction == Direction.Vertical ? Mathf.Max(contentSize.y - viewSize.y, 0) : Mathf.Max(contentSize.x - viewSize.x, 0);
public float ViewLength => direction == Direction.Vertical ? viewSize.y : viewSize.x; public float ViewLength => direction == Direction.Vertical ? viewSize.y : viewSize.x;
public ScrollerEvent OnValueChanged { get => scrollerEvent; set => scrollerEvent = value; } public ScrollerEvent OnValueChanged
{
get => scrollerEvent;
set => scrollerEvent = value;
}
public MoveStopEvent OnMoveStoped { get => moveStopEvent; set => moveStopEvent = value; } public MoveStopEvent OnMoveStoped
{
get => moveStopEvent;
set => moveStopEvent = value;
}
public DraggingEvent OnDragging { get => draggingEvent; set => draggingEvent = value; } public DraggingEvent OnDragging
{
get => draggingEvent;
set => draggingEvent = value;
}
public float dragStopTime = 0f; public float dragStopTime = 0f;
public bool InputEnabled { get; set; } = true;
protected virtual void Awake()
{
}
protected virtual void Update()
{
if (motionState == MotionState.Idle)
{
return;
}
TickMotion(Time.deltaTime);
}
private void TickMotion(float deltaTime)
{
switch (motionState)
{
case MotionState.Smooth:
TickSmooth(deltaTime);
break;
case MotionState.Duration:
TickDuration(deltaTime);
break;
case MotionState.Inertia:
TickInertia(deltaTime);
break;
default:
motionState = MotionState.Idle;
break;
}
}
public virtual void ScrollTo(float position, bool smooth = false) public virtual void ScrollTo(float position, bool smooth = false)
{ {
@ -86,11 +153,10 @@ namespace AlicizaX.UI
{ {
this.position = position; this.position = position;
OnValueChanged?.Invoke(this.position); OnValueChanged?.Invoke(this.position);
return;
} }
else
{ StartPositionMotion(position, scrollSpeed);
movementCoroutine = StartCoroutine(RunMotion(MoveTo(position)));
}
} }
public virtual void ScrollToDuration(float position, float duration) public virtual void ScrollToDuration(float position, float duration)
@ -108,7 +174,11 @@ namespace AlicizaX.UI
return; return;
} }
movementCoroutine = StartCoroutine(RunMotion(ToPositionByDuration(position, duration))); motionState = MotionState.Duration;
motionStartPosition = this.position;
motionTargetPosition = position;
motionDuration = Mathf.Max(duration, 0.0001f);
motionElapsed = 0f;
} }
public virtual void ScrollToRatio(float ratio) public virtual void ScrollToRatio(float ratio)
@ -118,12 +188,22 @@ namespace AlicizaX.UI
public void OnBeginDrag(PointerEventData eventData) public void OnBeginDrag(PointerEventData eventData)
{ {
if (!InputEnabled)
{
return;
}
OnDragging?.Invoke(true); OnDragging?.Invoke(true);
StopMovement(); StopMovement();
} }
public void OnEndDrag(PointerEventData eventData) public void OnEndDrag(PointerEventData eventData)
{ {
if (!InputEnabled)
{
return;
}
Inertia(); Inertia();
Elastic(); Elastic();
OnDragging?.Invoke(false); OnDragging?.Invoke(false);
@ -131,6 +211,11 @@ namespace AlicizaX.UI
public void OnDrag(PointerEventData eventData) public void OnDrag(PointerEventData eventData)
{ {
if (!InputEnabled)
{
return;
}
dragStopTime = Time.time; dragStopTime = Time.time;
velocity = GetDelta(eventData); velocity = GetDelta(eventData);
@ -141,6 +226,11 @@ namespace AlicizaX.UI
public void OnScroll(PointerEventData eventData) public void OnScroll(PointerEventData eventData)
{ {
if (!InputEnabled)
{
return;
}
StopMovement(); StopMovement();
float rate = GetScrollRate() * wheelSpeed; float rate = GetScrollRate() * wheelSpeed;
@ -158,32 +248,41 @@ namespace AlicizaX.UI
return direction == Direction.Vertical ? eventData.delta.y * rate : -eventData.delta.x * rate; return direction == Direction.Vertical ? eventData.delta.y * rate : -eventData.delta.x * rate;
} }
private float GetScrollRate() protected float GetScrollRate()
{ {
float rate = 1f; float rate = 1f;
float viewLength = ViewLength;
if (viewLength <= 0f)
{
return rate;
}
if (position < 0) if (position < 0)
{ {
rate = Mathf.Max(0, 1 - (Mathf.Abs(position) / ViewLength)); rate = Mathf.Max(0, 1 - (Mathf.Abs(position) / viewLength));
} }
else if (position > MaxPosition) else if (position > MaxPosition)
{ {
rate = Mathf.Max(0, 1 - (Mathf.Abs(position - MaxPosition) / ViewLength)); rate = Mathf.Max(0, 1 - (Mathf.Abs(position - MaxPosition) / viewLength));
} }
return rate; return rate;
} }
protected virtual void Inertia() protected virtual void Inertia()
{ {
if (Mathf.Abs(velocity) <= 0.1f)
{
CompleteMotion(true);
return;
}
if (Mathf.Abs(velocity) > 0.1f)
{
StopMovement(); StopMovement();
movementCoroutine = StartCoroutine(RunMotion(InertiaTo())); motionState = MotionState.Inertia;
} motionStartPosition = position;
else motionElapsed = 0f;
{ motionDuration = snap ? 0.1f : 1f;
OnMoveStoped?.Invoke(); inertiaVelocity = velocity > 0 ? Mathf.Min(velocity, 100) : Mathf.Max(velocity, -100);
}
} }
protected virtual void Elastic() protected virtual void Elastic()
@ -191,98 +290,105 @@ namespace AlicizaX.UI
if (position < 0) if (position < 0)
{ {
StopMovement(); StopMovement();
movementCoroutine = StartCoroutine(RunMotion(ElasticTo(0))); StartPositionMotion(0, 7f);
} }
else if (position > MaxPosition) else if (position > MaxPosition)
{ {
StopMovement(); StopMovement();
movementCoroutine = StartCoroutine(RunMotion(ElasticTo(MaxPosition))); StartPositionMotion(MaxPosition, 7f);
} }
} }
IEnumerator InertiaTo() protected void StopMovement()
{ {
float timer = 0f; motionState = MotionState.Idle;
float p = position; }
float v = velocity > 0 ? Mathf.Min(velocity, 100) : Mathf.Max(velocity, -100);
float duration = snap ? 0.1f : 1f; private void StartPositionMotion(float targetPosition, float speed)
while (timer < duration)
{ {
float y = (float)EaseUtil.EaseOutCirc(timer) * 40; motionState = MotionState.Smooth;
timer += Time.deltaTime; motionStartPosition = position;
position = p + y * v; motionTargetPosition = targetPosition;
motionElapsed = Time.deltaTime;
Elastic(); motionSpeed = speed;
}
private void TickSmooth(float deltaTime)
{
if (Mathf.Abs(motionTargetPosition - position) <= 0.1f)
{
position = motionTargetPosition;
OnValueChanged?.Invoke(position); OnValueChanged?.Invoke(position);
CompleteMotion(false);
yield return EndOfFrameYield;
}
OnMoveStoped?.Invoke();
}
IEnumerator ElasticTo(float targetPos)
{
yield return ToPosition(targetPos, 7);
}
IEnumerator MoveTo(float targetPos)
{
yield return ToPosition(targetPos, scrollSpeed);
}
IEnumerator ToPositionByDuration(float targetPos, float duration)
{
duration = Mathf.Max(duration, 0.0001f);
float startPos = position;
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float t = Mathf.Clamp01(elapsed / duration);
position = Mathf.Lerp(startPos, targetPos, t);
OnValueChanged?.Invoke(position);
yield return EndOfFrameYield;
}
position = targetPos;
OnValueChanged?.Invoke(position);
}
IEnumerator ToPosition(float targetPos, float speed)
{
float startPos = position;
float time = Time.deltaTime;
while (Mathf.Abs(targetPos - position) > 0.1f)
{
position = Mathf.Lerp(startPos, targetPos, time * speed);
OnValueChanged?.Invoke(position);
time += Time.deltaTime;
yield return EndOfFrameYield;
}
position = targetPos;
OnValueChanged?.Invoke(position);
}
private IEnumerator RunMotion(IEnumerator motion)
{
yield return motion;
movementCoroutine = null;
}
private void StopMovement()
{
if (movementCoroutine == null)
{
return; return;
} }
StopCoroutine(movementCoroutine); position = Mathf.Lerp(motionStartPosition, motionTargetPosition, motionElapsed * motionSpeed);
movementCoroutine = null; motionElapsed += deltaTime;
OnValueChanged?.Invoke(position);
}
private void TickDuration(float deltaTime)
{
motionElapsed += deltaTime;
float t = Mathf.Clamp01(motionElapsed / motionDuration);
position = Mathf.Lerp(motionStartPosition, motionTargetPosition, t);
OnValueChanged?.Invoke(position);
if (t >= 1f)
{
position = motionTargetPosition;
OnValueChanged?.Invoke(position);
CompleteMotion(false);
} }
} }
private void TickInertia(float deltaTime)
{
motionElapsed += deltaTime;
float t = Mathf.Clamp01(motionElapsed / motionDuration);
float y = (float)EaseUtil.EaseOutCirc(t) * 40f;
float nextPosition = motionStartPosition + y * inertiaVelocity;
float maxPosition = MaxPosition;
if (nextPosition < 0f)
{
position = 0f;
OnValueChanged?.Invoke(position);
StopMovement();
StartPositionMotion(0f, 7f);
return;
}
if (nextPosition > maxPosition)
{
position = maxPosition;
OnValueChanged?.Invoke(position);
StopMovement();
StartPositionMotion(maxPosition, 7f);
return;
}
position = nextPosition;
OnValueChanged?.Invoke(position);
if (t >= 1f)
{
CompleteMotion(true);
}
}
private void CompleteMotion(bool notifyStopped)
{
motionState = MotionState.Idle;
velocity = 0f;
if (notifyStopped)
{
OnMoveStoped?.Invoke();
}
}
}
} }

View File

@ -165,7 +165,7 @@ namespace AlicizaX.UI
public bool UnregisterItemRender(Type itemRenderType) public bool UnregisterItemRender(Type itemRenderType)
{ {
return UnregisterItemRender(nameof(itemRenderType)); return _adapter.UnregisterItemRender(itemRenderType);
} }

View File

@ -1,11 +1,11 @@
using UnityEngine; using UnityEngine;
using UnityEngine.EventSystems; using UnityEngine.EventSystems;
using UnityEngine.UI; using UnityEngine.UI;
using Cysharp.Text;
namespace AlicizaX.UI namespace AlicizaX.UI
{ {
[DisallowMultipleComponent] public abstract partial class ViewHolder :
public sealed class ItemInteractionProxy : MonoBehaviour,
IPointerClickHandler, IPointerClickHandler,
IPointerEnterHandler, IPointerEnterHandler,
IPointerExitHandler, IPointerExitHandler,
@ -23,22 +23,31 @@ namespace AlicizaX.UI
ICancelHandler ICancelHandler
#endif #endif
{ {
private IItemInteractionHost host; [SerializeField]
private ItemInteractionFlags flags; private ItemInteractionFlags itemInteractionFlags = ItemInteractionFlags.None;
private Selectable focusAnchor;
private IItemInteractionHost interactionHost;
private ItemInteractionFlags activeInteractionFlags;
private RecyclerItemSelectable ownedSelectable; private RecyclerItemSelectable ownedSelectable;
private Scroller parentScroller; private Scroller parentScroller;
private bool missingSelectableLogged;
internal void Bind(IItemInteractionHost interactionHost) public ItemInteractionFlags ItemInteractionFlags
{ {
host = interactionHost; get => itemInteractionFlags;
flags = interactionHost?.InteractionFlags ?? ItemInteractionFlags.None; set => itemInteractionFlags = value;
parentScroller = GetComponentInParent<Scroller>(); }
internal void BindInteractionHost(IItemInteractionHost host)
{
interactionHost = host;
activeInteractionFlags = host?.InteractionFlags ?? itemInteractionFlags;
parentScroller = RecyclerView != null ? RecyclerView.Scroller : null;
EnsureFocusAnchor(); EnsureFocusAnchor();
if (ownedSelectable != null) if (ownedSelectable != null)
{ {
bool requiresSelection = RequiresSelection(flags); bool requiresSelection = RequiresSelection(activeInteractionFlags);
ownedSelectable.interactable = requiresSelection; ownedSelectable.interactable = requiresSelection;
ownedSelectable.enabled = requiresSelection; ownedSelectable.enabled = requiresSelection;
} }
@ -46,10 +55,10 @@ namespace AlicizaX.UI
InvalidateNavigationScope(); InvalidateNavigationScope();
} }
public void Clear() internal void ClearInteractionHost()
{ {
host = null; interactionHost = null;
flags = ItemInteractionFlags.None; activeInteractionFlags = ItemInteractionFlags.None;
parentScroller = null; parentScroller = null;
if (ownedSelectable != null) if (ownedSelectable != null)
@ -61,45 +70,27 @@ namespace AlicizaX.UI
InvalidateNavigationScope(); InvalidateNavigationScope();
} }
public Selectable GetSelectable()
{
EnsureFocusAnchor();
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) public void OnPointerClick(PointerEventData eventData)
{ {
if ((flags & ItemInteractionFlags.PointerClick) != 0) if ((activeInteractionFlags & ItemInteractionFlags.PointerClick) != 0)
{ {
host?.HandlePointerClick(eventData); interactionHost?.HandlePointerClick(eventData);
} }
} }
public void OnPointerEnter(PointerEventData eventData) public void OnPointerEnter(PointerEventData eventData)
{ {
if ((flags & ItemInteractionFlags.PointerEnter) != 0) if ((activeInteractionFlags & ItemInteractionFlags.PointerEnter) != 0)
{ {
host?.HandlePointerEnter(eventData); interactionHost?.HandlePointerEnter(eventData);
} }
} }
public void OnPointerExit(PointerEventData eventData) public void OnPointerExit(PointerEventData eventData)
{ {
if ((flags & ItemInteractionFlags.PointerExit) != 0) if ((activeInteractionFlags & ItemInteractionFlags.PointerExit) != 0)
{ {
host?.HandlePointerExit(eventData); interactionHost?.HandlePointerExit(eventData);
} }
} }
@ -109,9 +100,9 @@ namespace AlicizaX.UI
#if INPUTSYSTEM_SUPPORT #if INPUTSYSTEM_SUPPORT
UXNavigationRuntime.NotifySelection(gameObject); UXNavigationRuntime.NotifySelection(gameObject);
#endif #endif
if ((flags & ItemInteractionFlags.Select) != 0) if ((activeInteractionFlags & ItemInteractionFlags.Select) != 0)
{ {
host?.HandleSelect(eventData); interactionHost?.HandleSelect(eventData);
} }
#endif #endif
} }
@ -119,9 +110,9 @@ namespace AlicizaX.UI
public void OnDeselect(BaseEventData eventData) public void OnDeselect(BaseEventData eventData)
{ {
#if UX_NAVIGATION #if UX_NAVIGATION
if ((flags & ItemInteractionFlags.Deselect) != 0) if ((activeInteractionFlags & ItemInteractionFlags.Deselect) != 0)
{ {
host?.HandleDeselect(eventData); interactionHost?.HandleDeselect(eventData);
} }
#endif #endif
} }
@ -129,18 +120,18 @@ namespace AlicizaX.UI
public void OnMove(AxisEventData eventData) public void OnMove(AxisEventData eventData)
{ {
#if UX_NAVIGATION #if UX_NAVIGATION
if ((flags & ItemInteractionFlags.Move) != 0) if ((activeInteractionFlags & ItemInteractionFlags.Move) != 0)
{ {
host?.HandleMove(eventData); interactionHost?.HandleMove(eventData);
} }
#endif #endif
} }
public void OnBeginDrag(PointerEventData eventData) public void OnBeginDrag(PointerEventData eventData)
{ {
if ((flags & ItemInteractionFlags.BeginDrag) != 0) if ((activeInteractionFlags & ItemInteractionFlags.BeginDrag) != 0)
{ {
host?.HandleBeginDrag(eventData); interactionHost?.HandleBeginDrag(eventData);
return; return;
} }
@ -149,9 +140,9 @@ namespace AlicizaX.UI
public void OnDrag(PointerEventData eventData) public void OnDrag(PointerEventData eventData)
{ {
if ((flags & ItemInteractionFlags.Drag) != 0) if ((activeInteractionFlags & ItemInteractionFlags.Drag) != 0)
{ {
host?.HandleDrag(eventData); interactionHost?.HandleDrag(eventData);
return; return;
} }
@ -160,9 +151,9 @@ namespace AlicizaX.UI
public void OnEndDrag(PointerEventData eventData) public void OnEndDrag(PointerEventData eventData)
{ {
if ((flags & ItemInteractionFlags.EndDrag) != 0) if ((activeInteractionFlags & ItemInteractionFlags.EndDrag) != 0)
{ {
host?.HandleEndDrag(eventData); interactionHost?.HandleEndDrag(eventData);
return; return;
} }
@ -172,9 +163,9 @@ namespace AlicizaX.UI
public void OnSubmit(BaseEventData eventData) public void OnSubmit(BaseEventData eventData)
{ {
#if UX_NAVIGATION #if UX_NAVIGATION
if ((flags & ItemInteractionFlags.Submit) != 0) if ((activeInteractionFlags & ItemInteractionFlags.Submit) != 0)
{ {
host?.HandleSubmit(eventData); interactionHost?.HandleSubmit(eventData);
} }
#endif #endif
} }
@ -182,9 +173,9 @@ namespace AlicizaX.UI
public void OnCancel(BaseEventData eventData) public void OnCancel(BaseEventData eventData)
{ {
#if UX_NAVIGATION #if UX_NAVIGATION
if ((flags & ItemInteractionFlags.Cancel) != 0) if ((activeInteractionFlags & ItemInteractionFlags.Cancel) != 0)
{ {
host?.HandleCancel(eventData); interactionHost?.HandleCancel(eventData);
} }
#endif #endif
} }
@ -210,28 +201,35 @@ namespace AlicizaX.UI
#if UX_NAVIGATION #if UX_NAVIGATION
ownedSelectable = GetComponent<RecyclerItemSelectable>(); ownedSelectable = GetComponent<RecyclerItemSelectable>();
if (ownedSelectable == null) #if UNITY_EDITOR || DEVELOPMENT_BUILD
if (ownedSelectable == null && RequiresSelection(activeInteractionFlags) && !missingSelectableLogged)
{ {
ownedSelectable = gameObject.AddComponent<RecyclerItemSelectable>(); missingSelectableLogged = true;
UnityEngine.Debug.LogError(ZString.Format("RecyclerItemSelectable is missing on '{0}'. Add it in prefab/editor setup.", GetHierarchyPath(transform)));
} }
#endif
focusAnchor = ownedSelectable; focusAnchor = ownedSelectable;
#endif #endif
} }
private bool TryGetFocusableSelectable(out Selectable selectable) private bool TryGetInteractionFocusTarget(out GameObject target)
{ {
target = null;
EnsureFocusAnchor(); EnsureFocusAnchor();
if (IsSelectableFocusable(focusAnchor)) if (IsSelectableFocusable(focusAnchor))
{ {
selectable = focusAnchor; target = focusAnchor.gameObject;
return true; return target != null;
} }
Selectable[] selectables = GetComponentsInChildren<Selectable>(true); if (selectableCache.Count == 0)
for (int i = 0; i < selectables.Length; i++)
{ {
Selectable candidate = selectables[i]; RefreshInteractionCache();
}
for (int i = 0; i < selectableCache.Count; i++)
{
Selectable candidate = selectableCache[i];
if (candidate == focusAnchor) if (candidate == focusAnchor)
{ {
continue; continue;
@ -245,22 +243,14 @@ namespace AlicizaX.UI
#endif #endif
if (IsSelectableFocusable(candidate)) if (IsSelectableFocusable(candidate))
{ {
selectable = candidate; target = candidate.gameObject;
return true; return target != null;
} }
} }
selectable = null;
return false; return false;
} }
private static bool IsSelectableFocusable(Selectable selectable)
{
return selectable != null &&
selectable.IsActive() &&
selectable.IsInteractable();
}
private static bool RequiresSelection(ItemInteractionFlags interactionFlags) private static bool RequiresSelection(ItemInteractionFlags interactionFlags)
{ {
#if !UX_NAVIGATION #if !UX_NAVIGATION
@ -277,6 +267,26 @@ namespace AlicizaX.UI
#endif #endif
} }
#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
[System.Diagnostics.Conditional("INPUTSYSTEM_SUPPORT")] [System.Diagnostics.Conditional("INPUTSYSTEM_SUPPORT")]
private void InvalidateNavigationScope() private void InvalidateNavigationScope()
{ {

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: ac4f0b81367e72b408b7d4a0148d39c3 guid: 6114223b5ccfeaa499630f5e4b71120c
MonoImporter: MonoImporter:
externalObjects: {} externalObjects: {}
serializedVersion: 2 serializedVersion: 2

View File

@ -1,13 +1,19 @@
using System; using System.Collections.Generic;
using UnityEngine; using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace AlicizaX.UI namespace AlicizaX.UI
{ {
public abstract class ViewHolder : MonoBehaviour public abstract partial class ViewHolder : MonoBehaviour
{ {
private RectTransform rectTransform; private RectTransform rectTransform;
private Selectable focusAnchor;
private readonly List<Selectable> selectableCache = new();
private bool interactionCacheReady;
internal event Action<ViewHolder> Destroyed; internal IItemRender CachedItemRender;
internal string CachedItemRenderViewName;
public RectTransform RectTransform public RectTransform RectTransform
{ {
@ -40,6 +46,57 @@ namespace AlicizaX.UI
return BindingVersion; return BindingVersion;
} }
internal void RefreshInteractionCache()
{
if (interactionCacheReady)
{
return;
}
focusAnchor = GetComponent<Selectable>();
selectableCache.Clear();
GetComponentsInChildren(true, selectableCache);
interactionCacheReady = true;
}
internal bool TryGetFocusTarget(out GameObject target)
{
if (TryGetInteractionFocusTarget(out target))
{
return true;
}
Selectable selectable = IsSelectableFocusable(focusAnchor) ? focusAnchor : null;
if (selectable == null)
{
for (int i = 0; i < selectableCache.Count; i++)
{
if (IsSelectableFocusable(selectableCache[i]))
{
selectable = selectableCache[i];
break;
}
}
}
if (selectable != null)
{
target = selectable.gameObject;
return true;
}
if (ExecuteEvents.CanHandleEvent<IMoveHandler>(gameObject) ||
ExecuteEvents.CanHandleEvent<ISelectHandler>(gameObject) ||
ExecuteEvents.CanHandleEvent<ISubmitHandler>(gameObject))
{
target = gameObject;
return true;
}
target = null;
return false;
}
protected internal virtual void OnRecycled() protected internal virtual void OnRecycled()
{ {
AdvanceBindingVersion(); AdvanceBindingVersion();
@ -49,10 +106,12 @@ namespace AlicizaX.UI
RecyclerView = null; RecyclerView = null;
} }
protected virtual void OnDestroy() private static bool IsSelectableFocusable(Selectable selectable)
{ {
Destroyed?.Invoke(this); return selectable != null &&
Destroyed = null; selectable.IsActive() &&
selectable.IsInteractable();
} }
} }
} }

View File

@ -1,19 +1,46 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Cysharp.Text;
namespace AlicizaX.UI namespace AlicizaX.UI
{ {
public class MixedViewProvider : ViewProvider public class MixedViewProvider : ViewProvider
{ {
private readonly MixedObjectPool<ViewHolder> objectPool; private readonly MixedObjectPool<ViewHolder> objectPool;
private readonly Dictionary<string, int> templateIdsByName = new(StringComparer.Ordinal);
private readonly Dictionary<string, ViewHolder> templatesByName = new(StringComparer.Ordinal); private readonly Dictionary<string, ViewHolder> templatesByName = new(StringComparer.Ordinal);
private readonly Dictionary<string, int> warmCounts = new(StringComparer.Ordinal); private readonly ViewHolder[] cachedTemplates;
private readonly string[] templateNames;
private readonly int[] warmCountsByType;
public override string PoolStats => public override string PoolStats
$"hits={objectPool.HitCount}, misses={objectPool.MissCount}, destroys={objectPool.DestroyCount}"; {
get
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
return ZString.Format("hits={0}, misses={1}, destroys={2}", objectPool.HitCount, objectPool.MissCount, objectPool.DestroyCount);
#else
return string.Empty;
#endif
}
}
public MixedViewProvider(RecyclerView recyclerView, ViewHolder[] templates) : base(recyclerView, templates) public MixedViewProvider(RecyclerView recyclerView, ViewHolder[] templates) : base(recyclerView, templates)
{ {
int count = 0;
for (int i = 0; i < templates.Length; i++)
{
if (templates[i] != null)
{
count++;
}
}
cachedTemplates = new ViewHolder[count];
templateNames = new string[count];
warmCountsByType = new int[count];
int typeId = 0;
for (int i = 0; i < templates.Length; i++) for (int i = 0; i < templates.Length; i++)
{ {
ViewHolder template = templates[i]; ViewHolder template = templates[i];
@ -22,7 +49,12 @@ namespace AlicizaX.UI
continue; continue;
} }
templatesByName[template.GetType().Name] = template; string templateName = template.GetType().Name;
templateIdsByName[templateName] = typeId;
templatesByName[templateName] = template;
cachedTemplates[typeId] = template;
templateNames[typeId] = templateName;
typeId++;
} }
UnityMixedComponentFactory<ViewHolder> factory = new(templatesByName, recyclerView.Content); UnityMixedComponentFactory<ViewHolder> factory = new(templatesByName, recyclerView.Content);
@ -31,31 +63,17 @@ namespace AlicizaX.UI
public override ViewHolder GetTemplate(string viewName) public override ViewHolder GetTemplate(string viewName)
{ {
if (templates == null || templates.Length == 0)
{
throw new NullReferenceException("ViewProvider templates can not null or empty.");
}
if (!templatesByName.TryGetValue(viewName, out ViewHolder template)) if (!templatesByName.TryGetValue(viewName, out ViewHolder template))
{ {
throw new KeyNotFoundException($"ViewProvider template '{viewName}' was not found."); #if UNITY_EDITOR || DEVELOPMENT_BUILD
UnityEngine.Debug.LogError("ViewProvider template was not found.");
#endif
return null;
} }
return template; return template;
} }
public override ViewHolder[] GetTemplates()
{
if (templates == null || templates.Length == 0)
{
throw new NullReferenceException("ViewProvider templates can not null or empty.");
}
ViewHolder[] values = new ViewHolder[templatesByName.Count];
templatesByName.Values.CopyTo(values, 0);
return values;
}
public override ViewHolder Allocate(string viewName) public override ViewHolder Allocate(string viewName)
{ {
var viewHolder = objectPool.Allocate(viewName); var viewHolder = objectPool.Allocate(viewName);
@ -72,7 +90,7 @@ namespace AlicizaX.UI
{ {
Clear(); Clear();
(Adapter as IItemRenderCacheOwner)?.ReleaseAllItemRenders(); (Adapter as IItemRenderCacheOwner)?.ReleaseAllItemRenders();
objectPool.Dispose(); objectPool.ClearInactive();
} }
public override void PreparePool() public override void PreparePool()
@ -83,28 +101,36 @@ namespace AlicizaX.UI
return; return;
} }
PrepareBucketPool(warmCount);
int itemCount = GetItemCount(); int itemCount = GetItemCount();
int start = Math.Max(0, LayoutManager.GetStartIndex()); int start = Math.Max(0, LayoutManager.GetStartIndex());
int end = Math.Min(itemCount - 1, start + warmCount - 1); int end = Math.Min(itemCount - 1, start + warmCount - 1);
warmCounts.Clear(); Array.Clear(warmCountsByType, 0, warmCountsByType.Length);
for (int index = start; index <= end; index++) for (int index = start; index <= end; index++)
{ {
string viewName = Adapter.GetViewName(index); string viewName = Adapter.GetViewName(index);
if (string.IsNullOrEmpty(viewName)) if (string.IsNullOrEmpty(viewName) || !templateIdsByName.TryGetValue(viewName, out int typeId))
{ {
continue; continue;
} }
warmCounts.TryGetValue(viewName, out int count); warmCountsByType[typeId]++;
warmCounts[viewName] = count + 1;
} }
foreach (var pair in warmCounts) for (int typeId = 0; typeId < warmCountsByType.Length; typeId++)
{ {
int targetCount = pair.Value + Math.Max(1, LayoutManager.Unit); int count = warmCountsByType[typeId];
objectPool.EnsureCapacity(pair.Key, targetCount); if (count <= 0)
objectPool.Warm(pair.Key, targetCount); {
continue;
}
string typeName = templateNames[typeId];
int targetCount = count + Math.Max(1, LayoutManager.Unit);
objectPool.EnsureCapacity(typeName, targetCount);
objectPool.Warm(typeName, targetCount);
} }
} }
} }

View File

@ -1,13 +1,29 @@
using System;
namespace AlicizaX.UI namespace AlicizaX.UI
{ {
using Cysharp.Text;
public sealed class SimpleViewProvider : ViewProvider public sealed class SimpleViewProvider : ViewProvider
{ {
private readonly ObjectPool<ViewHolder> objectPool; private readonly ObjectPool<ViewHolder> objectPool;
public override string PoolStats => public override string PoolStats
$"hits={objectPool.HitCount}, misses={objectPool.MissCount}, destroys={objectPool.DestroyCount}, active={objectPool.ActiveCount}, inactive={objectPool.InactiveCount}, peakActive={objectPool.PeakActive}, capacity={objectPool.MaxSize}"; {
get
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
return ZString.Format("hits={0}, misses={1}, destroys={2}, active={3}, inactive={4}, peakActive={5}, capacity={6}",
objectPool.HitCount,
objectPool.MissCount,
objectPool.DestroyCount,
objectPool.ActiveCount,
objectPool.InactiveCount,
objectPool.PeakActive,
objectPool.MaxSize);
#else
return string.Empty;
#endif
}
}
public SimpleViewProvider(RecyclerView recyclerView, ViewHolder[] templates) : base(recyclerView, templates) public SimpleViewProvider(RecyclerView recyclerView, ViewHolder[] templates) : base(recyclerView, templates)
{ {
@ -17,20 +33,7 @@ namespace AlicizaX.UI
public override ViewHolder GetTemplate(string viewName = "") public override ViewHolder GetTemplate(string viewName = "")
{ {
if (templates == null || templates.Length == 0) return templates != null && templates.Length > 0 ? templates[0] : null;
{
throw new NullReferenceException("ViewProvider templates can not null or empty.");
}
return templates[0];
}
public override ViewHolder[] GetTemplates()
{
if (templates == null || templates.Length == 0)
{
throw new NullReferenceException("ViewProvider templates can not null or empty.");
}
return templates;
} }
public override ViewHolder Allocate(string viewName) public override ViewHolder Allocate(string viewName)
@ -49,7 +52,7 @@ namespace AlicizaX.UI
{ {
Clear(); Clear();
(Adapter as IItemRenderCacheOwner)?.ReleaseAllItemRenders(); (Adapter as IItemRenderCacheOwner)?.ReleaseAllItemRenders();
objectPool.Dispose(); objectPool.ClearInactive();
} }
public override void PreparePool() public override void PreparePool()
@ -60,6 +63,8 @@ namespace AlicizaX.UI
return; return;
} }
PrepareBucketPool(warmCount);
objectPool.EnsureCapacity(warmCount); objectPool.EnsureCapacity(warmCount);
objectPool.Warm(warmCount); objectPool.Warm(warmCount);
} }

View File

@ -6,16 +6,26 @@ namespace AlicizaX.UI
{ {
public abstract class ViewProvider public abstract class ViewProvider
{ {
private readonly List<ViewHolder> viewHolders = new(); private ViewHolder[] visibleHolders = new ViewHolder[8];
private ViewHolder[] removeBuffer = new ViewHolder[4];
private ViewHolderBucket[] bucketPool = new ViewHolderBucket[8];
private int visibleHead;
private int visibleCount;
private int bucketPoolCount;
private readonly Dictionary<int, ViewHolder> viewHoldersByIndex = new(); private readonly Dictionary<int, ViewHolder> viewHoldersByIndex = new();
private readonly Dictionary<int, List<ViewHolder>> viewHoldersByDataIndex = new(); private readonly Dictionary<int, ViewHolderBucket> viewHoldersByDataIndex = new();
private readonly Dictionary<int, int> viewHolderPositions = new(); private readonly Dictionary<int, int> viewHolderPositions = new();
public IAdapter Adapter { get; set; } public IAdapter Adapter { get; set; }
public LayoutManager LayoutManager { get; set; } public LayoutManager LayoutManager { get; set; }
public IReadOnlyList<ViewHolder> ViewHolders => viewHolders; public int VisibleCount => visibleCount;
public ViewHolder GetVisibleViewHolder(int index)
{
return index >= 0 && index < visibleCount ? visibleHolders[GetVisibleSlot(index)] : null;
}
public abstract string PoolStats { get; } public abstract string PoolStats { get; }
@ -30,8 +40,6 @@ namespace AlicizaX.UI
public abstract ViewHolder GetTemplate(string viewName); public abstract ViewHolder GetTemplate(string viewName);
public abstract ViewHolder[] GetTemplates();
public abstract ViewHolder Allocate(string viewName); public abstract ViewHolder Allocate(string viewName);
public abstract void Free(string viewName, ViewHolder viewHolder); public abstract void Free(string viewName, ViewHolder viewHolder);
@ -52,8 +60,19 @@ namespace AlicizaX.UI
viewHolder.Index = i; viewHolder.Index = i;
viewHolder.DataIndex = i; viewHolder.DataIndex = i;
viewHolder.RecyclerView = recyclerView; viewHolder.RecyclerView = recyclerView;
viewHolders.Add(viewHolder); (Adapter as IItemRenderPrewarmer)?.PrewarmItemRender(viewHolder, viewName);
RegisterViewHolder(viewHolder); if (!AddVisibleHolder(viewHolder))
{
Free(viewName, viewHolder);
continue;
}
if (!RegisterViewHolder(viewHolder))
{
RemoveVisibleHolder(viewHolder);
Free(viewName, viewHolder);
continue;
}
LayoutManager.Layout(viewHolder, i); LayoutManager.Layout(viewHolder, i);
Adapter.OnBindViewHolder(viewHolder, i); Adapter.OnBindViewHolder(viewHolder, i);
@ -62,19 +81,37 @@ namespace AlicizaX.UI
public void RemoveViewHolder(int index) public void RemoveViewHolder(int index)
{ {
for (int i = index; i < index + LayoutManager.Unit; i++) int removeCount = 0;
int end = index + LayoutManager.Unit;
EnsureRemoveBufferCapacity(LayoutManager.Unit);
for (int i = index; i < end; i++)
{ {
if (i > Adapter.GetItemCount() - 1) break; if (i > Adapter.GetItemCount() - 1)
{
break;
}
int viewHolderIndex = GetViewHolderIndex(i); int viewHolderIndex = GetViewHolderIndex(i);
if (viewHolderIndex < 0 || viewHolderIndex >= visibleCount)
{
continue;
}
if (viewHolderIndex < 0 || viewHolderIndex >= viewHolders.Count) return; removeBuffer[removeCount++] = visibleHolders[GetVisibleSlot(viewHolderIndex)];
}
for (int i = 0; i < removeCount; i++)
{
ViewHolder viewHolder = removeBuffer[i];
removeBuffer[i] = null;
if (viewHolder == null)
{
continue;
}
var viewHolder = viewHolders[viewHolderIndex];
string viewName = viewHolder.Name; string viewName = viewHolder.Name;
viewHolders.RemoveAt(viewHolderIndex); RemoveVisibleHolder(viewHolder);
UnregisterViewHolder(viewHolder); UnregisterViewHolder(viewHolder);
RebuildViewHolderPositions(viewHolderIndex);
Adapter?.OnRecycleViewHolder(viewHolder); Adapter?.OnRecycleViewHolder(viewHolder);
viewHolder.OnRecycled(); viewHolder.OnRecycled();
ClearSelectedState(viewHolder); ClearSelectedState(viewHolder);
@ -91,22 +128,14 @@ namespace AlicizaX.UI
public ViewHolder GetViewHolderByDataIndex(int dataIndex) public ViewHolder GetViewHolderByDataIndex(int dataIndex)
{ {
return viewHoldersByDataIndex.TryGetValue(dataIndex, out List<ViewHolder> holders) && return viewHoldersByDataIndex.TryGetValue(dataIndex, out ViewHolderBucket bucket) && bucket.Count > 0
holders is { Count: > 0 } ? bucket[0]
? holders[0]
: null; : null;
} }
public bool TryGetViewHoldersByDataIndex(int dataIndex, out IReadOnlyList<ViewHolder> holders) public bool TryGetViewHolderBucket(int dataIndex, out ViewHolderBucket bucket)
{ {
if (viewHoldersByDataIndex.TryGetValue(dataIndex, out List<ViewHolder> list) && list.Count > 0) return viewHoldersByDataIndex.TryGetValue(dataIndex, out bucket) && bucket.Count > 0;
{
holders = list;
return true;
}
holders = null;
return false;
} }
public int GetViewHolderIndex(int index) public int GetViewHolderIndex(int index)
@ -118,8 +147,14 @@ namespace AlicizaX.UI
public void Clear() public void Clear()
{ {
foreach (var viewHolder in viewHolders) for (int i = 0; i < visibleCount; i++)
{ {
ViewHolder viewHolder = visibleHolders[GetVisibleSlot(i)];
if (viewHolder == null)
{
continue;
}
string viewName = viewHolder.Name; string viewName = viewHolder.Name;
Adapter?.OnRecycleViewHolder(viewHolder); Adapter?.OnRecycleViewHolder(viewHolder);
UnregisterViewHolder(viewHolder); UnregisterViewHolder(viewHolder);
@ -128,8 +163,14 @@ namespace AlicizaX.UI
Free(viewName, viewHolder); Free(viewName, viewHolder);
} }
viewHolders.Clear(); System.Array.Clear(visibleHolders, 0, visibleHolders.Length);
visibleHead = 0;
visibleCount = 0;
viewHoldersByIndex.Clear(); viewHoldersByIndex.Clear();
foreach (var pair in viewHoldersByDataIndex)
{
ReleaseBucket(pair.Value);
}
viewHoldersByDataIndex.Clear(); viewHoldersByDataIndex.Clear();
viewHolderPositions.Clear(); viewHolderPositions.Clear();
} }
@ -161,27 +202,47 @@ namespace AlicizaX.UI
int start = Mathf.Max(0, LayoutManager.GetStartIndex()); int start = Mathf.Max(0, LayoutManager.GetStartIndex());
int end = Mathf.Max(start, LayoutManager.GetEndIndex()); int end = Mathf.Max(start, LayoutManager.GetEndIndex());
int visibleCount = end - start + 1; int visibleCount = end - start + 1;
int bufferCount = Mathf.Max(1, LayoutManager.Unit); int unit = Mathf.Max(1, LayoutManager.Unit);
int bufferCount = unit * 2;
return Mathf.Min(itemCount, visibleCount + bufferCount); return Mathf.Min(itemCount, visibleCount + bufferCount);
} }
private void RegisterViewHolder(ViewHolder viewHolder) protected void PrepareVisibleStorage(int warmCount)
{
int capacity = Mathf.Max(Mathf.Max(1, LayoutManager != null ? LayoutManager.Unit : 1), warmCount);
if (visibleHolders.Length < capacity)
{
visibleHolders = new ViewHolder[capacity];
}
if (removeBuffer.Length < capacity)
{
removeBuffer = new ViewHolder[capacity];
}
}
private bool RegisterViewHolder(ViewHolder viewHolder)
{ {
if (viewHolder == null) if (viewHolder == null)
{ {
return; return false;
}
if (!viewHoldersByDataIndex.TryGetValue(viewHolder.DataIndex, out ViewHolderBucket bucket))
{
bucket = AllocateBucket();
if (bucket == null)
{
return false;
}
viewHoldersByDataIndex[viewHolder.DataIndex] = bucket;
} }
viewHoldersByIndex[viewHolder.Index] = viewHolder; viewHoldersByIndex[viewHolder.Index] = viewHolder;
viewHolderPositions[viewHolder.Index] = viewHolders.Count - 1; viewHolderPositions[viewHolder.Index] = GetVisibleSlot(visibleCount - 1);
bucket.Add(viewHolder);
if (!viewHoldersByDataIndex.TryGetValue(viewHolder.DataIndex, out List<ViewHolder> holders)) return true;
{
holders = new List<ViewHolder>(1);
viewHoldersByDataIndex[viewHolder.DataIndex] = holders;
}
holders.Add(viewHolder);
} }
private void UnregisterViewHolder(ViewHolder viewHolder) private void UnregisterViewHolder(ViewHolder viewHolder)
@ -194,35 +255,72 @@ namespace AlicizaX.UI
viewHoldersByIndex.Remove(viewHolder.Index); viewHoldersByIndex.Remove(viewHolder.Index);
viewHolderPositions.Remove(viewHolder.Index); viewHolderPositions.Remove(viewHolder.Index);
if (!viewHoldersByDataIndex.TryGetValue(viewHolder.DataIndex, out List<ViewHolder> holders)) if (!viewHoldersByDataIndex.TryGetValue(viewHolder.DataIndex, out ViewHolderBucket bucket))
{ {
return; return;
} }
// 用末尾元素覆盖目标项再移除末位,避免 List.Remove 的线性搜索+内存搬移。 bucket.Remove(viewHolder);
int idx = holders.LastIndexOf(viewHolder); if (bucket.Count == 0)
if (idx >= 0)
{
holders[idx] = holders[holders.Count - 1];
holders.RemoveAt(holders.Count - 1);
}
if (holders.Count == 0)
{ {
viewHoldersByDataIndex.Remove(viewHolder.DataIndex); viewHoldersByDataIndex.Remove(viewHolder.DataIndex);
ReleaseBucket(bucket);
} }
} }
private void RebuildViewHolderPositions(int startIndex) private bool AddVisibleHolder(ViewHolder viewHolder)
{ {
for (int i = startIndex; i < viewHolders.Count; i++) if (visibleCount == visibleHolders.Length)
{ {
ViewHolder holder = viewHolders[i]; #if UNITY_EDITOR || DEVELOPMENT_BUILD
if (holder != null) UnityEngine.Debug.LogError("RecyclerView visible holder capacity exceeded. Increase warm count.");
#endif
return false;
}
visibleHolders[GetVisibleSlot(visibleCount)] = viewHolder;
visibleCount++;
return true;
}
private void RemoveVisibleHolder(ViewHolder viewHolder)
{ {
viewHolderPositions[holder.Index] = i; if (!viewHolderPositions.TryGetValue(viewHolder.Index, out int slot))
{
return;
}
int last = GetVisibleSlot(visibleCount - 1);
ViewHolder lastHolder = visibleHolders[last];
visibleHolders[slot] = lastHolder;
visibleHolders[last] = null;
visibleCount--;
if (visibleCount == 0)
{
visibleHead = 0;
}
if (lastHolder != null && lastHolder != viewHolder)
{
viewHolderPositions[lastHolder.Index] = slot;
} }
} }
private int GetVisibleSlot(int index)
{
return (visibleHead + index) % visibleHolders.Length;
}
private void EnsureRemoveBufferCapacity(int required)
{
if (required <= removeBuffer.Length)
{
return;
}
#if UNITY_EDITOR || DEVELOPMENT_BUILD
UnityEngine.Debug.LogError("RecyclerView remove buffer capacity exceeded. Increase warm count.");
#endif
} }
private static void ClearSelectedState(ViewHolder viewHolder) private static void ClearSelectedState(ViewHolder viewHolder)
@ -244,5 +342,132 @@ namespace AlicizaX.UI
eventSystem.SetSelectedGameObject(null); eventSystem.SetSelectedGameObject(null);
} }
} }
protected void PrepareBucketPool(int count)
{
PrepareVisibleStorage(count);
EnsureBucketPoolCapacity(count);
int capacity = GetBucketCapacity();
while (bucketPoolCount < count)
{
bucketPool[bucketPoolCount++] = new ViewHolderBucket(capacity);
}
}
private ViewHolderBucket AllocateBucket()
{
if (bucketPoolCount > 0)
{
ViewHolderBucket bucket = bucketPool[--bucketPoolCount];
bucketPool[bucketPoolCount] = null;
bucket.Clear();
return bucket;
}
#if UNITY_EDITOR || DEVELOPMENT_BUILD
UnityEngine.Debug.LogError("ViewHolderBucket pool is empty. Increase RecyclerView warm count.");
#endif
return null;
}
private void ReleaseBucket(ViewHolderBucket bucket)
{
if (bucket == null)
{
return;
}
bucket.Clear();
EnsureBucketPoolCapacity(bucketPoolCount + 1);
bucketPool[bucketPoolCount++] = bucket;
}
private int GetBucketCapacity()
{
return Mathf.Max(4, LayoutManager != null ? LayoutManager.Unit + 1 : 4);
}
private void EnsureBucketPoolCapacity(int required)
{
if (required <= bucketPool.Length)
{
return;
}
int capacity = bucketPool.Length;
while (capacity < required)
{
capacity <<= 1;
}
ViewHolderBucket[] next = new ViewHolderBucket[capacity];
System.Array.Copy(bucketPool, next, bucketPoolCount);
bucketPool = next;
}
public sealed class ViewHolderBucket
{
private readonly ViewHolder[] holders;
public ViewHolderBucket(int capacity)
{
holders = new ViewHolder[capacity];
}
public int Count { get; private set; }
public ViewHolder this[int index]
{
get
{
if (index < 0 || index >= Count)
{
return null;
}
return holders[index];
}
}
public void Add(ViewHolder holder)
{
if (Count == holders.Length)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
UnityEngine.Debug.LogError("ViewHolderBucket capacity exceeded.");
#endif
return;
}
holders[Count++] = holder;
}
public void Remove(ViewHolder holder)
{
for (int i = 0; i < Count; i++)
{
if (holders[i] != holder)
{
continue;
}
Count--;
holders[i] = holders[Count];
holders[Count] = null;
return;
}
}
public void Clear()
{
for (int i = 0; i < Count; i++)
{
holders[i] = null;
}
Count = 0;
}
}
} }
} }

View File

@ -1,4 +1,4 @@
#if INPUTSYSTEM_SUPPORT && UX_NAVIGATION #if INPUTSYSTEM_SUPPORT && UX_NAVIGATION
using System; using System;
using UnityEngine.InputSystem; using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Controls; using UnityEngine.InputSystem.Controls;
@ -10,6 +10,30 @@ namespace UnityEngine.UI
private const float StickThresholdSqr = 0.04f; private const float StickThresholdSqr = 0.04f;
private const float AxisThreshold = 0.2f; private const float AxisThreshold = 0.2f;
private const string PointerActionName = "UXPointerInput";
private const string KeyboardActionName = "UXKeyboardInput";
private const string GamepadActionName = "UXGamepadInput";
private const string TouchActionName = "UXTouchInput";
private const string MouseDeltaBinding = "<Mouse>/delta";
private const string MouseScrollBinding = "<Mouse>/scroll";
private const string MouseLeftButtonBinding = "<Mouse>/leftButton";
private const string MouseRightButtonBinding = "<Mouse>/rightButton";
private const string MouseMiddleButtonBinding = "<Mouse>/middleButton";
private const string KeyboardAnyKeyBinding = "<Keyboard>/anyKey";
private const string GamepadButtonSouthBinding = "<Gamepad>/buttonSouth";
private const string GamepadButtonNorthBinding = "<Gamepad>/buttonNorth";
private const string GamepadButtonEastBinding = "<Gamepad>/buttonEast";
private const string GamepadButtonWestBinding = "<Gamepad>/buttonWest";
private const string GamepadStartButtonBinding = "<Gamepad>/startButton";
private const string GamepadSelectButtonBinding = "<Gamepad>/selectButton";
private const string GamepadLeftShoulderBinding = "<Gamepad>/leftShoulder";
private const string GamepadRightShoulderBinding = "<Gamepad>/rightShoulder";
private const string GamepadDpadBinding = "<Gamepad>/dpad";
private const string GamepadLeftStickBinding = "<Gamepad>/leftStick";
private const string GamepadRightStickBinding = "<Gamepad>/rightStick";
private const string TouchPressBinding = "<Touchscreen>/primaryTouch/press";
private const string TouchDeltaBinding = "<Touchscreen>/primaryTouch/delta";
private static UXInputModeService _instance; private static UXInputModeService _instance;
private InputAction _pointerAction; private InputAction _pointerAction;
@ -21,12 +45,6 @@ namespace UnityEngine.UI
public static event Action<UXInputMode> OnModeChanged; public static event Action<UXInputMode> OnModeChanged;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
private static void Bootstrap()
{
EnsureInstance();
}
internal static UXInputModeService EnsureInstance() internal static UXInputModeService EnsureInstance()
{ {
if (_instance != null) if (_instance != null)
@ -81,35 +99,35 @@ namespace UnityEngine.UI
return; return;
} }
_pointerAction = new InputAction("UXPointerInput", InputActionType.PassThrough); _pointerAction = new InputAction(PointerActionName, InputActionType.PassThrough);
_pointerAction.AddBinding("<Mouse>/delta"); _pointerAction.AddBinding(MouseDeltaBinding);
_pointerAction.AddBinding("<Mouse>/scroll"); _pointerAction.AddBinding(MouseScrollBinding);
_pointerAction.AddBinding("<Mouse>/leftButton"); _pointerAction.AddBinding(MouseLeftButtonBinding);
_pointerAction.AddBinding("<Mouse>/rightButton"); _pointerAction.AddBinding(MouseRightButtonBinding);
_pointerAction.AddBinding("<Mouse>/middleButton"); _pointerAction.AddBinding(MouseMiddleButtonBinding);
_pointerAction.performed += OnPointerInput; _pointerAction.performed += OnPointerInput;
_keyboardAction = new InputAction("UXKeyboardInput", InputActionType.PassThrough); _keyboardAction = new InputAction(KeyboardActionName, InputActionType.PassThrough);
_keyboardAction.AddBinding("<Keyboard>/anyKey"); _keyboardAction.AddBinding(KeyboardAnyKeyBinding);
_keyboardAction.performed += OnKeyboardInput; _keyboardAction.performed += OnKeyboardInput;
_gamepadAction = new InputAction("UXGamepadInput", InputActionType.PassThrough); _gamepadAction = new InputAction(GamepadActionName, InputActionType.PassThrough);
_gamepadAction.AddBinding("<Gamepad>/buttonSouth"); _gamepadAction.AddBinding(GamepadButtonSouthBinding);
_gamepadAction.AddBinding("<Gamepad>/buttonNorth"); _gamepadAction.AddBinding(GamepadButtonNorthBinding);
_gamepadAction.AddBinding("<Gamepad>/buttonEast"); _gamepadAction.AddBinding(GamepadButtonEastBinding);
_gamepadAction.AddBinding("<Gamepad>/buttonWest"); _gamepadAction.AddBinding(GamepadButtonWestBinding);
_gamepadAction.AddBinding("<Gamepad>/startButton"); _gamepadAction.AddBinding(GamepadStartButtonBinding);
_gamepadAction.AddBinding("<Gamepad>/selectButton"); _gamepadAction.AddBinding(GamepadSelectButtonBinding);
_gamepadAction.AddBinding("<Gamepad>/leftShoulder"); _gamepadAction.AddBinding(GamepadLeftShoulderBinding);
_gamepadAction.AddBinding("<Gamepad>/rightShoulder"); _gamepadAction.AddBinding(GamepadRightShoulderBinding);
_gamepadAction.AddBinding("<Gamepad>/dpad"); _gamepadAction.AddBinding(GamepadDpadBinding);
_gamepadAction.AddBinding("<Gamepad>/leftStick"); _gamepadAction.AddBinding(GamepadLeftStickBinding);
_gamepadAction.AddBinding("<Gamepad>/rightStick"); _gamepadAction.AddBinding(GamepadRightStickBinding);
_gamepadAction.performed += OnGamepadInput; _gamepadAction.performed += OnGamepadInput;
_touchAction = new InputAction("UXTouchInput", InputActionType.PassThrough); _touchAction = new InputAction(TouchActionName, InputActionType.PassThrough);
_touchAction.AddBinding("<Touchscreen>/primaryTouch/press"); _touchAction.AddBinding(TouchPressBinding);
_touchAction.AddBinding("<Touchscreen>/primaryTouch/delta"); _touchAction.AddBinding(TouchDeltaBinding);
_touchAction.performed += OnTouchInput; _touchAction.performed += OnTouchInput;
} }
@ -222,7 +240,6 @@ namespace UnityEngine.UI
internal static void SetMode(UXInputMode mode) internal static void SetMode(UXInputMode mode)
{ {
EnsureInstance();
if (CurrentMode == mode) if (CurrentMode == mode)
{ {
return; return;
@ -234,3 +251,4 @@ namespace UnityEngine.UI
} }
} }
#endif #endif

View File

@ -1,5 +1,4 @@
#if INPUTSYSTEM_SUPPORT && UX_NAVIGATION #if INPUTSYSTEM_SUPPORT && UX_NAVIGATION
using AlicizaX;
using AlicizaX.UI.Runtime; using AlicizaX.UI.Runtime;
using UnityEngine.EventSystems; using UnityEngine.EventSystems;
@ -7,39 +6,28 @@ namespace UnityEngine.UI
{ {
public interface IUXNavigationCursorPolicy public interface IUXNavigationCursorPolicy
{ {
void OnInputModeChanged(UXInputMode mode, UXNavigationScope topScope); void OnNavigationContextChanged(UXInputMode mode, UXNavigationScope previousTopScope, UXNavigationScope currentTopScope);
} }
public sealed class UXNavigationRuntime : MonoBehaviour public sealed class UXNavigationRuntime : MonoBehaviour
{ {
private const int InitialScopeCapacity = 64; private const int ScopeCapacity = 128;
private const int InvalidIndex = -1; private const int InvalidIndex = -1;
private static UXNavigationRuntime _instance; private static UXNavigationRuntime _instance;
private static IUXNavigationCursorPolicy _cursorPolicy; private static IUXNavigationCursorPolicy _cursorPolicy;
private UXNavigationScope[] _scopes = new UXNavigationScope[InitialScopeCapacity]; private readonly UXNavigationScope[] _scopes = new UXNavigationScope[ScopeCapacity];
private int[] _freeIndices = new int[InitialScopeCapacity];
private int _freeCount;
private int _scopeCount; private int _scopeCount;
private int _scopeCapacityHighWater;
private UXNavigationScope _topScope; private UXNavigationScope _topScope;
private ulong _activationSerial; private ulong _activationSerial;
private bool _stateDirty = true; private bool _stateDirty = true;
private bool _suppressionDirty = true; private bool _suppressionDirty = true;
private bool _isFlushingState;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] private bool _isEnsuringSelection;
private static void Bootstrap() private bool _contextNotificationDirty;
{ private UXNavigationScope _pendingPreviousTopScope;
if (!AppServices.TryGetApp<IUIService>(out var uiService) || uiService == null)
{
return;
}
EnsureInstance();
UXInputModeService.EnsureInstance();
}
internal static UXNavigationRuntime EnsureInstance() internal static UXNavigationRuntime EnsureInstance()
{ {
@ -64,6 +52,10 @@ namespace UnityEngine.UI
public static void SetCursorPolicy(IUXNavigationCursorPolicy cursorPolicy) public static void SetCursorPolicy(IUXNavigationCursorPolicy cursorPolicy)
{ {
_cursorPolicy = cursorPolicy; _cursorPolicy = cursorPolicy;
if (_instance != null)
{
_cursorPolicy?.OnNavigationContextChanged(UXInputModeService.CurrentMode, _instance._topScope, _instance._topScope);
}
} }
public static void NotifySelection(GameObject selectedObject) public static void NotifySelection(GameObject selectedObject)
@ -119,6 +111,7 @@ namespace UnityEngine.UI
if (_instance == this) if (_instance == this)
{ {
_instance = null; _instance = null;
_cursorPolicy = null;
} }
} }
@ -129,25 +122,16 @@ namespace UnityEngine.UI
return; return;
} }
int index; if (_scopeCount >= _scopes.Length)
if (_freeCount > 0)
{ {
index = _freeIndices[--_freeCount]; ReportCapacityExceeded();
}
else
{
if (_scopeCapacityHighWater >= _scopes.Length)
{
ReportCapacityExceeded("UXNavigationRuntime scope capacity exceeded.");
return; return;
} }
index = _scopeCapacityHighWater++; int index = _scopeCount++;
}
_scopes[index] = scope; _scopes[index] = scope;
scope.RuntimeIndex = index; scope.RuntimeIndex = index;
_scopeCount++; UXInputModeService.EnsureInstance();
MarkStateDirty(); MarkStateDirty();
} }
@ -159,23 +143,30 @@ namespace UnityEngine.UI
} }
int index = scope.RuntimeIndex; int index = scope.RuntimeIndex;
if (index < 0 || index >= _scopes.Length || _scopes[index] != scope) if (index < 0 || index >= _scopeCount || _scopes[index] != scope)
{ {
return; return;
} }
if (_topScope == scope) if (_topScope == scope)
{ {
_topScope = null; SetTopScope(null, false);
} }
scope.IsAvailable = false; scope.IsAvailable = false;
scope.WasAvailable = false; scope.WasAvailable = false;
scope.SetNavigationSuppressed(false); scope.SetNavigationSuppressed(false);
scope.RuntimeIndex = InvalidIndex; scope.RuntimeIndex = InvalidIndex;
_scopes[index] = null;
_freeIndices[_freeCount++] = index; int last = --_scopeCount;
_scopeCount--; UXNavigationScope movedScope = _scopes[last];
_scopes[last] = null;
if (index != last)
{
_scopes[index] = movedScope;
movedScope.RuntimeIndex = index;
}
MarkStateDirty(); MarkStateDirty();
} }
@ -192,29 +183,28 @@ namespace UnityEngine.UI
internal void InvalidateSkipCaches() internal void InvalidateSkipCaches()
{ {
for (int i = 0; i < _scopeCapacityHighWater; i++) for (int i = 0; i < _scopeCount; i++)
{ {
UXNavigationScope scope = _scopes[i]; _scopes[i].InvalidateSkipCacheOnly();
if (scope != null)
{
scope.InvalidateSkipCacheOnly();
}
} }
MarkStateDirty(); MarkStateDirty();
} }
private void FlushStateIfDirty() private void FlushStateIfDirty(bool notifyContext)
{ {
if (_isFlushingState)
{
return;
}
_isFlushingState = true;
UXNavigationScope previousTopScope = _topScope;
if (_stateDirty) if (_stateDirty)
{ {
UXNavigationScope newTopScope = FindTopScope(); UXNavigationScope newTopScope = FindTopScope();
_stateDirty = false; _stateDirty = false;
if (!ReferenceEquals(_topScope, newTopScope)) SetTopScope(newTopScope, false);
{
_topScope = newTopScope;
_suppressionDirty = true;
}
} }
if (_suppressionDirty) if (_suppressionDirty)
@ -227,19 +217,24 @@ namespace UnityEngine.UI
{ {
EnsureNavigationSelection(); EnsureNavigationSelection();
} }
_isFlushingState = false;
if (notifyContext)
{
NotifyContextIfChanged(previousTopScope, _topScope);
}
else if (!ReferenceEquals(previousTopScope, _topScope))
{
QueueContextNotification(previousTopScope);
}
} }
private UXNavigationScope FindTopScope() private UXNavigationScope FindTopScope()
{ {
UXNavigationScope bestScope = null; UXNavigationScope bestScope = null;
for (int i = 0; i < _scopeCapacityHighWater; i++) for (int i = 0; i < _scopeCount; i++)
{ {
UXNavigationScope scope = _scopes[i]; UXNavigationScope scope = _scopes[i];
if (scope == null)
{
continue;
}
bool available = IsScopeAvailable(scope); bool available = IsScopeAvailable(scope);
scope.IsAvailable = available; scope.IsAvailable = available;
if (scope.WasAvailable != available) if (scope.WasAvailable != available)
@ -278,14 +273,9 @@ namespace UnityEngine.UI
private void ApplyScopeSuppression() private void ApplyScopeSuppression()
{ {
for (int i = 0; i < _scopeCapacityHighWater; i++) for (int i = 0; i < _scopeCount; i++)
{ {
UXNavigationScope scope = _scopes[i]; UXNavigationScope scope = _scopes[i];
if (scope == null)
{
continue;
}
bool suppress = scope.IsAvailable bool suppress = scope.IsAvailable
&& _topScope != null && _topScope != null
&& scope != _topScope && scope != _topScope
@ -311,7 +301,9 @@ namespace UnityEngine.UI
} }
Selectable preferred = _topScope.GetPreferredSelectable(); Selectable preferred = _topScope.GetPreferredSelectable();
_isEnsuringSelection = true;
eventSystem.SetSelectedGameObject(preferred != null ? preferred.gameObject : null); eventSystem.SetSelectedGameObject(preferred != null ? preferred.gameObject : null);
_isEnsuringSelection = false;
GameObject selectedObject = eventSystem.currentSelectedGameObject; GameObject selectedObject = eventSystem.currentSelectedGameObject;
if (selectedObject != null) if (selectedObject != null)
{ {
@ -321,9 +313,9 @@ namespace UnityEngine.UI
private void RecordSelection(GameObject selectedObject) private void RecordSelection(GameObject selectedObject)
{ {
if (_stateDirty || _suppressionDirty) if (!_isEnsuringSelection && (_stateDirty || _suppressionDirty))
{ {
FlushStateIfDirty(); FlushStateIfDirty(true);
} }
if (_topScope != null && _topScope.IsSelectableOwnedAndValid(selectedObject)) if (_topScope != null && _topScope.IsSelectableOwnedAndValid(selectedObject))
@ -334,12 +326,54 @@ namespace UnityEngine.UI
private void OnInputModeChanged(UXInputMode mode) private void OnInputModeChanged(UXInputMode mode)
{ {
_cursorPolicy?.OnInputModeChanged(mode, _topScope); UXNavigationScope previousTopScope = _topScope;
if (mode == UXInputMode.Gamepad || mode == UXInputMode.Keyboard) if (mode == UXInputMode.Gamepad || mode == UXInputMode.Keyboard)
{ {
FlushStateIfDirty(); FlushStateIfDirty(false);
EnsureNavigationSelection();
} }
NotifyContextIfChanged(previousTopScope, _topScope);
}
private void SetTopScope(UXNavigationScope topScope, bool notifyContext)
{
if (ReferenceEquals(_topScope, topScope))
{
return;
}
UXNavigationScope previousTopScope = _topScope;
_topScope = topScope;
_suppressionDirty = true;
if (notifyContext)
{
NotifyContextIfChanged(previousTopScope, _topScope);
}
else
{
QueueContextNotification(previousTopScope);
}
}
private void QueueContextNotification(UXNavigationScope previousTopScope)
{
if (!_contextNotificationDirty)
{
_pendingPreviousTopScope = previousTopScope;
_contextNotificationDirty = true;
}
}
private void NotifyContextIfChanged(UXNavigationScope previousTopScope, UXNavigationScope currentTopScope)
{
if (_contextNotificationDirty)
{
previousTopScope = _pendingPreviousTopScope;
_pendingPreviousTopScope = null;
_contextNotificationDirty = false;
}
_cursorPolicy?.OnNavigationContextChanged(UXInputModeService.CurrentMode, previousTopScope, currentTopScope);
} }
private static bool IsHigherPriority(UXNavigationScope left, UXNavigationScope right) private static bool IsHigherPriority(UXNavigationScope left, UXNavigationScope right)
@ -361,10 +395,10 @@ namespace UnityEngine.UI
return left.ActivationSerial > right.ActivationSerial; return left.ActivationSerial > right.ActivationSerial;
} }
private static void ReportCapacityExceeded(string message) private static void ReportCapacityExceeded()
{ {
#if UNITY_EDITOR || DEVELOPMENT_BUILD #if UNITY_EDITOR || DEVELOPMENT_BUILD
Debug.LogError(message); Debug.LogError("UXNavigationRuntime scope capacity exceeded.");
#endif #endif
} }
} }

View File

@ -1,4 +1,4 @@
#if INPUTSYSTEM_SUPPORT && UX_NAVIGATION #if INPUTSYSTEM_SUPPORT && UX_NAVIGATION
using AlicizaX.UI.Runtime; using AlicizaX.UI.Runtime;
using UnityEngine.EventSystems; using UnityEngine.EventSystems;
@ -30,6 +30,14 @@ namespace UnityEngine.UI
private bool _cachedIsSkipped; private bool _cachedIsSkipped;
private bool _isSkippedCacheValid; private bool _isSkippedCacheValid;
private int _runtimeSelectableCount; private int _runtimeSelectableCount;
private int[] _bakedSelectableHashIds = System.Array.Empty<int>();
private int[] _bakedSelectableHashIndices = System.Array.Empty<int>();
private int[] _runtimeSelectableHashIds = System.Array.Empty<int>();
private int[] _runtimeSelectableHashIndices = System.Array.Empty<int>();
private bool _selectableSetDirty = true;
private bool _selectableAvailabilityDirty = true;
private int _availableSelectableCount;
private Selectable _firstAvailableSelectable;
internal int RuntimeIndex { get; set; } = InvalidIndex; internal int RuntimeIndex { get; set; } = InvalidIndex;
internal ulong ActivationSerial { get; set; } internal ulong ActivationSerial { get; set; }
@ -45,6 +53,7 @@ namespace UnityEngine.UI
set set
{ {
_defaultSelectable = value; _defaultSelectable = value;
MarkSelectableAvailabilityDirty();
MarkRuntimeStateDirty(); MarkRuntimeStateDirty();
} }
} }
@ -59,7 +68,7 @@ namespace UnityEngine.UI
{ {
if (!_isSkippedCacheValid) if (!_isSkippedCacheValid)
{ {
_cachedIsSkipped = GetComponentInParent<UXNavigationSkip>(true) != null; _cachedIsSkipped = HasSkipInParents();
_isSkippedCacheValid = true; _isSkippedCacheValid = true;
} }
@ -67,6 +76,22 @@ namespace UnityEngine.UI
} }
} }
private bool HasSkipInParents()
{
Transform current = transform;
while (current != null)
{
if (current.TryGetComponent(out UXNavigationSkip skip))
{
return true;
}
current = current.parent;
}
return false;
}
internal Canvas Canvas internal Canvas Canvas
{ {
get get
@ -99,14 +124,14 @@ namespace UnityEngine.UI
private void Awake() private void Awake()
{ {
EnsureRuntimeBuffers(); EnsureRuntimeBuffers(false);
CaptureAllBaselines(); RefreshBaselineWhenUnsuppressed();
} }
private void OnEnable() private void OnEnable()
{ {
EnsureRuntimeBuffers(); EnsureRuntimeBuffers(false);
CaptureAllBaselines(); RefreshBaselineWhenUnsuppressed();
UXNavigationRuntime.EnsureInstance().RegisterScope(this); UXNavigationRuntime.EnsureInstance().RegisterScope(this);
} }
@ -126,6 +151,7 @@ namespace UnityEngine.UI
private void OnTransformChildrenChanged() private void OnTransformChildrenChanged()
{ {
MarkSelectableAvailabilityDirty();
MarkRuntimeStateDirty(); MarkRuntimeStateDirty();
} }
@ -155,21 +181,21 @@ namespace UnityEngine.UI
return false; return false;
} }
EnsureRuntimeBuffers(); EnsureRuntimeBuffers(true);
if (_runtimeSelectableCount >= _runtimeSelectables.Length) if (_runtimeSelectableCount >= _runtimeSelectables.Length)
{ {
ReportCapacityExceeded(); ReportCapacityExceeded();
return false; return false;
} }
_runtimeSelectables[_runtimeSelectableCount] = selectable; int index = _runtimeSelectableCount++;
_runtimeBaselineNavigation[_runtimeSelectableCount] = selectable.navigation; _runtimeSelectables[index] = selectable;
_runtimeSelectableCount++; _runtimeBaselineNavigation[index] = selectable.navigation;
MarkSelectableSetDirty();
if (_navigationSuppressed) if (_navigationSuppressed)
{ {
SetSelectableSuppressed(selectable, true); SetSelectableSuppressed(selectable);
} }
MarkRuntimeStateDirty(); MarkRuntimeStateDirty();
return true; return true;
} }
@ -181,39 +207,51 @@ namespace UnityEngine.UI
return false; return false;
} }
for (int i = 0; i < _runtimeSelectableCount; i++) int index = FindRuntimeIndex(selectable);
if (index < 0)
{ {
if (_runtimeSelectables[i] != selectable) return false;
{
continue;
} }
if (_navigationSuppressed) if (_navigationSuppressed)
{ {
selectable.navigation = _runtimeBaselineNavigation[i]; selectable.navigation = _runtimeBaselineNavigation[index];
} }
int last = _runtimeSelectableCount - 1; int last = --_runtimeSelectableCount;
_runtimeSelectables[i] = _runtimeSelectables[last]; Selectable movedSelectable = _runtimeSelectables[last];
_runtimeBaselineNavigation[i] = _runtimeBaselineNavigation[last]; Navigation movedNavigation = _runtimeBaselineNavigation[last];
_runtimeSelectables[last] = null; _runtimeSelectables[last] = null;
_runtimeBaselineNavigation[last] = default(Navigation); _runtimeBaselineNavigation[last] = default(Navigation);
_runtimeSelectableCount--; if (index != last)
{
_runtimeSelectables[index] = movedSelectable;
_runtimeBaselineNavigation[index] = movedNavigation;
}
if (_lastSelected == selectable) if (_lastSelected == selectable)
{ {
_lastSelected = null; _lastSelected = null;
} }
MarkSelectableSetDirty();
MarkRuntimeStateDirty(); MarkRuntimeStateDirty();
return true; return true;
} }
return false;
}
public void InvalidateSelectableCache() public void InvalidateSelectableCache()
{ {
CaptureAllBaselines(); if (_navigationSuppressed)
{
ApplySuppression(_bakedSelectables, _bakedBaselineNavigation, BakedSelectableCount, false);
ApplySuppression(_runtimeSelectables, _runtimeBaselineNavigation, _runtimeSelectableCount, false);
CaptureBaselineBeforeSuppress();
ApplySuppression(_bakedSelectables, _bakedBaselineNavigation, BakedSelectableCount, true);
ApplySuppression(_runtimeSelectables, _runtimeBaselineNavigation, _runtimeSelectableCount, true);
}
else
{
RefreshBaselineWhenUnsuppressed();
}
MarkRuntimeStateDirty(); MarkRuntimeStateDirty();
} }
@ -260,6 +298,7 @@ namespace UnityEngine.UI
internal Selectable GetPreferredSelectable() internal Selectable GetPreferredSelectable()
{ {
RefreshSelectableAvailabilityIfDirty();
if (_rememberLastSelection && IsSelectableValid(_lastSelected)) if (_rememberLastSelection && IsSelectableValid(_lastSelected))
{ {
return _lastSelected; return _lastSelected;
@ -270,25 +309,18 @@ namespace UnityEngine.UI
return _defaultSelectable; return _defaultSelectable;
} }
if (!_autoSelectFirstAvailable) return _autoSelectFirstAvailable ? _firstAvailableSelectable : null;
{
return null;
}
Selectable selectable = FirstUsable(_bakedSelectables, BakedSelectableCount);
if (selectable != null)
{
return selectable;
}
return FirstUsable(_runtimeSelectables, _runtimeSelectableCount);
} }
internal bool HasAvailableSelectable() internal bool HasAvailableSelectable()
{ {
return IsSelectableValid(_defaultSelectable) RefreshSelectableAvailabilityIfDirty();
|| FirstUsable(_bakedSelectables, BakedSelectableCount) != null if (IsSelectableValid(_defaultSelectable))
|| FirstUsable(_runtimeSelectables, _runtimeSelectableCount) != null; {
return true;
}
return _autoSelectFirstAvailable && _availableSelectableCount > 0;
} }
internal void RecordSelection(GameObject selectedObject) internal void RecordSelection(GameObject selectedObject)
@ -317,36 +349,78 @@ namespace UnityEngine.UI
return; return;
} }
CaptureAllBaselines(); if (suppressed)
{
CaptureBaselineBeforeSuppress();
}
_navigationSuppressed = suppressed; _navigationSuppressed = suppressed;
ApplySuppression(_bakedSelectables, _bakedBaselineNavigation, BakedSelectableCount, suppressed); ApplySuppression(_bakedSelectables, _bakedBaselineNavigation, BakedSelectableCount, suppressed);
ApplySuppression(_runtimeSelectables, _runtimeBaselineNavigation, _runtimeSelectableCount, suppressed); ApplySuppression(_runtimeSelectables, _runtimeBaselineNavigation, _runtimeSelectableCount, suppressed);
} }
private void EnsureRuntimeBuffers() private void EnsureRuntimeBuffers(bool preserveRuntimeSelectables)
{ {
int capacity = _runtimeSelectableCapacity > 0 ? _runtimeSelectableCapacity : 0; int capacity = _runtimeSelectableCapacity > 0 ? _runtimeSelectableCapacity : 0;
if (_runtimeSelectables == null || _runtimeSelectables.Length != capacity) if (_runtimeSelectables == null || _runtimeSelectables.Length != capacity)
{ {
Selectable[] previousSelectables = _runtimeSelectables;
Navigation[] previousBaseline = _runtimeBaselineNavigation;
int previousCount = _runtimeSelectableCount;
_runtimeSelectables = capacity > 0 ? new Selectable[capacity] : System.Array.Empty<Selectable>(); _runtimeSelectables = capacity > 0 ? new Selectable[capacity] : System.Array.Empty<Selectable>();
_runtimeBaselineNavigation = capacity > 0 ? new Navigation[capacity] : System.Array.Empty<Navigation>(); _runtimeBaselineNavigation = capacity > 0 ? new Navigation[capacity] : System.Array.Empty<Navigation>();
CreateRuntimeHash(capacity);
_runtimeSelectableCount = 0; _runtimeSelectableCount = 0;
if (preserveRuntimeSelectables && previousSelectables != null && capacity > 0)
{
int copyCount = previousCount < capacity ? previousCount : capacity;
for (int i = 0; i < copyCount; i++)
{
Selectable selectable = previousSelectables[i];
if (selectable == null)
{
continue;
}
_runtimeSelectables[_runtimeSelectableCount] = selectable;
_runtimeBaselineNavigation[_runtimeSelectableCount] = previousBaseline != null && i < previousBaseline.Length
? previousBaseline[i]
: selectable.navigation;
_runtimeSelectableCount++;
}
}
MarkSelectableSetDirty();
} }
int bakedCount = BakedSelectableCount; int bakedCount = BakedSelectableCount;
if (_bakedBaselineNavigation == null || _bakedBaselineNavigation.Length != bakedCount) if (_bakedBaselineNavigation == null || _bakedBaselineNavigation.Length != bakedCount)
{ {
_bakedBaselineNavigation = bakedCount > 0 ? new Navigation[bakedCount] : System.Array.Empty<Navigation>(); _bakedBaselineNavigation = bakedCount > 0 ? new Navigation[bakedCount] : System.Array.Empty<Navigation>();
CreateBakedHash(bakedCount);
MarkSelectableSetDirty();
} }
} }
private void CaptureAllBaselines() private void CaptureBaselineBeforeSuppress()
{ {
EnsureRuntimeBuffers(); EnsureRuntimeBuffers(true);
RefreshSelectableHashesIfDirty();
CaptureBaseline(_bakedSelectables, _bakedBaselineNavigation, BakedSelectableCount); CaptureBaseline(_bakedSelectables, _bakedBaselineNavigation, BakedSelectableCount);
CaptureBaseline(_runtimeSelectables, _runtimeBaselineNavigation, _runtimeSelectableCount); CaptureBaseline(_runtimeSelectables, _runtimeBaselineNavigation, _runtimeSelectableCount);
} }
private void RefreshBaselineWhenUnsuppressed()
{
if (_navigationSuppressed)
{
return;
}
CaptureBaselineBeforeSuppress();
}
private static void CaptureBaseline(Selectable[] selectables, Navigation[] baseline, int count) private static void CaptureBaseline(Selectable[] selectables, Navigation[] baseline, int count)
{ {
if (selectables == null || baseline == null) if (selectables == null || baseline == null)
@ -381,7 +455,7 @@ namespace UnityEngine.UI
if (suppressed) if (suppressed)
{ {
SetSelectableSuppressed(selectable, true); SetSelectableSuppressed(selectable);
} }
else else
{ {
@ -390,9 +464,9 @@ namespace UnityEngine.UI
} }
} }
private static void SetSelectableSuppressed(Selectable selectable, bool suppressed) private static void SetSelectableSuppressed(Selectable selectable)
{ {
if (selectable == null || !suppressed) if (selectable == null)
{ {
return; return;
} }
@ -404,8 +478,15 @@ namespace UnityEngine.UI
private bool ContainsSelectable(Selectable selectable) private bool ContainsSelectable(Selectable selectable)
{ {
return IndexOf(_bakedSelectables, BakedSelectableCount, selectable) >= 0 if (selectable == null)
|| IndexOf(_runtimeSelectables, _runtimeSelectableCount, selectable) >= 0; {
return false;
}
RefreshSelectableHashesIfDirty();
int instanceId = selectable.GetInstanceID();
return FindHashIndex(_bakedSelectableHashIds, _bakedSelectableHashIndices, instanceId) >= 0
|| FindHashIndex(_runtimeSelectableHashIds, _runtimeSelectableHashIndices, instanceId) >= 0;
} }
private bool IsSelectableValid(Selectable selectable) private bool IsSelectableValid(Selectable selectable)
@ -413,25 +494,204 @@ namespace UnityEngine.UI
return IsSelectableUsable(selectable) && ContainsSelectable(selectable); return IsSelectableUsable(selectable) && ContainsSelectable(selectable);
} }
private static Selectable FirstUsable(Selectable[] selectables, int count) private void MarkSelectableSetDirty()
{
_selectableSetDirty = true;
MarkSelectableAvailabilityDirty();
}
private void MarkSelectableAvailabilityDirty()
{
_selectableAvailabilityDirty = true;
}
public void NotifySelectableStateChanged()
{
MarkSelectableAvailabilityDirty();
MarkRuntimeStateDirty();
}
private void RefreshSelectableAvailabilityIfDirty()
{
if (!_selectableAvailabilityDirty)
{
return;
}
_availableSelectableCount = 0;
_firstAvailableSelectable = null;
AccumulateAvailableSelectables(_bakedSelectables, BakedSelectableCount);
AccumulateAvailableSelectables(_runtimeSelectables, _runtimeSelectableCount);
_selectableAvailabilityDirty = false;
}
private void AccumulateAvailableSelectables(Selectable[] selectables, int count)
{ {
if (selectables == null) if (selectables == null)
{ {
return null; return;
} }
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++)
{ {
Selectable selectable = selectables[i]; Selectable selectable = selectables[i];
if (IsSelectableUsable(selectable)) if (!IsSelectableUsable(selectable))
{ {
return selectable; continue;
}
if (_firstAvailableSelectable == null)
{
_firstAvailableSelectable = selectable;
}
_availableSelectableCount++;
}
}
private void RefreshSelectableHashesIfDirty()
{
if (!_selectableSetDirty)
{
return;
}
RebuildBakedHash();
RebuildRuntimeHash();
_selectableSetDirty = false;
}
private void RebuildBakedHash()
{
ClearHash(_bakedSelectableHashIds, _bakedSelectableHashIndices);
for (int i = 0; i < BakedSelectableCount; i++)
{
Selectable selectable = _bakedSelectables[i];
if (selectable != null)
{
AddHash(_bakedSelectableHashIds, _bakedSelectableHashIndices, selectable.GetInstanceID(), i);
}
} }
} }
return null; private void RebuildRuntimeHash()
{
ClearHash(_runtimeSelectableHashIds, _runtimeSelectableHashIndices);
for (int i = 0; i < _runtimeSelectableCount; i++)
{
Selectable selectable = _runtimeSelectables[i];
if (selectable != null)
{
AddHash(_runtimeSelectableHashIds, _runtimeSelectableHashIndices, selectable.GetInstanceID(), i);
}
}
} }
private void CreateBakedHash(int itemCapacity)
{
int hashCapacity = GetHashCapacity(itemCapacity);
_bakedSelectableHashIds = hashCapacity > 0 ? new int[hashCapacity] : System.Array.Empty<int>();
_bakedSelectableHashIndices = hashCapacity > 0 ? new int[hashCapacity] : System.Array.Empty<int>();
}
private void CreateRuntimeHash(int itemCapacity)
{
int hashCapacity = GetHashCapacity(itemCapacity);
_runtimeSelectableHashIds = hashCapacity > 0 ? new int[hashCapacity] : System.Array.Empty<int>();
_runtimeSelectableHashIndices = hashCapacity > 0 ? new int[hashCapacity] : System.Array.Empty<int>();
}
private static int GetHashCapacity(int itemCapacity)
{
if (itemCapacity <= 0)
{
return 0;
}
int hashCapacity = 1;
int required = itemCapacity << 1;
while (hashCapacity < required)
{
hashCapacity <<= 1;
}
return hashCapacity;
}
private static void ClearHash(int[] ids, int[] indices)
{
if (ids == null || indices == null)
{
return;
}
for (int i = 0; i < ids.Length; i++)
{
ids[i] = 0;
indices[i] = InvalidIndex;
}
}
private static int FindHashIndex(int[] ids, int[] indices, int instanceId)
{
if (ids == null || indices == null || ids.Length == 0 || instanceId == 0)
{
return InvalidIndex;
}
int mask = ids.Length - 1;
int index = instanceId & mask;
for (int i = 0; i < ids.Length; i++)
{
int storedId = ids[index];
if (storedId == 0)
{
return InvalidIndex;
}
if (storedId == instanceId)
{
return indices[index];
}
index = (index + 1) & mask;
}
return InvalidIndex;
}
private static void AddHash(int[] ids, int[] indices, int instanceId, int selectableIndex)
{
if (ids == null || indices == null || ids.Length == 0 || instanceId == 0)
{
return;
}
int mask = ids.Length - 1;
int index = instanceId & mask;
for (int i = 0; i < ids.Length; i++)
{
int storedId = ids[index];
if (storedId == 0 || storedId == instanceId)
{
ids[index] = instanceId;
indices[index] = selectableIndex;
return;
}
index = (index + 1) & mask;
}
}
private int FindRuntimeIndex(Selectable selectable)
{
if (selectable == null)
{
return InvalidIndex;
}
RefreshSelectableHashesIfDirty();
return FindHashIndex(_runtimeSelectableHashIds, _runtimeSelectableHashIndices, selectable.GetInstanceID());
}
private static int IndexOf(Selectable[] selectables, int count, Selectable selectable) private static int IndexOf(Selectable[] selectables, int count, Selectable selectable)
{ {
if (selectables == null || selectable == null) if (selectables == null || selectable == null)
@ -484,3 +744,11 @@ namespace UnityEngine.UI
} }
} }
#endif #endif