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

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

@ -227,6 +227,57 @@ namespace Avalonia.Controls.UnitTests
Assert.Null(removed.VisualParent); 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) private static IReadOnlyList<int> GetRealizedIndexes(VirtualizingStackPanel target, ItemsControl itemsControl)
{ {
return target.GetRealizedElements() return target.GetRealizedElements()
@ -326,6 +377,6 @@ namespace Avalonia.Controls.UnitTests
}.RegisterInNameScope(ns)); }.RegisterInNameScope(ns));
} }
private static IDisposable App() => UnitTestApplication.Start(); private static IDisposable App() => UnitTestApplication.Start(TestServices.RealFocus);
} }
} }

Loading…
Cancel
Save