Browse Source

Don't recycle focused element.

Fixes #9269
pull/9677/head
Steven Kirk 4 years ago
parent
commit
d0f479d80c
  1. 56
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  2. 53
      tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs

56
src/Avalonia.Controls/VirtualizingStackPanel.cs

@ -26,7 +26,7 @@ namespace Avalonia.Controls
AvaloniaProperty.RegisterAttached<VirtualizingStackPanel, Control, bool>("ItemIsOwnContainer");
private static readonly Rect s_invalidViewport = new(double.PositiveInfinity, double.PositiveInfinity, 0, 0);
private readonly Action<Control> _recycleElement;
private readonly Action<Control, int> _recycleElement;
private readonly Action<Control> _recycleElementOnItemRemoved;
private readonly Action<Control, int, int> _updateElementIndex;
private int _anchorIndex = -1;
@ -38,6 +38,8 @@ namespace Avalonia.Controls
private RealizedElementList? _realizedElements;
private Rect _viewport = s_invalidViewport;
private Stack<Control>? _recyclePool;
private Control? _unrealizedFocusedElement;
private int _unrealizedFocusedIndex = -1;
public VirtualizingStackPanel()
{
@ -167,7 +169,7 @@ namespace Avalonia.Controls
_realizedElements.ItemsInserted(e.NewStartingIndex, e.NewItems!.Count, _updateElementIndex);
break;
case NotifyCollectionChangedAction.Reset:
_realizedElements.RecycleAllElements(_recycleElementOnItemRemoved);
_realizedElements.ItemsReset(_recycleElementOnItemRemoved);
break;
}
}
@ -482,6 +484,13 @@ namespace Avalonia.Controls
var generator = ItemContainerGenerator!;
var item = items[index];
if (_unrealizedFocusedIndex == index)
{
var element = _unrealizedFocusedElement;
_unrealizedFocusedElement = null;
_unrealizedFocusedIndex = -1;
return element;
}
if (_recyclePool?.Count > 0)
{
var recycled = _recyclePool.Pop();
@ -515,7 +524,7 @@ namespace Avalonia.Controls
return index * estimatedElementSize;
}
private void RecycleElement(Control element)
private void RecycleElement(Control element, int index)
{
Debug.Assert(ItemContainerGenerator is not null);
@ -523,6 +532,11 @@ namespace Avalonia.Controls
{
element.IsVisible = false;
}
else if (element.IsKeyboardFocusWithin)
{
_unrealizedFocusedElement = element;
_unrealizedFocusedIndex = index;
}
else
{
ItemContainerGenerator!.ClearItemContainer(element);
@ -932,12 +946,33 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Recycles all elements in response to the source collection being reset.
/// </summary>
/// <param name="recycleElement">A method used to recycle elements.</param>
public void ItemsReset(Action<Control> recycleElement)
{
if (_elements is null || _elements.Count == 0)
return;
foreach (var e in _elements)
{
if (e is not null)
recycleElement(e);
}
_startU = _firstIndex = 0;
_elements?.Clear();
_sizes?.Clear();
}
/// <summary>
/// Recycles elements before a specific index.
/// </summary>
/// <param name="index">The index in the source collection of new first element.</param>
/// <param name="recycleElement">A method used to recycle elements.</param>
public void RecycleElementsBefore(int index, Action<Control> recycleElement)
public void RecycleElementsBefore(int index, Action<Control, int> recycleElement)
{
if (index <= FirstIndex || _elements is null || _elements.Count == 0)
return;
@ -953,7 +988,7 @@ namespace Avalonia.Controls
for (var i = 0; i < endIndex; ++i)
{
if (_elements[i] is Control e)
recycleElement(e);
recycleElement(e, i + FirstIndex);
}
_elements.RemoveRange(0, endIndex);
@ -967,7 +1002,7 @@ namespace Avalonia.Controls
/// </summary>
/// <param name="index">The index in the source collection of new last element.</param>
/// <param name="recycleElement">A method used to recycle elements.</param>
public void RecycleElementsAfter(int index, Action<Control> recycleElement)
public void RecycleElementsAfter(int index, Action<Control, int> recycleElement)
{
if (index >= LastIndex || _elements is null || _elements.Count == 0)
return;
@ -984,7 +1019,7 @@ namespace Avalonia.Controls
for (var i = startIndex; i < count; ++i)
{
if (_elements[i] is Control e)
recycleElement(e);
recycleElement(e, i + FirstIndex);
}
_elements.RemoveRange(startIndex, _elements.Count - startIndex);
@ -996,15 +1031,18 @@ namespace Avalonia.Controls
/// Recycles all realized elements.
/// </summary>
/// <param name="recycleElement">A method used to recycle elements.</param>
public void RecycleAllElements(Action<Control> recycleElement)
public void RecycleAllElements(Action<Control, int> recycleElement)
{
if (_elements is null || _elements.Count == 0)
return;
var i = FirstIndex;
foreach (var e in _elements)
{
if (e is not null)
recycleElement(e);
recycleElement(e, i);
++i;
}
_startU = _firstIndex = 0;

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

@ -227,6 +227,57 @@ namespace Avalonia.Controls.UnitTests
Assert.Null(removed.VisualParent);
}
[Fact]
public void Does_Not_Recycle_Focused_Element()
{
using var app = App();
var (target, scroll, itemsControl) = CreateTarget();
target.GetRealizedElements().First()!.Focus();
Assert.True(target.GetRealizedElements().First()!.IsKeyboardFocusWithin);
scroll.Offset = new Vector(0, 200);
Layout(target);
Assert.All(target.GetRealizedElements(), x => Assert.False(x!.IsKeyboardFocusWithin));
}
[Fact]
public void Removing_Item_Of_Focused_Element_Clears_Focus()
{
using var app = App();
var (target, scroll, itemsControl) = CreateTarget();
var focused = target.GetRealizedElements().First()!;
focused.Focus();
Assert.True(focused.IsKeyboardFocusWithin);
scroll.Offset = new Vector(0, 200);
Layout(target);
Assert.All(target.GetRealizedElements(), x => Assert.False(x!.IsKeyboardFocusWithin));
Assert.All(target.GetRealizedElements(), x => Assert.NotSame(focused, x));
}
[Fact]
public void Scrolling_Back_To_Focused_Element_Uses_Correct_Element()
{
using var app = App();
var (target, scroll, itemsControl) = CreateTarget();
var focused = target.GetRealizedElements().First()!;
focused.Focus();
Assert.True(focused.IsKeyboardFocusWithin);
scroll.Offset = new Vector(0, 200);
Layout(target);
scroll.Offset = new Vector(0, 0);
Layout(target);
Assert.Same(focused, target.GetRealizedElements().First());
}
private static IReadOnlyList<int> GetRealizedIndexes(VirtualizingStackPanel target, ItemsControl itemsControl)
{
return target.GetRealizedElements()
@ -326,6 +377,6 @@ namespace Avalonia.Controls.UnitTests
}.RegisterInNameScope(ns));
}
private static IDisposable App() => UnitTestApplication.Start();
private static IDisposable App() => UnitTestApplication.Start(TestServices.RealFocus);
}
}

Loading…
Cancel
Save