[Opt] 优化虚拟列表 优化手柄导航功能
This commit is contained in:
parent
7d5ae32361
commit
1ba8c9a1c8
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -165,7 +165,7 @@ namespace AlicizaX.UI
|
||||
|
||||
public bool UnregisterItemRender(Type itemRenderType)
|
||||
{
|
||||
return UnregisterItemRender(nameof(itemRenderType));
|
||||
return _adapter.UnregisterItemRender(itemRenderType);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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()
|
||||
{
|
||||
@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ac4f0b81367e72b408b7d4a0148d39c3
|
||||
guid: 6114223b5ccfeaa499630f5e4b71120c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user