diff --git a/samples/VirtualizationTest/MainWindow.xaml b/samples/VirtualizationTest/MainWindow.xaml index e8baecc28c..55bd729fec 100644 --- a/samples/VirtualizationTest/MainWindow.xaml +++ b/samples/VirtualizationTest/MainWindow.xaml @@ -27,6 +27,8 @@ + + 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(); public ReactiveCommand AddItemCommand { get; private set; } - public ReactiveCommand RecreateCommand { get; private set; } - public ReactiveCommand RemoveItemCommand { get; private set; } + public ReactiveCommand SelectFirstCommand { get; private set; } + public ReactiveCommand 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(items); } + + private void SelectItem(int index) + { + SelectedItems.Clear(); + SelectedItems.Add(Items[index]); + } } } diff --git a/src/Avalonia.Controls/Mixins/SelectableMixin.cs b/src/Avalonia.Controls/Mixins/SelectableMixin.cs index 0472b604db..2369e9f530 100644 --- a/src/Avalonia.Controls/Mixins/SelectableMixin.cs +++ b/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 { diff --git a/src/Avalonia.Controls/Presenters/IItemsPresenter.cs b/src/Avalonia.Controls/Presenters/IItemsPresenter.cs index 95df903ed3..42311dc781 100644 --- a/src/Avalonia.Controls/Presenters/IItemsPresenter.cs +++ b/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); } } diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs index 4112c849d2..964ce82849 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs @@ -175,14 +175,11 @@ namespace Avalonia.Controls.Presenters } /// - /// Called when a request is made to bring an item into view. + /// Scrolls the specified item into view. /// - /// The item to bring into view. - /// The rect on the item to bring into view. - /// True if the request was handled; otherwise false. - public virtual bool BringIntoView(IVisual target, Rect targetRect) + /// The item. + public virtual void ScrollIntoView(object item) { - return false; } /// diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index e06b1749db..55e684015c 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/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; - } - } + /// + public override void ScrollIntoView(object item) + { + var index = Items.IndexOf(item); - return container; + if (index != -1) + { + ScrollIntoView(index); } - - return null; } /// @@ -434,6 +411,60 @@ namespace Avalonia.Controls.Presenters NextIndex -= count; } + /// + /// Scrolls the item with the specified index into view. + /// + /// The item index. + /// The container that was brought into view. + 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; + } + /// /// Ensures an offset value is within the value range. /// diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 151d8679cf..4547c8fd45 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -77,7 +77,7 @@ namespace Avalonia.Controls.Presenters /// bool ILogicalScrollable.BringIntoView(IControl target, Rect targetRect) { - return _virtualizer?.BringIntoView(target, targetRect) ?? false; + return false; } /// @@ -86,6 +86,11 @@ namespace Avalonia.Controls.Presenters return _virtualizer?.GetControlInDirection(direction, from); } + public override void ScrollIntoView(object item) + { + _virtualizer?.ScrollIntoView(item); + } + /// protected override void PanelCreated(IPanel panel) { diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index abebe85080..5a56e52029 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs @@ -163,6 +163,11 @@ namespace Avalonia.Controls.Presenters } } + /// + public virtual void ScrollIntoView(object item) + { + } + /// /// Creates the for the control. /// diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index a46aa8d853..27f3407fd9 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -280,6 +280,12 @@ namespace Avalonia.Controls.Primitives } } + /// + /// Scrolls the specified item into view. + /// + /// The item. + public void ScrollIntoView(object item) => Presenter?.ScrollIntoView(item); + /// /// Tries to get the container that was the source of an event. /// @@ -723,6 +729,12 @@ namespace Avalonia.Controls.Primitives { case NotifyCollectionChangedAction.Add: SelectedItemsAdded(e.NewItems.Cast().ToList()); + + if (AutoScrollToSelectedItem) + { + ScrollIntoView(e.NewItems[0]); + } + added = e.NewItems; break; diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index 2afd3f4bc3..354f93097b 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/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]