Browse Source

Implement AutoScrollToSelectedItem

For virtualizing lists.
pull/545/head
Steven Kirk 10 years ago
parent
commit
95eefa3cf1
  1. 2
      samples/VirtualizationTest/MainWindow.xaml
  2. 16
      samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs
  3. 17
      src/Avalonia.Controls/Mixins/SelectableMixin.cs
  4. 2
      src/Avalonia.Controls/Presenters/IItemsPresenter.cs
  5. 9
      src/Avalonia.Controls/Presenters/ItemVirtualizer.cs
  6. 95
      src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
  7. 7
      src/Avalonia.Controls/Presenters/ItemsPresenter.cs
  8. 5
      src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs
  9. 12
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  10. 71
      tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs

2
samples/VirtualizationTest/MainWindow.xaml

@ -27,6 +27,8 @@
<Button Command="{Binding AddItemCommand}">Add Item</Button>
<Button Command="{Binding RemoveItemCommand}">Remove Item</Button>
<Button Command="{Binding RecreateCommand}">Recreate</Button>
<Button Command="{Binding SelectFirstCommand}">Select First</Button>
<Button Command="{Binding SelectLastCommand}">Select Last</Button>
</StackPanel>
<ListBox Name="listBox"

16
samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs

@ -31,6 +31,12 @@ namespace VirtualizationTest.ViewModels
RemoveItemCommand = ReactiveCommand.Create();
RemoveItemCommand.Subscribe(_ => Remove());
SelectFirstCommand = ReactiveCommand.Create();
SelectFirstCommand.Subscribe(_ => SelectItem(0));
SelectLastCommand = ReactiveCommand.Create();
SelectLastCommand.Subscribe(_ => SelectItem(Items.Count - 1));
}
public string NewItemString
@ -73,10 +79,10 @@ namespace VirtualizationTest.ViewModels
Enum.GetValues(typeof(ItemVirtualizationMode)).Cast<ItemVirtualizationMode>();
public ReactiveCommand<object> AddItemCommand { get; private set; }
public ReactiveCommand<object> RecreateCommand { get; private set; }
public ReactiveCommand<object> RemoveItemCommand { get; private set; }
public ReactiveCommand<object> SelectFirstCommand { get; private set; }
public ReactiveCommand<object> SelectLastCommand { get; private set; }
private void ResizeItems(int count)
{
@ -125,5 +131,11 @@ namespace VirtualizationTest.ViewModels
.Select(x => new ItemViewModel(x, _prefix));
Items = new ReactiveList<ItemViewModel>(items);
}
private void SelectItem(int index)
{
SelectedItems.Clear();
SelectedItems.Add(Items[index]);
}
}
}

17
src/Avalonia.Controls/Mixins/SelectableMixin.cs

@ -51,22 +51,7 @@ namespace Avalonia.Controls.Mixins
if (sender != null)
{
var itemsControl = sender.Parent as SelectingItemsControl;
if ((bool)x.NewValue)
{
((IPseudoClasses)sender.Classes).Add(":selected");
if (((IVisual)sender).IsAttachedToVisualTree &&
itemsControl?.AutoScrollToSelectedItem == true)
{
sender.BringIntoView();
}
}
else
{
((IPseudoClasses)sender.Classes).Remove(":selected");
}
((IPseudoClasses)sender.Classes).Set(":selected", (bool)x.NewValue);
sender.RaiseEvent(new RoutedEventArgs
{

2
src/Avalonia.Controls/Presenters/IItemsPresenter.cs

@ -6,5 +6,7 @@ namespace Avalonia.Controls.Presenters
public interface IItemsPresenter : IPresenter
{
IPanel Panel { get; }
void ScrollIntoView(object item);
}
}

9
src/Avalonia.Controls/Presenters/ItemVirtualizer.cs

@ -175,14 +175,11 @@ namespace Avalonia.Controls.Presenters
}
/// <summary>
/// Called when a request is made to bring an item into view.
/// Scrolls the specified item into view.
/// </summary>
/// <param name="target">The item to bring into view.</param>
/// <param name="targetRect">The rect on the item to bring into view.</param>
/// <returns>True if the request was handled; otherwise false.</returns>
public virtual bool BringIntoView(IVisual target, Rect targetRect)
/// <param name="item">The item.</param>
public virtual void ScrollIntoView(object item)
{
return false;
}
/// <inheritdoc/>

95
src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs

@ -197,41 +197,18 @@ namespace Avalonia.Controls.Presenters
}
}
if (newItemIndex >= 0 && newItemIndex < ItemCount)
{
// Get the index of the first and last fully visible items (i.e. excluding any
// partially visible item at the beginning or end).
var firstIndex = panel.PixelOffset == 0 ? FirstIndex : FirstIndex + 1;
var lastIndex = (FirstIndex + ViewportValue) - 1;
if (newItemIndex < firstIndex || newItemIndex > lastIndex)
{
var newOffset = OffsetValue + (newItemIndex - itemIndex);
OffsetValue = CoerceOffset(newOffset);
InvalidateScroll();
}
var container = generator.ContainerFromIndex(newItemIndex);
var layoutManager = LayoutManager.Instance;
// We need to do a layout here because it's possible that the container we moved to
// is only partially visible due to differing item sizes. If the container is only
// partially visible, scroll again. Don't do this if there's no layout manager:
// it means we're running a unit test.
if (layoutManager != null)
{
layoutManager.ExecuteLayoutPass();
return ScrollIntoView(newItemIndex);
}
if (!new Rect(panel.Bounds.Size).Contains(container.Bounds))
{
OffsetValue += newItemIndex > itemIndex ? 1 : -1;
}
}
/// <inheritdoc/>
public override void ScrollIntoView(object item)
{
var index = Items.IndexOf(item);
return container;
if (index != -1)
{
ScrollIntoView(index);
}
return null;
}
/// <summary>
@ -434,6 +411,60 @@ namespace Avalonia.Controls.Presenters
NextIndex -= count;
}
/// <summary>
/// Scrolls the item with the specified index into view.
/// </summary>
/// <param name="index">The item index.</param>
/// <returns>The container that was brought into view.</returns>
private IControl ScrollIntoView(int index)
{
var panel = VirtualizingPanel;
var generator = Owner.ItemContainerGenerator;
var newOffset = -1.0;
if (index >= 0 && index < ItemCount)
{
if (index < FirstIndex)
{
newOffset = index;
}
else if (index >= NextIndex)
{
newOffset = index - Math.Ceiling(ViewportValue - 1);
}
else if (OffsetValue + ViewportValue >= ItemCount)
{
newOffset = OffsetValue - 1;
}
if (newOffset != -1)
{
OffsetValue = newOffset;
}
var container = generator.ContainerFromIndex(index);
var layoutManager = LayoutManager.Instance;
// We need to do a layout here because it's possible that the container we moved to
// is only partially visible due to differing item sizes. If the container is only
// partially visible, scroll again. Don't do this if there's no layout manager:
// it means we're running a unit test.
if (layoutManager != null)
{
layoutManager.ExecuteLayoutPass();
if (!new Rect(panel.Bounds.Size).Contains(container.Bounds))
{
OffsetValue += 1;
}
}
return container;
}
return null;
}
/// <summary>
/// Ensures an offset value is within the value range.
/// </summary>

7
src/Avalonia.Controls/Presenters/ItemsPresenter.cs

@ -77,7 +77,7 @@ namespace Avalonia.Controls.Presenters
/// <inheritdoc/>
bool ILogicalScrollable.BringIntoView(IControl target, Rect targetRect)
{
return _virtualizer?.BringIntoView(target, targetRect) ?? false;
return false;
}
/// <inheritdoc/>
@ -86,6 +86,11 @@ namespace Avalonia.Controls.Presenters
return _virtualizer?.GetControlInDirection(direction, from);
}
public override void ScrollIntoView(object item)
{
_virtualizer?.ScrollIntoView(item);
}
/// <inheritdoc/>
protected override void PanelCreated(IPanel panel)
{

5
src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs

@ -163,6 +163,11 @@ namespace Avalonia.Controls.Presenters
}
}
/// <inheritdoc/>
public virtual void ScrollIntoView(object item)
{
}
/// <summary>
/// Creates the <see cref="ItemContainerGenerator"/> for the control.
/// </summary>

12
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@ -280,6 +280,12 @@ namespace Avalonia.Controls.Primitives
}
}
/// <summary>
/// Scrolls the specified item into view.
/// </summary>
/// <param name="item">The item.</param>
public void ScrollIntoView(object item) => Presenter?.ScrollIntoView(item);
/// <summary>
/// Tries to get the container that was the source of an event.
/// </summary>
@ -723,6 +729,12 @@ namespace Avalonia.Controls.Primitives
{
case NotifyCollectionChangedAction.Add:
SelectedItemsAdded(e.NewItems.Cast<object>().ToList());
if (AutoScrollToSelectedItem)
{
ScrollIntoView(e.NewItems[0]);
}
added = e.NewItems;
break;

71
tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs

@ -412,38 +412,44 @@ namespace Avalonia.Controls.UnitTests.Presenters
[Fact]
public void GetControlInDirection_Down_Should_Scroll_If_Partially_Visible()
{
var target = CreateTarget();
using (UnitTestApplication.Start(TestServices.RealLayoutManager))
{
var target = CreateTarget();
var scroller = (ScrollContentPresenter)target.Parent;
target.ApplyTemplate();
target.Measure(new Size(100, 95));
target.Arrange(new Rect(0, 0, 100, 95));
scroller.Measure(new Size(100, 95));
scroller.Arrange(new Rect(0, 0, 100, 95));
var from = target.Panel.Children[8];
var result = ((ILogicalScrollable)target).GetControlInDirection(
NavigationDirection.Down,
from);
var from = target.Panel.Children[8];
var result = ((ILogicalScrollable)target).GetControlInDirection(
NavigationDirection.Down,
from);
Assert.Equal(new Vector(0, 1), ((ILogicalScrollable)target).Offset);
Assert.Same(target.Panel.Children[8], result);
Assert.Equal(new Vector(0, 1), ((ILogicalScrollable)target).Offset);
Assert.Same(target.Panel.Children[8], result);
}
}
[Fact]
public void GetControlInDirection_Up_Should_Scroll_If_Partially_Visible_Item_Is_Currently_Shown()
{
var target = CreateTarget();
using (UnitTestApplication.Start(TestServices.RealLayoutManager))
{
var target = CreateTarget();
var scroller = (ScrollContentPresenter)target.Parent;
target.ApplyTemplate();
target.Measure(new Size(100, 95));
target.Arrange(new Rect(0, 0, 100, 95));
((ILogicalScrollable)target).Offset = new Vector(0, 11);
scroller.Measure(new Size(100, 95));
scroller.Arrange(new Rect(0, 0, 100, 95));
((ILogicalScrollable)target).Offset = new Vector(0, 11);
var from = target.Panel.Children[1];
var result = ((ILogicalScrollable)target).GetControlInDirection(
NavigationDirection.Up,
from);
var from = target.Panel.Children[1];
var result = ((ILogicalScrollable)target).GetControlInDirection(
NavigationDirection.Up,
from);
Assert.Equal(new Vector(0, 10), ((ILogicalScrollable)target).Offset);
Assert.Same(target.Panel.Children[0], result);
Assert.Equal(new Vector(0, 10), ((ILogicalScrollable)target).Offset);
Assert.Same(target.Panel.Children[0], result);
}
}
}
@ -487,19 +493,22 @@ namespace Avalonia.Controls.UnitTests.Presenters
[Fact]
public void GetControlInDirection_Right_Should_Scroll_If_Partially_Visible()
{
var target = CreateTarget(orientation: Orientation.Horizontal);
using (UnitTestApplication.Start(TestServices.RealLayoutManager))
{
var target = CreateTarget(orientation: Orientation.Horizontal);
var scroller = (ScrollContentPresenter)target.Parent;
target.ApplyTemplate();
target.Measure(new Size(95, 100));
target.Arrange(new Rect(0, 0, 95, 100));
scroller.Measure(new Size(95, 100));
scroller.Arrange(new Rect(0, 0, 95, 100));
var from = target.Panel.Children[8];
var result = ((ILogicalScrollable)target).GetControlInDirection(
NavigationDirection.Right,
from);
var from = target.Panel.Children[8];
var result = ((ILogicalScrollable)target).GetControlInDirection(
NavigationDirection.Right,
from);
Assert.Equal(new Vector(1, 0), ((ILogicalScrollable)target).Offset);
Assert.Same(target.Panel.Children[8], result);
Assert.Equal(new Vector(1, 0), ((ILogicalScrollable)target).Offset);
Assert.Same(target.Panel.Children[8], result);
}
}
[Fact]

Loading…
Cancel
Save