Browse Source

Merge pull request #10285 from AvaloniaUI/fixes/virtualization-fixes

List virtualization fixes
fixes/6684-two-way-style-bindings-with-localvalue
Max Katz 3 years ago
committed by GitHub
parent
commit
3e3dbb1d3d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  2. 9
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  3. 34
      tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs
  4. 96
      tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs

1
src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs

@ -427,6 +427,7 @@ namespace Avalonia.Controls.Presenters
Viewport = finalSize; Viewport = finalSize;
Extent = Child!.Bounds.Size.Inflate(Child.Margin); Extent = Child!.Bounds.Size.Inflate(Child.Margin);
Offset = ScrollViewer.CoerceOffset(Extent, finalSize, Offset);
_isAnchorElementDirty = true; _isAnchorElementDirty = true;
return finalSize; return finalSize;

9
src/Avalonia.Controls/VirtualizingStackPanel.cs

@ -403,7 +403,7 @@ namespace Avalonia.Controls
if (firstIndex == -1) if (firstIndex == -1)
{ {
estimatedElementSize = EstimateElementSizeU(); estimatedElementSize = EstimateElementSizeU();
firstIndex = (int)(viewportStart / estimatedElementSize); firstIndex = Math.Min((int)(viewportStart / estimatedElementSize), maxIndex);
firstIndexU = firstIndex * estimatedElementSize; firstIndexU = firstIndex * estimatedElementSize;
} }
@ -411,13 +411,13 @@ namespace Avalonia.Controls
{ {
if (estimatedElementSize == -1) if (estimatedElementSize == -1)
estimatedElementSize = EstimateElementSizeU(); estimatedElementSize = EstimateElementSizeU();
lastIndex = (int)(viewportEnd / estimatedElementSize); lastIndex = Math.Min((int)(viewportEnd / estimatedElementSize), maxIndex);
} }
return new MeasureViewport return new MeasureViewport
{ {
firstIndex = MathUtilities.Clamp(firstIndex, 0, maxIndex), firstIndex = firstIndex,
lastIndex = MathUtilities.Clamp(lastIndex, 0, maxIndex), lastIndex = lastIndex,
viewportUStart = viewportStart, viewportUStart = viewportStart,
viewportUEnd = viewportEnd, viewportUEnd = viewportEnd,
startU = firstIndexU, startU = firstIndexU,
@ -1131,6 +1131,7 @@ namespace Avalonia.Controls
// The removed range was before the realized elements. Update the first index and // The removed range was before the realized elements. Update the first index and
// the indexes of the realized elements. // the indexes of the realized elements.
_firstIndex -= count; _firstIndex -= count;
_startUUnstable = true;
var newIndex = _firstIndex; var newIndex = _firstIndex;
for (var i = 0; i < _elements.Count; ++i) for (var i = 0; i < _elements.Count; ++i)

34
tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs

@ -237,6 +237,40 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(1, raised); Assert.Equal(1, raised);
} }
[Fact]
public void Reducing_Extent_Should_Constrain_Offset()
{
var target = new ScrollViewer
{
Template = new FuncControlTemplate<ScrollViewer>(CreateTemplate),
};
var root = new TestRoot(target);
var raised = 0;
target.SetValue(ScrollViewer.ExtentProperty, new Size(100, 100));
target.SetValue(ScrollViewer.ViewportProperty, new Size(50, 50));
target.Offset = new Vector(50, 50);
root.LayoutManager.ExecuteInitialLayoutPass();
target.ScrollChanged += (s, e) =>
{
Assert.Equal(new Vector(-30, -30), e.ExtentDelta);
Assert.Equal(new Vector(-30, -30), e.OffsetDelta);
Assert.Equal(default, e.ViewportDelta);
++raised;
};
target.SetValue(ScrollViewer.ExtentProperty, new Size(70, 70));
Assert.Equal(0, raised);
root.LayoutManager.ExecuteLayoutPass();
Assert.Equal(1, raised);
Assert.Equal(new Vector(20, 20), target.Offset);
}
private Control CreateTemplate(ScrollViewer control, INameScope scope) private Control CreateTemplate(ScrollViewer control, INameScope scope)
{ {
return new Grid return new Grid

96
tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs

@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Linq; using System.Linq;
using Avalonia.Collections;
using Avalonia.Controls.Presenters; using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Data; using Avalonia.Data;
@ -278,6 +279,82 @@ namespace Avalonia.Controls.UnitTests
Assert.Same(focused, target.GetRealizedElements().First()); Assert.Same(focused, target.GetRealizedElements().First());
} }
[Fact]
public void Removing_Range_When_Scrolled_To_End_Updates_Viewport()
{
using var app = App();
var items = new AvaloniaList<string>(Enumerable.Range(0, 100).Select(x => $"Item {x}"));
var (target, scroll, itemsControl) = CreateTarget(items: items);
scroll.Offset = new Vector(0, 900);
Layout(target);
AssertRealizedItems(target, itemsControl, 90, 10);
items.RemoveRange(0, 80);
Layout(target);
AssertRealizedItems(target, itemsControl, 10, 10);
Assert.Equal(new Vector(0, 100), scroll.Offset);
}
[Fact]
public void Removing_Range_To_Have_Less_Than_A_Page_Of_Items_When_Scrolled_To_End_Updates_Viewport()
{
using var app = App();
var items = new AvaloniaList<string>(Enumerable.Range(0, 100).Select(x => $"Item {x}"));
var (target, scroll, itemsControl) = CreateTarget(items: items);
scroll.Offset = new Vector(0, 900);
Layout(target);
AssertRealizedItems(target, itemsControl, 90, 10);
items.RemoveRange(0, 95);
Layout(target);
AssertRealizedItems(target, itemsControl, 0, 5);
Assert.Equal(new Vector(0, 0), scroll.Offset);
}
[Fact]
public void Resetting_Collection_To_Have_Less_Items_When_Scrolled_To_End_Updates_Viewport()
{
using var app = App();
var items = new ResettingCollection(Enumerable.Range(0, 100).Select(x => $"Item {x}"));
var (target, scroll, itemsControl) = CreateTarget(items: items);
scroll.Offset = new Vector(0, 900);
Layout(target);
AssertRealizedItems(target, itemsControl, 90, 10);
items.Reset(Enumerable.Range(0, 20).Select(x => $"Item {x}"));
Layout(target);
AssertRealizedItems(target, itemsControl, 10, 10);
Assert.Equal(new Vector(0, 100), scroll.Offset);
}
[Fact]
public void Resetting_Collection_To_Have_Less_Than_A_Page_Of_Items_When_Scrolled_To_End_Updates_Viewport()
{
using var app = App();
var items = new ResettingCollection(Enumerable.Range(0, 100).Select(x => $"Item {x}"));
var (target, scroll, itemsControl) = CreateTarget(items: items);
scroll.Offset = new Vector(0, 900);
Layout(target);
AssertRealizedItems(target, itemsControl, 90, 10);
items.Reset(Enumerable.Range(0, 5).Select(x => $"Item {x}"));
Layout(target);
AssertRealizedItems(target, itemsControl, 0, 5);
Assert.Equal(new Vector(0, 0), scroll.Offset);
}
private static IReadOnlyList<int> GetRealizedIndexes(VirtualizingStackPanel target, ItemsControl itemsControl) private static IReadOnlyList<int> GetRealizedIndexes(VirtualizingStackPanel target, ItemsControl itemsControl)
{ {
return target.GetRealizedElements() return target.GetRealizedElements()
@ -378,5 +455,24 @@ namespace Avalonia.Controls.UnitTests
} }
private static IDisposable App() => UnitTestApplication.Start(TestServices.RealFocus); private static IDisposable App() => UnitTestApplication.Start(TestServices.RealFocus);
private class ResettingCollection : List<string>, INotifyCollectionChanged
{
public ResettingCollection(IEnumerable<string> items)
{
AddRange(items);
}
public void Reset(IEnumerable<string> items)
{
Clear();
AddRange(items);
CollectionChanged?.Invoke(
this,
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
public event NotifyCollectionChangedEventHandler? CollectionChanged;
}
} }
} }

Loading…
Cancel
Save