[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()
{
// 先绑定所有 SerializedProperty
InitializeLayoutManagerProperties();
InitializeScrollerProperties();
InitializeBaseProperties();
InitializeTemplateProperties();
// 确保序列化对象是最新的
serializedObject.Update();
// 如果 layoutManager 的 managedReferenceValue 丢失但有记录的 typeName则尝试恢复实例
RestoreLayoutManagerFromTypeNameIfMissing();
// 如果 scroller 组件丢失但有记录的 typeName则尝试恢复组件到目标 GameObject 上
RestoreScrollerFromTypeNameIfMissing();
// 应用修改(若有)
serializedObject.ApplyModifiedProperties();
}
@ -134,6 +124,7 @@ namespace AlicizaX.UI.Editor
serializedObject.Update();
bool isPlaying = Application.isPlaying;
DrawMissingReferenceRepairSection(isPlaying);
DrawLayoutManagerSection(isPlaying);
DrawBaseSettingsSection(isPlaying);
DrawScrollerSection(isPlaying);
@ -144,6 +135,71 @@ namespace AlicizaX.UI.Editor
#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
private void DrawLayoutManagerSection(bool isPlaying)
@ -697,6 +753,106 @@ namespace AlicizaX.UI.Editor
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
#region Helper Methods

View File

@ -1,4 +1,4 @@
#if INPUTSYSTEM_SUPPORT && UX_NAVIGATION
#if INPUTSYSTEM_SUPPORT && UX_NAVIGATION
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
@ -9,6 +9,7 @@ namespace AlicizaX.UI.Extension.Editor
[CustomEditor(typeof(UXNavigationScope))]
public sealed class UXNavigationScopeEditor : UnityEditor.Editor
{
private readonly List<Selectable> _selectableBuffer = new List<Selectable>(64);
private SerializedProperty _defaultSelectable;
private SerializedProperty _bakedSelectables;
private SerializedProperty _runtimeSelectableCapacity;
@ -55,7 +56,7 @@ namespace AlicizaX.UI.Extension.Editor
{
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("收集 Selectable"))
if (GUILayout.Button("收集本 Scope Selectable"))
{
BakeSelectables();
}
@ -70,6 +71,19 @@ namespace AlicizaX.UI.Extension.Editor
SortBakedSelectables();
}
}
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("烘焙当前 Prefab 所有 Scope"))
{
BakeAllScopesInRoot();
}
if (GUILayout.Button("校验当前 Prefab 所有 Scope"))
{
ValidateAllScopesInRoot();
}
}
}
private void DrawDiagnostics()
@ -105,32 +119,22 @@ namespace AlicizaX.UI.Extension.Editor
EditorGUILayout.HelpBox("烘焙列表存在跨 Scope 引用。", MessageType.Error);
return;
}
for (int j = i + 1; j < _bakedSelectables.arraySize; j++)
{
if (_bakedSelectables.GetArrayElementAtIndex(j).objectReferenceValue == selectable)
{
EditorGUILayout.HelpBox("烘焙列表存在重复引用。", MessageType.Error);
return;
}
}
}
}
private void BakeSelectables()
{
UXNavigationScope scope = (UXNavigationScope)target;
Selectable[] allSelectables = scope.GetComponentsInChildren<Selectable>(true);
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);
BakeScope(scope, serializedObject, _bakedSelectables, _selectableBuffer);
}
private void RemoveNullEntries()
@ -152,10 +156,85 @@ namespace AlicizaX.UI.Extension.Editor
private void SortBakedSelectables()
{
UXNavigationScope scope = (UXNavigationScope)target;
List<Selectable> selectables = new List<Selectable>(_bakedSelectables.arraySize);
for (int i = 0; i < _bakedSelectables.arraySize; i++)
SortScope(scope, serializedObject, _bakedSelectables);
}
private void BakeAllScopesInRoot()
{
GameObject root = GetRootGameObject(((UXNavigationScope)target).gameObject);
UXNavigationScope[] scopes = root.GetComponentsInChildren<UXNavigationScope>(true);
for (int i = 0; i < scopes.Length; i++)
{
Selectable selectable = _bakedSelectables.GetArrayElementAtIndex(i).objectReferenceValue as Selectable;
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)
{
selectables.Add(selectable);
@ -164,16 +243,70 @@ namespace AlicizaX.UI.Extension.Editor
selectables.Sort(CompareSiblingPath);
Undo.RecordObject(scope, "Sort UX Navigation Selectables");
_bakedSelectables.arraySize = selectables.Count;
bakedSelectables.arraySize = selectables.Count;
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);
}
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)
{
if (left == right)
@ -181,22 +314,67 @@ namespace AlicizaX.UI.Extension.Editor
return 0;
}
string leftPath = GetSiblingPath(left.transform);
string rightPath = GetSiblingPath(right.transform);
return string.CompareOrdinal(leftPath, rightPath);
Transform leftTransform = left != null ? left.transform : null;
Transform rightTransform = right != null ? right.transform : null;
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
? transform.GetSiblingIndex().ToString("D4")
: GetSiblingPath(transform.parent) + "/" + transform.GetSiblingIndex().ToString("D4");
if (left == null)
{
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

View File

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

View File

@ -7,6 +7,7 @@ namespace AlicizaX.UI
{
private readonly List<TData> showList = new();
private readonly string groupViewName;
private readonly Dictionary<int, TData> groupsByType = new();
public GroupAdapter(RecyclerView recyclerView, string groupViewName) : base(recyclerView)
{
@ -42,12 +43,16 @@ namespace AlicizaX.UI
{
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)
{
showList.Clear();
groupsByType.Clear();
base.NotifyDataChanged();
return;
}
@ -80,21 +85,24 @@ namespace AlicizaX.UI
public override void SetList(List<TData> list)
{
showList.Clear();
groupsByType.Clear();
base.SetList(list);
}
private void CreateGroup(int type)
{
var groupData = showList.Find(data => data.Type == type && data.TemplateName == groupViewName);
if (groupData == null)
if (groupsByType.ContainsKey(type))
{
groupData = new TData
{
TemplateName = groupViewName,
Type = type
};
showList.Add(groupData);
return;
}
TData groupData = new TData
{
TemplateName = groupViewName,
Type = type
};
groupsByType[type] = groupData;
showList.Add(groupData);
}
public void Expand(int index)

View File

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

View File

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

View File

@ -94,14 +94,20 @@ namespace AlicizaX.UI
public override void DoItemAnimation()
{
var viewHolders = viewProvider.ViewHolders;
for (int i = 0; i < viewHolders.Count; i++)
int visibleCount = viewProvider.VisibleCount;
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);
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 readonly Dictionary<string, Stack<T>> entries;
private readonly Dictionary<T, string> activeEntries;
private readonly Dictionary<string, int> typeSize;
private readonly Dictionary<string, int> activeCountByType;
private readonly Dictionary<string, int> peakActiveByType;
@ -17,6 +18,7 @@ namespace AlicizaX.UI
private int hitCount;
private int missCount;
private int destroyCount;
private bool disposed;
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);
activeEntries = new Dictionary<T, string>();
typeSize = new Dictionary<string, int>(StringComparer.Ordinal);
activeCountByType = 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)
{
if (disposed)
{
return null;
}
Stack<T> stack = GetOrCreateStack(typeName);
if (stack.Count > 0)
{
T obj = stack.Pop();
hitCount++;
activeEntries[obj] = typeName;
TrackAllocate(typeName);
return obj;
}
missCount++;
T created = factory.Create(typeName);
activeEntries[created] = typeName;
TrackAllocate(typeName);
return factory.Create(typeName);
return created;
}
public void Free(string typeName, T obj)
@ -60,6 +71,16 @@ namespace AlicizaX.UI
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);
destroyCount++;
TrackFree(typeName);
@ -70,6 +91,7 @@ namespace AlicizaX.UI
Stack<T> stack = GetOrCreateStack(typeName);
factory.Reset(typeName, obj);
activeEntries.Remove(obj);
TrackFree(typeName);
if (stack.Count >= maxSize)
@ -99,6 +121,11 @@ namespace AlicizaX.UI
public void EnsureCapacity(string typeName, int value)
{
if (disposed)
{
return;
}
if (value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value));
@ -113,6 +140,11 @@ namespace AlicizaX.UI
public void Warm(string typeName, int count)
{
if (disposed)
{
return;
}
if (count <= 0)
{
return;
@ -169,8 +201,14 @@ namespace AlicizaX.UI
peakActiveByType.Clear();
}
public void ClearInactive()
{
Clear();
}
public void Dispose()
{
disposed = true;
Clear();
GC.SuppressFinalize(this);
}

View File

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

View File

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

View File

@ -50,6 +50,11 @@ namespace AlicizaX.UI
obj.transform.localScale = Vector3.one;
}
if (obj is ViewHolder viewHolder)
{
viewHolder.RefreshInteractionCache();
}
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 void Awake()
protected override void Awake()
{
base.Awake();
RectTransform rectTransform = GetComponent<RectTransform>();
Vector2 position = transform.position;
Vector2 size = rectTransform.sizeDelta;

View File

@ -14,11 +14,13 @@ namespace AlicizaX.UI
private bool dragging;
private bool hovering;
private float targetHandleScale = 1f;
private void Awake()
{
scrollbar = GetComponent<Scrollbar>();
handle = scrollbar.handleRect;
targetHandleScale = GetCurrentHandleScale();
}
public void OnBeginDrag(PointerEventData eventData)
@ -31,23 +33,7 @@ namespace AlicizaX.UI
dragging = false;
if (!hovering)
{
if (scrollbar.direction == Scrollbar.Direction.TopToBottom ||
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
}
SetHandleScale(1f);
}
OnDragEnd?.Invoke();
@ -56,24 +42,7 @@ namespace AlicizaX.UI
public void OnPointerEnter(PointerEventData eventData)
{
hovering = true;
if (scrollbar.direction == Scrollbar.Direction.TopToBottom ||
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
}
SetHandleScale(2f);
}
public void OnPointerExit(PointerEventData eventData)
@ -81,24 +50,58 @@ namespace AlicizaX.UI
hovering = false;
if (!dragging)
{
if (scrollbar.direction == Scrollbar.Direction.TopToBottom ||
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
}
SetHandleScale(1f);
}
}
private void SetHandleScale(float target)
{
if (handle == null || Mathf.Approximately(targetHandleScale, target))
{
return;
}
targetHandleScale = target;
bool vertical = IsVerticalScrollbar();
#if PRIMETWEEN_SUPPORT
if (vertical)
{
PrimeTween.Tween.ScaleX(handle, target, 0.2f);
}
else
{
PrimeTween.Tween.ScaleY(handle, target, 0.2f);
}
#else
Vector3 scale = handle.localScale;
if (vertical)
{
scale.x = target;
}
else
{
scale.y = target;
}
handle.localScale = scale;
#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.EventSystems;
@ -6,16 +5,27 @@ namespace AlicizaX.UI
{
public class Scroller : MonoBehaviour, IScroller, IBeginDragHandler, IEndDragHandler, IDragHandler, IScrollHandler
{
private static readonly WaitForEndOfFrame EndOfFrameYield = new();
private Coroutine movementCoroutine;
protected enum MotionState
{
Idle,
Smooth,
Duration,
Inertia
}
protected float position;
public float Position { get => position; set => position = value; }
public float Position
{
get => position;
set => position = value;
}
protected float velocity;
public float Velocity => velocity;
protected Direction direction;
public Direction Direction
{
get => direction;
@ -23,6 +33,7 @@ namespace AlicizaX.UI
}
protected Vector2 contentSize;
public Vector2 ContentSize
{
get => contentSize;
@ -30,6 +41,7 @@ namespace AlicizaX.UI
}
protected Vector2 viewSize;
public Vector2 ViewSize
{
get => viewSize;
@ -37,6 +49,7 @@ namespace AlicizaX.UI
}
protected float scrollSpeed = 1f;
public float ScrollSpeed
{
get => scrollSpeed;
@ -44,6 +57,7 @@ namespace AlicizaX.UI
}
protected float wheelSpeed = 30f;
public float WheelSpeed
{
get => wheelSpeed;
@ -51,6 +65,7 @@ namespace AlicizaX.UI
}
protected bool snap;
public bool Snap
{
get => snap;
@ -61,20 +76,72 @@ namespace AlicizaX.UI
protected MoveStopEvent moveStopEvent = new();
protected DraggingEvent draggingEvent = new();
public float MaxPosition => direction == Direction.Vertical ?
Mathf.Max(contentSize.y - viewSize.y, 0) :
Mathf.Max(contentSize.x - viewSize.x, 0);
private MotionState motionState;
private float motionStartPosition;
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 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 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)
{
@ -86,11 +153,10 @@ namespace AlicizaX.UI
{
this.position = position;
OnValueChanged?.Invoke(this.position);
return;
}
else
{
movementCoroutine = StartCoroutine(RunMotion(MoveTo(position)));
}
StartPositionMotion(position, scrollSpeed);
}
public virtual void ScrollToDuration(float position, float duration)
@ -108,7 +174,11 @@ namespace AlicizaX.UI
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)
@ -118,12 +188,22 @@ namespace AlicizaX.UI
public void OnBeginDrag(PointerEventData eventData)
{
if (!InputEnabled)
{
return;
}
OnDragging?.Invoke(true);
StopMovement();
}
public void OnEndDrag(PointerEventData eventData)
{
if (!InputEnabled)
{
return;
}
Inertia();
Elastic();
OnDragging?.Invoke(false);
@ -131,6 +211,11 @@ namespace AlicizaX.UI
public void OnDrag(PointerEventData eventData)
{
if (!InputEnabled)
{
return;
}
dragStopTime = Time.time;
velocity = GetDelta(eventData);
@ -141,6 +226,11 @@ namespace AlicizaX.UI
public void OnScroll(PointerEventData eventData)
{
if (!InputEnabled)
{
return;
}
StopMovement();
float rate = GetScrollRate() * wheelSpeed;
@ -158,32 +248,41 @@ namespace AlicizaX.UI
return direction == Direction.Vertical ? eventData.delta.y * rate : -eventData.delta.x * rate;
}
private float GetScrollRate()
protected float GetScrollRate()
{
float rate = 1f;
float viewLength = ViewLength;
if (viewLength <= 0f)
{
return rate;
}
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)
{
rate = Mathf.Max(0, 1 - (Mathf.Abs(position - MaxPosition) / ViewLength));
rate = Mathf.Max(0, 1 - (Mathf.Abs(position - MaxPosition) / viewLength));
}
return rate;
}
protected virtual void Inertia()
{
if (Mathf.Abs(velocity) <= 0.1f)
{
CompleteMotion(true);
return;
}
if (Mathf.Abs(velocity) > 0.1f)
{
StopMovement();
movementCoroutine = StartCoroutine(RunMotion(InertiaTo()));
}
else
{
OnMoveStoped?.Invoke();
}
StopMovement();
motionState = MotionState.Inertia;
motionStartPosition = position;
motionElapsed = 0f;
motionDuration = snap ? 0.1f : 1f;
inertiaVelocity = velocity > 0 ? Mathf.Min(velocity, 100) : Mathf.Max(velocity, -100);
}
protected virtual void Elastic()
@ -191,98 +290,105 @@ namespace AlicizaX.UI
if (position < 0)
{
StopMovement();
movementCoroutine = StartCoroutine(RunMotion(ElasticTo(0)));
StartPositionMotion(0, 7f);
}
else if (position > MaxPosition)
{
StopMovement();
movementCoroutine = StartCoroutine(RunMotion(ElasticTo(MaxPosition)));
StartPositionMotion(MaxPosition, 7f);
}
}
IEnumerator InertiaTo()
protected void StopMovement()
{
float timer = 0f;
float p = position;
float v = velocity > 0 ? Mathf.Min(velocity, 100) : Mathf.Max(velocity, -100);
float duration = snap ? 0.1f : 1f;
while (timer < duration)
motionState = MotionState.Idle;
}
private void StartPositionMotion(float targetPosition, float speed)
{
motionState = MotionState.Smooth;
motionStartPosition = position;
motionTargetPosition = targetPosition;
motionElapsed = Time.deltaTime;
motionSpeed = speed;
}
private void TickSmooth(float deltaTime)
{
if (Mathf.Abs(motionTargetPosition - position) <= 0.1f)
{
float y = (float)EaseUtil.EaseOutCirc(timer) * 40;
timer += Time.deltaTime;
position = p + y * v;
Elastic();
position = motionTargetPosition;
OnValueChanged?.Invoke(position);
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)
{
CompleteMotion(false);
return;
}
StopCoroutine(movementCoroutine);
movementCoroutine = null;
position = Mathf.Lerp(motionStartPosition, motionTargetPosition, motionElapsed * motionSpeed);
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)
{
return UnregisterItemRender(nameof(itemRenderType));
return _adapter.UnregisterItemRender(itemRenderType);
}

View File

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

View File

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

View File

@ -1,13 +1,19 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace AlicizaX.UI
{
public abstract class ViewHolder : MonoBehaviour
public abstract partial class ViewHolder : MonoBehaviour
{
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
{
@ -40,6 +46,57 @@ namespace AlicizaX.UI
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()
{
AdvanceBindingVersion();
@ -49,10 +106,12 @@ namespace AlicizaX.UI
RecyclerView = null;
}
protected virtual void OnDestroy()
private static bool IsSelectableFocusable(Selectable selectable)
{
Destroyed?.Invoke(this);
Destroyed = null;
return selectable != null &&
selectable.IsActive() &&
selectable.IsInteractable();
}
}
}

View File

@ -1,19 +1,46 @@
using System;
using System.Collections.Generic;
using Cysharp.Text;
namespace AlicizaX.UI
{
public class MixedViewProvider : ViewProvider
{
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, int> warmCounts = new(StringComparer.Ordinal);
private readonly ViewHolder[] cachedTemplates;
private readonly string[] templateNames;
private readonly int[] warmCountsByType;
public override string PoolStats =>
$"hits={objectPool.HitCount}, misses={objectPool.MissCount}, destroys={objectPool.DestroyCount}";
public override string PoolStats
{
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)
{
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++)
{
ViewHolder template = templates[i];
@ -22,7 +49,12 @@ namespace AlicizaX.UI
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);
@ -31,31 +63,17 @@ namespace AlicizaX.UI
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))
{
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;
}
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)
{
var viewHolder = objectPool.Allocate(viewName);
@ -72,7 +90,7 @@ namespace AlicizaX.UI
{
Clear();
(Adapter as IItemRenderCacheOwner)?.ReleaseAllItemRenders();
objectPool.Dispose();
objectPool.ClearInactive();
}
public override void PreparePool()
@ -83,28 +101,36 @@ namespace AlicizaX.UI
return;
}
PrepareBucketPool(warmCount);
int itemCount = GetItemCount();
int start = Math.Max(0, LayoutManager.GetStartIndex());
int end = Math.Min(itemCount - 1, start + warmCount - 1);
warmCounts.Clear();
Array.Clear(warmCountsByType, 0, warmCountsByType.Length);
for (int index = start; index <= end; index++)
{
string viewName = Adapter.GetViewName(index);
if (string.IsNullOrEmpty(viewName))
if (string.IsNullOrEmpty(viewName) || !templateIdsByName.TryGetValue(viewName, out int typeId))
{
continue;
}
warmCounts.TryGetValue(viewName, out int count);
warmCounts[viewName] = count + 1;
warmCountsByType[typeId]++;
}
foreach (var pair in warmCounts)
for (int typeId = 0; typeId < warmCountsByType.Length; typeId++)
{
int targetCount = pair.Value + Math.Max(1, LayoutManager.Unit);
objectPool.EnsureCapacity(pair.Key, targetCount);
objectPool.Warm(pair.Key, targetCount);
int count = warmCountsByType[typeId];
if (count <= 0)
{
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
{
using Cysharp.Text;
public sealed class SimpleViewProvider : ViewProvider
{
private readonly ObjectPool<ViewHolder> objectPool;
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}";
public override string PoolStats
{
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)
{
@ -17,20 +33,7 @@ namespace AlicizaX.UI
public override ViewHolder GetTemplate(string viewName = "")
{
if (templates == null || templates.Length == 0)
{
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;
return templates != null && templates.Length > 0 ? templates[0] : null;
}
public override ViewHolder Allocate(string viewName)
@ -49,7 +52,7 @@ namespace AlicizaX.UI
{
Clear();
(Adapter as IItemRenderCacheOwner)?.ReleaseAllItemRenders();
objectPool.Dispose();
objectPool.ClearInactive();
}
public override void PreparePool()
@ -60,6 +63,8 @@ namespace AlicizaX.UI
return;
}
PrepareBucketPool(warmCount);
objectPool.EnsureCapacity(warmCount);
objectPool.Warm(warmCount);
}

View File

@ -6,16 +6,26 @@ namespace AlicizaX.UI
{
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, List<ViewHolder>> viewHoldersByDataIndex = new();
private readonly Dictionary<int, ViewHolderBucket> viewHoldersByDataIndex = new();
private readonly Dictionary<int, int> viewHolderPositions = new();
public IAdapter Adapter { 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; }
@ -30,8 +40,6 @@ namespace AlicizaX.UI
public abstract ViewHolder GetTemplate(string viewName);
public abstract ViewHolder[] GetTemplates();
public abstract ViewHolder Allocate(string viewName);
public abstract void Free(string viewName, ViewHolder viewHolder);
@ -52,8 +60,19 @@ namespace AlicizaX.UI
viewHolder.Index = i;
viewHolder.DataIndex = i;
viewHolder.RecyclerView = recyclerView;
viewHolders.Add(viewHolder);
RegisterViewHolder(viewHolder);
(Adapter as IItemRenderPrewarmer)?.PrewarmItemRender(viewHolder, viewName);
if (!AddVisibleHolder(viewHolder))
{
Free(viewName, viewHolder);
continue;
}
if (!RegisterViewHolder(viewHolder))
{
RemoveVisibleHolder(viewHolder);
Free(viewName, viewHolder);
continue;
}
LayoutManager.Layout(viewHolder, i);
Adapter.OnBindViewHolder(viewHolder, i);
@ -62,19 +81,37 @@ namespace AlicizaX.UI
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);
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;
viewHolders.RemoveAt(viewHolderIndex);
RemoveVisibleHolder(viewHolder);
UnregisterViewHolder(viewHolder);
RebuildViewHolderPositions(viewHolderIndex);
Adapter?.OnRecycleViewHolder(viewHolder);
viewHolder.OnRecycled();
ClearSelectedState(viewHolder);
@ -91,22 +128,14 @@ namespace AlicizaX.UI
public ViewHolder GetViewHolderByDataIndex(int dataIndex)
{
return viewHoldersByDataIndex.TryGetValue(dataIndex, out List<ViewHolder> holders) &&
holders is { Count: > 0 }
? holders[0]
return viewHoldersByDataIndex.TryGetValue(dataIndex, out ViewHolderBucket bucket) && bucket.Count > 0
? bucket[0]
: 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)
{
holders = list;
return true;
}
holders = null;
return false;
return viewHoldersByDataIndex.TryGetValue(dataIndex, out bucket) && bucket.Count > 0;
}
public int GetViewHolderIndex(int index)
@ -118,8 +147,14 @@ namespace AlicizaX.UI
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;
Adapter?.OnRecycleViewHolder(viewHolder);
UnregisterViewHolder(viewHolder);
@ -128,8 +163,14 @@ namespace AlicizaX.UI
Free(viewName, viewHolder);
}
viewHolders.Clear();
System.Array.Clear(visibleHolders, 0, visibleHolders.Length);
visibleHead = 0;
visibleCount = 0;
viewHoldersByIndex.Clear();
foreach (var pair in viewHoldersByDataIndex)
{
ReleaseBucket(pair.Value);
}
viewHoldersByDataIndex.Clear();
viewHolderPositions.Clear();
}
@ -161,27 +202,47 @@ namespace AlicizaX.UI
int start = Mathf.Max(0, LayoutManager.GetStartIndex());
int end = Mathf.Max(start, LayoutManager.GetEndIndex());
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);
}
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)
{
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;
viewHolderPositions[viewHolder.Index] = viewHolders.Count - 1;
if (!viewHoldersByDataIndex.TryGetValue(viewHolder.DataIndex, out List<ViewHolder> holders))
{
holders = new List<ViewHolder>(1);
viewHoldersByDataIndex[viewHolder.DataIndex] = holders;
}
holders.Add(viewHolder);
viewHolderPositions[viewHolder.Index] = GetVisibleSlot(visibleCount - 1);
bucket.Add(viewHolder);
return true;
}
private void UnregisterViewHolder(ViewHolder viewHolder)
@ -194,35 +255,72 @@ namespace AlicizaX.UI
viewHoldersByIndex.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;
}
// 用末尾元素覆盖目标项再移除末位,避免 List.Remove 的线性搜索+内存搬移。
int idx = holders.LastIndexOf(viewHolder);
if (idx >= 0)
{
holders[idx] = holders[holders.Count - 1];
holders.RemoveAt(holders.Count - 1);
}
if (holders.Count == 0)
bucket.Remove(viewHolder);
if (bucket.Count == 0)
{
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 (holder != null)
{
viewHolderPositions[holder.Index] = i;
}
#if UNITY_EDITOR || DEVELOPMENT_BUILD
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)
{
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)
@ -244,5 +342,132 @@ namespace AlicizaX.UI
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 UnityEngine.InputSystem;
using UnityEngine.InputSystem.Controls;
@ -10,6 +10,30 @@ namespace UnityEngine.UI
private const float StickThresholdSqr = 0.04f;
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 InputAction _pointerAction;
@ -21,12 +45,6 @@ namespace UnityEngine.UI
public static event Action<UXInputMode> OnModeChanged;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
private static void Bootstrap()
{
EnsureInstance();
}
internal static UXInputModeService EnsureInstance()
{
if (_instance != null)
@ -81,35 +99,35 @@ namespace UnityEngine.UI
return;
}
_pointerAction = new InputAction("UXPointerInput", InputActionType.PassThrough);
_pointerAction.AddBinding("<Mouse>/delta");
_pointerAction.AddBinding("<Mouse>/scroll");
_pointerAction.AddBinding("<Mouse>/leftButton");
_pointerAction.AddBinding("<Mouse>/rightButton");
_pointerAction.AddBinding("<Mouse>/middleButton");
_pointerAction = new InputAction(PointerActionName, InputActionType.PassThrough);
_pointerAction.AddBinding(MouseDeltaBinding);
_pointerAction.AddBinding(MouseScrollBinding);
_pointerAction.AddBinding(MouseLeftButtonBinding);
_pointerAction.AddBinding(MouseRightButtonBinding);
_pointerAction.AddBinding(MouseMiddleButtonBinding);
_pointerAction.performed += OnPointerInput;
_keyboardAction = new InputAction("UXKeyboardInput", InputActionType.PassThrough);
_keyboardAction.AddBinding("<Keyboard>/anyKey");
_keyboardAction = new InputAction(KeyboardActionName, InputActionType.PassThrough);
_keyboardAction.AddBinding(KeyboardAnyKeyBinding);
_keyboardAction.performed += OnKeyboardInput;
_gamepadAction = new InputAction("UXGamepadInput", InputActionType.PassThrough);
_gamepadAction.AddBinding("<Gamepad>/buttonSouth");
_gamepadAction.AddBinding("<Gamepad>/buttonNorth");
_gamepadAction.AddBinding("<Gamepad>/buttonEast");
_gamepadAction.AddBinding("<Gamepad>/buttonWest");
_gamepadAction.AddBinding("<Gamepad>/startButton");
_gamepadAction.AddBinding("<Gamepad>/selectButton");
_gamepadAction.AddBinding("<Gamepad>/leftShoulder");
_gamepadAction.AddBinding("<Gamepad>/rightShoulder");
_gamepadAction.AddBinding("<Gamepad>/dpad");
_gamepadAction.AddBinding("<Gamepad>/leftStick");
_gamepadAction.AddBinding("<Gamepad>/rightStick");
_gamepadAction = new InputAction(GamepadActionName, InputActionType.PassThrough);
_gamepadAction.AddBinding(GamepadButtonSouthBinding);
_gamepadAction.AddBinding(GamepadButtonNorthBinding);
_gamepadAction.AddBinding(GamepadButtonEastBinding);
_gamepadAction.AddBinding(GamepadButtonWestBinding);
_gamepadAction.AddBinding(GamepadStartButtonBinding);
_gamepadAction.AddBinding(GamepadSelectButtonBinding);
_gamepadAction.AddBinding(GamepadLeftShoulderBinding);
_gamepadAction.AddBinding(GamepadRightShoulderBinding);
_gamepadAction.AddBinding(GamepadDpadBinding);
_gamepadAction.AddBinding(GamepadLeftStickBinding);
_gamepadAction.AddBinding(GamepadRightStickBinding);
_gamepadAction.performed += OnGamepadInput;
_touchAction = new InputAction("UXTouchInput", InputActionType.PassThrough);
_touchAction.AddBinding("<Touchscreen>/primaryTouch/press");
_touchAction.AddBinding("<Touchscreen>/primaryTouch/delta");
_touchAction = new InputAction(TouchActionName, InputActionType.PassThrough);
_touchAction.AddBinding(TouchPressBinding);
_touchAction.AddBinding(TouchDeltaBinding);
_touchAction.performed += OnTouchInput;
}
@ -222,7 +240,6 @@ namespace UnityEngine.UI
internal static void SetMode(UXInputMode mode)
{
EnsureInstance();
if (CurrentMode == mode)
{
return;
@ -234,3 +251,4 @@ namespace UnityEngine.UI
}
}
#endif

View File

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

View File

@ -1,4 +1,4 @@
#if INPUTSYSTEM_SUPPORT && UX_NAVIGATION
#if INPUTSYSTEM_SUPPORT && UX_NAVIGATION
using AlicizaX.UI.Runtime;
using UnityEngine.EventSystems;
@ -30,6 +30,14 @@ namespace UnityEngine.UI
private bool _cachedIsSkipped;
private bool _isSkippedCacheValid;
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 ulong ActivationSerial { get; set; }
@ -45,6 +53,7 @@ namespace UnityEngine.UI
set
{
_defaultSelectable = value;
MarkSelectableAvailabilityDirty();
MarkRuntimeStateDirty();
}
}
@ -59,7 +68,7 @@ namespace UnityEngine.UI
{
if (!_isSkippedCacheValid)
{
_cachedIsSkipped = GetComponentInParent<UXNavigationSkip>(true) != null;
_cachedIsSkipped = HasSkipInParents();
_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
{
get
@ -99,14 +124,14 @@ namespace UnityEngine.UI
private void Awake()
{
EnsureRuntimeBuffers();
CaptureAllBaselines();
EnsureRuntimeBuffers(false);
RefreshBaselineWhenUnsuppressed();
}
private void OnEnable()
{
EnsureRuntimeBuffers();
CaptureAllBaselines();
EnsureRuntimeBuffers(false);
RefreshBaselineWhenUnsuppressed();
UXNavigationRuntime.EnsureInstance().RegisterScope(this);
}
@ -126,6 +151,7 @@ namespace UnityEngine.UI
private void OnTransformChildrenChanged()
{
MarkSelectableAvailabilityDirty();
MarkRuntimeStateDirty();
}
@ -155,21 +181,21 @@ namespace UnityEngine.UI
return false;
}
EnsureRuntimeBuffers();
EnsureRuntimeBuffers(true);
if (_runtimeSelectableCount >= _runtimeSelectables.Length)
{
ReportCapacityExceeded();
return false;
}
_runtimeSelectables[_runtimeSelectableCount] = selectable;
_runtimeBaselineNavigation[_runtimeSelectableCount] = selectable.navigation;
_runtimeSelectableCount++;
int index = _runtimeSelectableCount++;
_runtimeSelectables[index] = selectable;
_runtimeBaselineNavigation[index] = selectable.navigation;
MarkSelectableSetDirty();
if (_navigationSuppressed)
{
SetSelectableSuppressed(selectable, true);
SetSelectableSuppressed(selectable);
}
MarkRuntimeStateDirty();
return true;
}
@ -181,39 +207,51 @@ namespace UnityEngine.UI
return false;
}
for (int i = 0; i < _runtimeSelectableCount; i++)
int index = FindRuntimeIndex(selectable);
if (index < 0)
{
if (_runtimeSelectables[i] != selectable)
{
continue;
}
if (_navigationSuppressed)
{
selectable.navigation = _runtimeBaselineNavigation[i];
}
int last = _runtimeSelectableCount - 1;
_runtimeSelectables[i] = _runtimeSelectables[last];
_runtimeBaselineNavigation[i] = _runtimeBaselineNavigation[last];
_runtimeSelectables[last] = null;
_runtimeBaselineNavigation[last] = default(Navigation);
_runtimeSelectableCount--;
if (_lastSelected == selectable)
{
_lastSelected = null;
}
MarkRuntimeStateDirty();
return true;
return false;
}
return false;
if (_navigationSuppressed)
{
selectable.navigation = _runtimeBaselineNavigation[index];
}
int last = --_runtimeSelectableCount;
Selectable movedSelectable = _runtimeSelectables[last];
Navigation movedNavigation = _runtimeBaselineNavigation[last];
_runtimeSelectables[last] = null;
_runtimeBaselineNavigation[last] = default(Navigation);
if (index != last)
{
_runtimeSelectables[index] = movedSelectable;
_runtimeBaselineNavigation[index] = movedNavigation;
}
if (_lastSelected == selectable)
{
_lastSelected = null;
}
MarkSelectableSetDirty();
MarkRuntimeStateDirty();
return true;
}
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();
}
@ -260,6 +298,7 @@ namespace UnityEngine.UI
internal Selectable GetPreferredSelectable()
{
RefreshSelectableAvailabilityIfDirty();
if (_rememberLastSelection && IsSelectableValid(_lastSelected))
{
return _lastSelected;
@ -270,25 +309,18 @@ namespace UnityEngine.UI
return _defaultSelectable;
}
if (!_autoSelectFirstAvailable)
{
return null;
}
Selectable selectable = FirstUsable(_bakedSelectables, BakedSelectableCount);
if (selectable != null)
{
return selectable;
}
return FirstUsable(_runtimeSelectables, _runtimeSelectableCount);
return _autoSelectFirstAvailable ? _firstAvailableSelectable : null;
}
internal bool HasAvailableSelectable()
{
return IsSelectableValid(_defaultSelectable)
|| FirstUsable(_bakedSelectables, BakedSelectableCount) != null
|| FirstUsable(_runtimeSelectables, _runtimeSelectableCount) != null;
RefreshSelectableAvailabilityIfDirty();
if (IsSelectableValid(_defaultSelectable))
{
return true;
}
return _autoSelectFirstAvailable && _availableSelectableCount > 0;
}
internal void RecordSelection(GameObject selectedObject)
@ -317,36 +349,78 @@ namespace UnityEngine.UI
return;
}
CaptureAllBaselines();
if (suppressed)
{
CaptureBaselineBeforeSuppress();
}
_navigationSuppressed = suppressed;
ApplySuppression(_bakedSelectables, _bakedBaselineNavigation, BakedSelectableCount, suppressed);
ApplySuppression(_runtimeSelectables, _runtimeBaselineNavigation, _runtimeSelectableCount, suppressed);
}
private void EnsureRuntimeBuffers()
private void EnsureRuntimeBuffers(bool preserveRuntimeSelectables)
{
int capacity = _runtimeSelectableCapacity > 0 ? _runtimeSelectableCapacity : 0;
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>();
_runtimeBaselineNavigation = capacity > 0 ? new Navigation[capacity] : System.Array.Empty<Navigation>();
CreateRuntimeHash(capacity);
_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;
if (_bakedBaselineNavigation == null || _bakedBaselineNavigation.Length != bakedCount)
{
_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(_runtimeSelectables, _runtimeBaselineNavigation, _runtimeSelectableCount);
}
private void RefreshBaselineWhenUnsuppressed()
{
if (_navigationSuppressed)
{
return;
}
CaptureBaselineBeforeSuppress();
}
private static void CaptureBaseline(Selectable[] selectables, Navigation[] baseline, int count)
{
if (selectables == null || baseline == null)
@ -381,7 +455,7 @@ namespace UnityEngine.UI
if (suppressed)
{
SetSelectableSuppressed(selectable, true);
SetSelectableSuppressed(selectable);
}
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;
}
@ -404,8 +478,15 @@ namespace UnityEngine.UI
private bool ContainsSelectable(Selectable selectable)
{
return IndexOf(_bakedSelectables, BakedSelectableCount, selectable) >= 0
|| IndexOf(_runtimeSelectables, _runtimeSelectableCount, selectable) >= 0;
if (selectable == null)
{
return false;
}
RefreshSelectableHashesIfDirty();
int instanceId = selectable.GetInstanceID();
return FindHashIndex(_bakedSelectableHashIds, _bakedSelectableHashIndices, instanceId) >= 0
|| FindHashIndex(_runtimeSelectableHashIds, _runtimeSelectableHashIndices, instanceId) >= 0;
}
private bool IsSelectableValid(Selectable selectable)
@ -413,25 +494,204 @@ namespace UnityEngine.UI
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)
{
return null;
return;
}
for (int i = 0; i < count; 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;
}
return null;
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);
}
}
}
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)
{
if (selectables == null || selectable == null)
@ -484,3 +744,11 @@ namespace UnityEngine.UI
}
}
#endif