From 843e0220c26e39312fc855e046956223383574a8 Mon Sep 17 00:00:00 2001 From: rowan Date: Wed, 6 Aug 2025 13:30:28 -0400 Subject: [PATCH] configurable buffers and gutters --- Assets/Scenes/RecycleView.unity | 45 +++++++++ .../UI/Elements/Layout/DynamicLayout.cs | 95 ++++++------------- .../Scripts/UI/Elements/Layout/FixedLayout.cs | 15 +++ Assets/Scripts/UI/Elements/Layout/ILayout.cs | 4 + .../UI/Elements/RecycleView/RecycleView.cs | 51 +++++++++- .../RecycleVirtualizationController.cs | 48 +--------- Assets/UI/Test/Resources/UserProfileList.uxml | 2 +- 7 files changed, 144 insertions(+), 116 deletions(-) diff --git a/Assets/Scenes/RecycleView.unity b/Assets/Scenes/RecycleView.unity index 1f5080d..1bdef73 100644 --- a/Assets/Scenes/RecycleView.unity +++ b/Assets/Scenes/RecycleView.unity @@ -316,6 +316,7 @@ GameObject: - component: {fileID: 1889780345} - component: {fileID: 1889780344} - component: {fileID: 1889780343} + - component: {fileID: 1889780346} m_Layer: 0 m_Name: Main Camera m_TagString: MainCamera @@ -397,6 +398,50 @@ Transform: m_Children: [] m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1889780346 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1889780342} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: a79441f348de89743a2939f4d699eac1, type: 3} + m_Name: + m_EditorClassIdentifier: + m_RenderShadows: 1 + m_RequiresDepthTextureOption: 2 + m_RequiresOpaqueTextureOption: 2 + m_CameraType: 0 + m_Cameras: [] + m_RendererIndex: -1 + m_VolumeLayerMask: + serializedVersion: 2 + m_Bits: 1 + m_VolumeTrigger: {fileID: 0} + m_VolumeFrameworkUpdateModeOption: 2 + m_RenderPostProcessing: 0 + m_Antialiasing: 0 + m_AntialiasingQuality: 2 + m_StopNaN: 0 + m_Dithering: 0 + m_ClearDepth: 1 + m_AllowXRRendering: 1 + m_AllowHDROutput: 1 + m_UseScreenCoordOverride: 0 + m_ScreenSizeOverride: {x: 0, y: 0, z: 0, w: 0} + m_ScreenCoordScaleBias: {x: 0, y: 0, z: 0, w: 0} + m_RequiresDepthTexture: 0 + m_RequiresColorTexture: 0 + m_Version: 2 + m_TaaSettings: + m_Quality: 3 + m_FrameInfluence: 0.1 + m_JitterScale: 1 + m_MipBias: 0 + m_VarianceClampScale: 0.9 + m_ContrastAdaptiveSharpening: 0 --- !u!1660057539 &9223372036854775807 SceneRoots: m_ObjectHideFlags: 0 diff --git a/Assets/Scripts/UI/Elements/Layout/DynamicLayout.cs b/Assets/Scripts/UI/Elements/Layout/DynamicLayout.cs index 86f384e..c2ae2fc 100644 --- a/Assets/Scripts/UI/Elements/Layout/DynamicLayout.cs +++ b/Assets/Scripts/UI/Elements/Layout/DynamicLayout.cs @@ -1,3 +1,4 @@ +using System; using System.Collections; using System.Collections.Generic; using UnityEngine; @@ -5,55 +6,15 @@ using UnityEngine.UIElements; namespace KitsuneCafe.UI { - public record LayoutItem(float Position, float Size) + + public class DynamicLayout : ILayout { - public float Min => Position; - public float Max => Position + Size; - } - - public class DynamicLayout : ILayout, IEnumerable - { - - public struct LayoutEnumerator : IEnumerator - { - public readonly int Start; - public int Index; - private readonly DynamicLayout layout; - - public LayoutEnumerator(int index, DynamicLayout layout) - { - Start = index; - Index = index; - this.layout = layout; - } - - public LayoutEnumerator(DynamicLayout layout) : this(0, layout) { } - - public readonly LayoutItem Current => new( - layout.positions[Index - 1], - layout.sizes[Index - 1] - ); - - readonly object IEnumerator.Current => Current; - - public readonly void Dispose() { } - - public bool MoveNext() - { - Index += 1; - return Index < layout.positions.Count; - } - - public void Reset() - { - Index = Start; - } - } - public float DefaultItemSize { get; set; } public float GutterSize { get; set; } public FlowDirection Direction { get; set; } public float ContentSize { get; private set; } + public int Count { get; set; } + public int Buffer { get; set; } private readonly List positions = new(); private readonly List sizes = new(); @@ -105,6 +66,7 @@ namespace KitsuneCafe.UI sizes.RemoveRange(itemCount, sizes.Count - itemCount); } + Count = itemCount; ContentSize = LastPositionOrDefault(); } @@ -112,7 +74,7 @@ namespace KitsuneCafe.UI { var position = initialPosition; - for (int i = startingIndex; i < positions.Count; i++) + for (int i = startingIndex; i < Count; i++) { positions[i] = position; position += GetItemSize(i) + GutterSize; @@ -134,13 +96,13 @@ namespace KitsuneCafe.UI CalculatePositions(index, position); } - public int GetFirstVisibleIndex(float scrollOffset) + public int GetFirstVisibleIndex(float offset) { - if (scrollOffset <= 0) { return 0; } + if (offset <= 0) { return 0; } - for (int i = 0; i < positions.Count; i++) + for (int i = 0; i < Count; i++) { - if (positions[i] >= scrollOffset) + if (positions[i] >= offset) { return i; } @@ -149,28 +111,29 @@ namespace KitsuneCafe.UI return positions.Count - 1; } - public IEnumerable Enumerate(int startIndex = 0) + public Range GetVisibleRange(float offset, float size) { - var len = positions.Count; - for (int i = startIndex; i < len; i++) + var halfBuffer = Mathf.CeilToInt(Buffer / 2); + + var first = Math.Max( + GetFirstVisibleIndex(offset) - halfBuffer, + 0 + ); + + var count = 1; + var position = GetItemPosition(first); + var width = position + size; + + for (int i = first; i < Count; i++) { - yield return new LayoutItem(positions[i], sizes[i]); + var item = GetItemPosition(i); + if (item > width) { break; } + count += 1; } - } - public IEnumerator GetEnumerator() - { - return new LayoutEnumerator(this); - } + count += halfBuffer; - public IEnumerator GetEnumerator(int startIndex) - { - return new LayoutEnumerator(startIndex, this); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); + return new Range(first, Math.Min(Count, first + count)); } } } diff --git a/Assets/Scripts/UI/Elements/Layout/FixedLayout.cs b/Assets/Scripts/UI/Elements/Layout/FixedLayout.cs index a62309f..523f8ee 100644 --- a/Assets/Scripts/UI/Elements/Layout/FixedLayout.cs +++ b/Assets/Scripts/UI/Elements/Layout/FixedLayout.cs @@ -1,3 +1,5 @@ +using System; +using UnityEngine; using UnityEngine.UIElements; namespace KitsuneCafe.UI @@ -10,6 +12,8 @@ namespace KitsuneCafe.UI public float ItemSize { get; set; } public float GutterSize { get; set; } + public int Count { get; set; } + public int Buffer { get; set; } public FixedLayout(FlowDirection direction, float itemSize, float gutterSize) { @@ -32,7 +36,18 @@ namespace KitsuneCafe.UI public void Update(int itemCount, VisualElement _container) { + Count = itemCount; ContentSize = itemCount * (ItemSize + GutterSize); } + + public Range GetVisibleRange(float offset, float containerSize) + { + var size = ItemSize + GutterSize; + var first = Mathf.FloorToInt(offset / (ItemSize + GutterSize)); + var count = Mathf.CeilToInt(containerSize / size) + Buffer; + var last = Math.Min(Count, first + count); + + return new Range(first, last); + } } } diff --git a/Assets/Scripts/UI/Elements/Layout/ILayout.cs b/Assets/Scripts/UI/Elements/Layout/ILayout.cs index eae001f..3023f60 100644 --- a/Assets/Scripts/UI/Elements/Layout/ILayout.cs +++ b/Assets/Scripts/UI/Elements/Layout/ILayout.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using UnityEngine.UIElements; namespace KitsuneCafe.UI @@ -6,9 +8,11 @@ namespace KitsuneCafe.UI { FlowDirection Direction { get; } float ContentSize { get; } + int Buffer { get; } float GetItemPosition(int index); float GetItemSize(int index); + Range GetVisibleRange(float offset, float size); void Update(int itemCount, VisualElement container); } } diff --git a/Assets/Scripts/UI/Elements/RecycleView/RecycleView.cs b/Assets/Scripts/UI/Elements/RecycleView/RecycleView.cs index eed8d71..801e6a0 100644 --- a/Assets/Scripts/UI/Elements/RecycleView/RecycleView.cs +++ b/Assets/Scripts/UI/Elements/RecycleView/RecycleView.cs @@ -112,6 +112,53 @@ namespace KitsuneCafe.UI } } + private int bufferCount = 8; + + [UxmlAttribute, CreateProperty, Delayed] + public int BufferCount + { + get => bufferCount; + set + { + if (bufferCount != value) + { + bufferCount = value; + if (virtualizationController.Layout is FixedLayout fixedLayout) + { + fixedLayout.Buffer = value; + } + else if (virtualizationController.Layout is DynamicLayout dynamicLayout) + { + dynamicLayout.Buffer = value; + } + } + } + } + + private float gutter = 0; + + [UxmlAttribute, CreateProperty, Delayed] + public float Gutter + { + get => gutter; + set + { + if (gutter != value) + { + gutter = value; + if (virtualizationController.Layout is FixedLayout fixedLayout) + { + fixedLayout.GutterSize = value; + } + else if (virtualizationController.Layout is DynamicLayout dynamicLayout) + { + dynamicLayout.GutterSize = value; + } + } + } + } + + [CreateProperty] public ICollectionDataSource DataSource { @@ -152,8 +199,8 @@ namespace KitsuneCafe.UI { return isDynamicSize switch { - true => new DynamicLayout(Direction, itemSize), - false => new FixedLayout(Direction, itemSize) + true => new DynamicLayout(Direction, itemSize) { Buffer = bufferCount, GutterSize = gutter }, + false => new FixedLayout(Direction, itemSize) { Buffer = bufferCount, GutterSize = gutter } }; } diff --git a/Assets/Scripts/UI/Elements/RecycleView/RecycleVirtualizationController.cs b/Assets/Scripts/UI/Elements/RecycleView/RecycleVirtualizationController.cs index e8d01db..ef5a5dc 100644 --- a/Assets/Scripts/UI/Elements/RecycleView/RecycleVirtualizationController.cs +++ b/Assets/Scripts/UI/Elements/RecycleView/RecycleVirtualizationController.cs @@ -71,9 +71,6 @@ namespace KitsuneCafe.UI private readonly BiDictionary visibleItems = new(); private float parentSize = 0; private float lastKnownScrollOffset = 0; - - private readonly int itemCountBuffer = 2; - private readonly Subject requestSubject = new(); private IDisposable subscriptions; @@ -102,16 +99,6 @@ namespace KitsuneCafe.UI }); subscriptions = Disposable.Combine(full, partial); - // subscriptions = Observable.Merge(full, partial) - // // var updateSubscription = requestSubject.ThrottleLastFrame(1) - // .Subscribe(request => - // { - // if (request is PartialUpdate req) - // { - // UpdateFromIndex(req.Index); - // } - // else { UpdateVisibleItems(); } - // }); Layout.Update(DataSource.Length, Container); ScheduleUpdate(); @@ -238,33 +225,6 @@ namespace KitsuneCafe.UI } } - private Range GetDynamicItems(DynamicLayout layout, float scrollOffset) - { - var first = layout.GetFirstVisibleIndex(scrollOffset); - var count = 1; - var position = layout.GetItemPosition(first); - var width = position + parentSize; - - foreach (var item in layout.Enumerate(first)) - { - if (item.Position > width) { break; } - count += 1; - position = item.Position; - } - - return new Range(first, Math.Min(DataSource.Length, first + count)); - } - - private Range GetFixedItems(FixedLayout layout, float scrollOffset) - { - var size = layout.ItemSize + layout.GutterSize; - var first = Mathf.FloorToInt(scrollOffset / (layout.ItemSize + layout.GutterSize)); - var count = Mathf.CeilToInt(parentSize / size) + itemCountBuffer; - var last = Math.Min(DataSource.Length, first + count); - - return new Range(first, last); - } - private void UpdateVisibleItems() { if (DataSource == null || DataSource.Length == 0 || Mathf.Approximately(parentSize, 0)) @@ -272,15 +232,9 @@ namespace KitsuneCafe.UI return; } - var scrollOffset = lastKnownScrollOffset; - var range = Layout switch - { - FixedLayout fl => GetFixedItems(fl, scrollOffset), - DynamicLayout dl => GetDynamicItems(dl, scrollOffset), - _ => throw new NotImplementedException(), - }; + var range = layout.GetVisibleRange(scrollOffset, parentSize); var firstIndex = range.Start.Value; var lastIndex = range.End.Value; diff --git a/Assets/UI/Test/Resources/UserProfileList.uxml b/Assets/UI/Test/Resources/UserProfileList.uxml index b10758c..690114e 100644 --- a/Assets/UI/Test/Resources/UserProfileList.uxml +++ b/Assets/UI/Test/Resources/UserProfileList.uxml @@ -1,3 +1,3 @@ - +