Browse Source

Initial impl. of scrolling with arrow keys.

- Currently only vertical implemented
- Doesn't handle partially visible items at end of list
pull/545/head
Steven Kirk 10 years ago
parent
commit
acdf599dec
  1. 4
      Avalonia.sln
  2. 13
      src/Avalonia.Controls/IVirtualizingPanel.cs
  3. 17
      src/Avalonia.Controls/Presenters/ItemVirtualizer.cs
  4. 46
      src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
  5. 9
      src/Avalonia.Controls/Presenters/ItemsPresenter.cs
  6. 5
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  7. 12
      src/Avalonia.Controls/Primitives/ILogicalScrollable.cs
  8. 12
      src/Avalonia.Controls/StackPanel.cs
  9. 18
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  10. 8
      tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs
  11. 20
      tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs

4
Avalonia.sln

@ -161,6 +161,8 @@ EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
src\Shared\RenderHelpers\RenderHelpers.projitems*{fb05ac90-89ba-4f2f-a924-f37875fb547c}*SharedItemsImports = 4
src\Shared\PlatformSupport\PlatformSupport.projitems*{4488ad85-1495-4809-9aa4-ddfe0a48527e}*SharedItemsImports = 4
src\Shared\PlatformSupport\PlatformSupport.projitems*{7b92af71-6287-4693-9dcb-bd5b6e927e23}*SharedItemsImports = 4
src\Shared\PlatformSupport\PlatformSupport.projitems*{e4d9629c-f168-4224-3f51-a5e482ffbc42}*SharedItemsImports = 13
src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{2f59f3d0-748d-4652-b01e-e0d954756308}*SharedItemsImports = 13
src\Shared\PlatformSupport\PlatformSupport.projitems*{db070a10-bf39-4752-8456-86e9d5928478}*SharedItemsImports = 4
@ -168,11 +170,13 @@ Global
src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{925dd807-b651-475f-9f7c-cbeb974ce43d}*SharedItemsImports = 4
samples\TestApplicationShared\TestApplicationShared.projitems*{78345174-5b52-4a14-b9fd-d5f2428137f0}*SharedItemsImports = 13
src\Shared\PlatformSupport\PlatformSupport.projitems*{54f237d5-a70a-4752-9656-0c70b1a7b047}*SharedItemsImports = 4
samples\TestApplicationShared\TestApplicationShared.projitems*{ff69b927-c545-49ae-8e16-3d14d621aa12}*SharedItemsImports = 4
src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13
src\Shared\PlatformSupport\PlatformSupport.projitems*{811a76cf-1cf6-440f-963b-bbe31bd72a82}*SharedItemsImports = 4
src\Shared\PlatformSupport\PlatformSupport.projitems*{88060192-33d5-4932-b0f9-8bd2763e857d}*SharedItemsImports = 4
src\Shared\RenderHelpers\RenderHelpers.projitems*{47be08a7-5985-410b-9ffc-2264b8ea595f}*SharedItemsImports = 4
src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{47be08a7-5985-410b-9ffc-2264b8ea595f}*SharedItemsImports = 4
samples\TestApplicationShared\TestApplicationShared.projitems*{8c923867-8a8f-4f6b-8b80-47d9e8436166}*SharedItemsImports = 4
samples\TestApplicationShared\TestApplicationShared.projitems*{e3a1060b-50d0-44e8-88b6-f44ef2e5bd72}*SharedItemsImports = 4
src\Shared\RenderHelpers\RenderHelpers.projitems*{bd43f7c0-396b-4aa1-bad9-dfde54d51298}*SharedItemsImports = 4
src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{bd43f7c0-396b-4aa1-bad9-dfde54d51298}*SharedItemsImports = 4

13
src/Avalonia.Controls/IVirtualizingPanel.cs

@ -1,9 +1,6 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Controls.Primitives;
namespace Avalonia.Controls
{
/// <summary>
@ -25,11 +22,21 @@ namespace Avalonia.Controls
/// <summary>
/// Gets a value indicating whether the panel is full.
/// </summary>
/// <remarks>
/// This property should return false until enough children are added to fill the space
/// passed into the last measure in the direction of scroll. It should be updated
/// immediately after a child is added or removed.
/// </remarks>
bool IsFull { get; }
/// <summary>
/// Gets the number of items that can be removed while keeping the panel full.
/// </summary>
/// <remarks>
/// This property should return the number of children that are completely out of the
/// panel's current bounds in the direction of scroll. It should be updated after an
/// arrange.
/// </remarks>
int OverflowCount { get; }
/// <summary>

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

@ -6,6 +6,7 @@ using System.Collections;
using System.Collections.Specialized;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Utils;
using Avalonia.Input;
using Avalonia.VisualTree;
namespace Avalonia.Controls.Presenters
@ -145,6 +146,17 @@ namespace Avalonia.Controls.Presenters
{
}
/// <summary>
/// Gets the next control in the specified direction.
/// </summary>
/// <param name="direction">The movement direction.</param>
/// <param name="from">The control from which movement begins.</param>
/// <returns>The control.</returns>
public virtual IControl GetControlInDirection(FocusNavigationDirection direction, IControl from)
{
return null;
}
/// <summary>
/// Called when the items for the presenter change, either because
/// <see cref="ItemsPresenterBase.Items"/> has been set, the items collection has been
@ -168,5 +180,10 @@ namespace Avalonia.Controls.Presenters
{
return false;
}
/// <summary>
/// Invalidates the current scroll.
/// </summary>
protected void InvalidateScroll() => ((ILogicalScrollable)Owner).InvalidateScroll();
}
}

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

@ -5,8 +5,8 @@ using System;
using System.Collections;
using System.Collections.Specialized;
using System.Linq;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Utils;
using Avalonia.Input;
namespace Avalonia.Controls.Presenters
{
@ -87,7 +87,7 @@ namespace Avalonia.Controls.Presenters
public override void UpdateControls()
{
CreateAndRemoveContainers();
((ILogicalScrollable)Owner).InvalidateScroll();
InvalidateScroll();
}
/// <inheritdoc/>
@ -134,7 +134,47 @@ namespace Avalonia.Controls.Presenters
VirtualizingPanel.Children.Clear();
}
((ILogicalScrollable)Owner).InvalidateScroll();
InvalidateScroll();
}
public override IControl GetControlInDirection(FocusNavigationDirection direction, IControl from)
{
var generator = Owner.ItemContainerGenerator;
var itemIndex = generator.IndexFromContainer(from);
if (itemIndex == -1)
{
return null;
}
var newItemIndex = -1;
if (VirtualizingPanel.ScrollDirection == Orientation.Vertical)
{
switch (direction)
{
case FocusNavigationDirection.Up:
newItemIndex = itemIndex - 1;
break;
case FocusNavigationDirection.Down:
newItemIndex = itemIndex + 1;
break;
}
}
if (newItemIndex >= 0 && newItemIndex < ItemCount)
{
if (newItemIndex < FirstIndex || newItemIndex >= NextIndex)
{
OffsetValue += newItemIndex - itemIndex;
InvalidateScroll();
}
return generator.ContainerFromIndex(newItemIndex);
}
return null;
}
/// <summary>

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

@ -5,7 +5,6 @@ using System;
using System.Collections.Specialized;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.VisualTree;
using static Avalonia.Utilities.MathUtilities;
namespace Avalonia.Controls.Presenters
@ -73,11 +72,17 @@ namespace Avalonia.Controls.Presenters
Size ILogicalScrollable.PageScrollSize => new Size(0, 1);
/// <inheritdoc/>
bool ILogicalScrollable.BringIntoView(IVisual target, Rect targetRect)
bool ILogicalScrollable.BringIntoView(IControl target, Rect targetRect)
{
return _virtualizer?.BringIntoView(target, targetRect) ?? false;
}
/// <inheritdoc/>
IControl ILogicalScrollable.GetControlInDirection(FocusNavigationDirection direction, IControl from)
{
return _virtualizer?.GetControlInDirection(direction, from);
}
/// <inheritdoc/>
protected override void PanelCreated(IPanel panel)
{

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

@ -118,10 +118,11 @@ namespace Avalonia.Controls.Presenters
}
var scrollable = Child as ILogicalScrollable;
var control = target as IControl;
if (scrollable?.IsLogicalScrollEnabled == true)
if (scrollable?.IsLogicalScrollEnabled == true && control != null)
{
return scrollable.BringIntoView(target, targetRect);
return scrollable.BringIntoView(control, targetRect);
}
var transform = target.TransformToVisual(Child);

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

@ -2,7 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.VisualTree;
using Avalonia.Input;
namespace Avalonia.Controls.Primitives
{
@ -56,6 +56,14 @@ namespace Avalonia.Controls.Primitives
/// <param name="target">The target visual.</param>
/// <param name="targetRect">The portion of the target visual to bring into view.</param>
/// <returns>True if the scroll offset was changed; otherwise false.</returns>
bool BringIntoView(IVisual target, Rect targetRect);
bool BringIntoView(IControl target, Rect targetRect);
/// <summary>
/// Gets the next control in the specified direction.
/// </summary>
/// <param name="direction">The movement direction.</param>
/// <param name="from">The control from which movement begins.</param>
/// <returns>The control.</returns>
IControl GetControlInDirection(FocusNavigationDirection direction, IControl from);
}
}

12
src/Avalonia.Controls/StackPanel.cs

@ -73,6 +73,18 @@ namespace Avalonia.Controls
/// <param name="from">The control from which movement begins.</param>
/// <returns>The control.</returns>
IInputElement INavigableContainer.GetControl(FocusNavigationDirection direction, IInputElement from)
{
var fromControl = from as IControl;
return (fromControl != null) ? GetControlInDirection(direction, fromControl) : null;
}
/// <summary>
/// Gets the next control in the specified direction.
/// </summary>
/// <param name="direction">The movement direction.</param>
/// <param name="from">The control from which movement begins.</param>
/// <returns>The control.</returns>
protected virtual IInputElement GetControlInDirection(FocusNavigationDirection direction, IControl from)
{
var horiz = Orientation == Orientation.Horizontal;
int index = Children.IndexOf((IControl)from);

18
src/Avalonia.Controls/VirtualizingStackPanel.cs

@ -3,6 +3,9 @@
using System;
using System.Collections.Specialized;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.VisualTree;
@ -107,6 +110,21 @@ namespace Avalonia.Controls
}
}
protected override IInputElement GetControlInDirection(FocusNavigationDirection direction, IControl from)
{
var logicalScrollable = Parent as ILogicalScrollable;
var fromControl = from as IControl;
if (logicalScrollable?.IsLogicalScrollEnabled == true && fromControl != null)
{
return logicalScrollable.GetControlInDirection(direction, fromControl);
}
else
{
return base.GetControlInDirection(direction, from);
}
}
internal override void ArrangeChild(
IControl child,
Rect rect,

8
tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs

@ -5,6 +5,7 @@ using System;
using System.Reactive.Linq;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.VisualTree;
using Xunit;
@ -293,7 +294,7 @@ namespace Avalonia.Controls.UnitTests
}
}
public bool BringIntoView(IVisual target, Rect targetRect)
public bool BringIntoView(IControl target, Rect targetRect)
{
throw new NotImplementedException();
}
@ -303,6 +304,11 @@ namespace Avalonia.Controls.UnitTests
AvailableSize = availableSize;
return new Size(150, 150);
}
public IControl GetControlInDirection(FocusNavigationDirection direction, IControl from)
{
throw new NotImplementedException();
}
}
}
}

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

@ -1,6 +1,9 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.LogicalTree;
using Moq;
using Xunit;
@ -42,6 +45,7 @@ namespace Avalonia.Controls.UnitTests
target.Measure(new Size(100, 100));
Assert.Equal(new Size(0, 0), target.DesiredSize);
Assert.Equal(new Size(0, 0), target.Bounds.Size);
Assert.False(target.IsFull);
@ -75,6 +79,22 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(2, target.OverflowCount);
}
[Fact]
public void Passes_Navigation_Request_To_ILogicalScrollable_Parent()
{
var presenter = new Mock<ILogical>().As<IControl>();
var scrollable = presenter.As<ILogicalScrollable>();
var target = (IVirtualizingPanel)new VirtualizingStackPanel();
var from = new Canvas();
scrollable.Setup(x => x.IsLogicalScrollEnabled).Returns(true);
((ISetLogicalParent)target).SetParent(presenter.Object);
((INavigableContainer)target).GetControl(FocusNavigationDirection.Next, from);
scrollable.Verify(x => x.GetControlInDirection(FocusNavigationDirection.Next, from));
}
}
}
}
Loading…
Cancel
Save