Browse Source

Merge pull request #758 from AvaloniaUI/fixes/633-virt-horizontal-scroll

WIP: Cross-axis scrolling for virtualized lists.
pull/765/head
Steven Kirk 10 years ago
committed by GitHub
parent
commit
d35c483b64
  1. 67
      src/Avalonia.Controls/Presenters/ItemVirtualizer.cs
  2. 52
      src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
  3. 21
      src/Avalonia.Controls/Presenters/ItemsPresenter.cs
  4. 2
      src/Avalonia.Layout/LayoutManager.cs
  5. 8
      tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs
  6. 19
      tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs

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

@ -16,6 +16,8 @@ namespace Avalonia.Controls.Presenters
/// </summary> /// </summary>
internal abstract class ItemVirtualizer : IVirtualizingController, IDisposable internal abstract class ItemVirtualizer : IVirtualizingController, IDisposable
{ {
private double _crossAxisOffset;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ItemVirtualizer"/> class. /// Initializes a new instance of the <see cref="ItemVirtualizer"/> class.
/// </summary> /// </summary>
@ -60,7 +62,7 @@ namespace Avalonia.Controls.Presenters
/// <summary> /// <summary>
/// Gets a value indicating whether the items should be scroll horizontally or vertically. /// Gets a value indicating whether the items should be scroll horizontally or vertically.
/// </summary> /// </summary>
public bool Vertical => VirtualizingPanel.ScrollDirection == Orientation.Vertical; public bool Vertical => VirtualizingPanel?.ScrollDirection == Orientation.Vertical;
/// <summary> /// <summary>
/// Gets a value indicating whether logical scrolling is enabled. /// Gets a value indicating whether logical scrolling is enabled.
@ -85,12 +87,28 @@ namespace Avalonia.Controls.Presenters
/// <summary> /// <summary>
/// Gets the <see cref="ExtentValue"/> as a <see cref="Size"/>. /// Gets the <see cref="ExtentValue"/> as a <see cref="Size"/>.
/// </summary> /// </summary>
public Size Extent => Vertical ? new Size(0, ExtentValue) : new Size(ExtentValue, 0); public Size Extent
{
get
{
return Vertical ?
new Size(Owner.Panel.DesiredSize.Width, ExtentValue) :
new Size(ExtentValue, Owner.Panel.DesiredSize.Height);
}
}
/// <summary> /// <summary>
/// Gets the <see cref="ViewportValue"/> as a <see cref="Size"/>. /// Gets the <see cref="ViewportValue"/> as a <see cref="Size"/>.
/// </summary> /// </summary>
public Size Viewport => Vertical ? new Size(0, ViewportValue) : new Size(ViewportValue, 0); public Size Viewport
{
get
{
return Vertical ?
new Size(Owner.Panel.Bounds.Width, ViewportValue) :
new Size(ViewportValue, Owner.Panel.Bounds.Height);
}
}
/// <summary> /// <summary>
/// Gets or sets the <see cref="OffsetValue"/> as a <see cref="Vector"/>. /// Gets or sets the <see cref="OffsetValue"/> as a <see cref="Vector"/>.
@ -99,12 +117,28 @@ namespace Avalonia.Controls.Presenters
{ {
get get
{ {
return Vertical ? new Vector(0, OffsetValue) : new Vector(OffsetValue, 0); return Vertical ? new Vector(_crossAxisOffset, OffsetValue) : new Vector(OffsetValue, _crossAxisOffset);
} }
set set
{ {
OffsetValue = Vertical ? value.Y : value.X; var oldCrossAxisOffset = _crossAxisOffset;
if (Vertical)
{
OffsetValue = value.Y;
_crossAxisOffset = value.X;
}
else
{
OffsetValue = value.X;
_crossAxisOffset = value.Y;
}
if (_crossAxisOffset != oldCrossAxisOffset)
{
Owner.InvalidateArrange();
}
} }
} }
@ -143,6 +177,29 @@ namespace Avalonia.Controls.Presenters
return result; return result;
} }
/// <summary>
/// Carries out a measure for the related <see cref="ItemsPresenter"/>.
/// </summary>
/// <param name="availableSize">The size available to the control.</param>
/// <returns>The desired size for the control.</returns>
public virtual Size MeasureOverride(Size availableSize)
{
Owner.Panel.Measure(availableSize);
return Owner.Panel.DesiredSize;
}
/// <summary>
/// Carries out an arrange for the related <see cref="ItemsPresenter"/>.
/// </summary>
/// <param name="finalSize">The size available to the control.</param>
/// <returns>The actual size used.</returns>
public virtual Size ArrangeOverride(Size finalSize)
{
var origin = Vertical ? new Point(-_crossAxisOffset, 0) : new Point(0, _crossAxisOffset);
Owner.Panel.Arrange(new Rect(origin, finalSize));
return finalSize;
}
/// <inheritdoc/> /// <inheritdoc/>
public virtual void UpdateControls() public virtual void UpdateControls()
{ {

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

@ -94,6 +94,44 @@ namespace Avalonia.Controls.Presenters
} }
} }
/// <inheritdoc/>
public override Size MeasureOverride(Size availableSize)
{
var window = Owner.GetVisualRoot() as TopLevel;
// If infinity is passed as the available size and we're virtualized then we need to
// fill the available space, but to do that we *don't* want to materialize all our
// items! Take a look at the root of the tree for a MaxClientSize and use that as
// the available size.
if (VirtualizingPanel.ScrollDirection == Orientation.Vertical)
{
if (availableSize.Height == double.PositiveInfinity)
{
if (window != null)
{
availableSize = availableSize.WithHeight(window.PlatformImpl.MaxClientSize.Height);
}
}
availableSize = availableSize.WithWidth(double.PositiveInfinity);
}
else
{
if (availableSize.Width == double.PositiveInfinity)
{
if (window != null)
{
availableSize = availableSize.WithWidth(window.PlatformImpl.MaxClientSize.Width);
}
}
availableSize = availableSize.WithHeight(double.PositiveInfinity);
}
Owner.Panel.Measure(availableSize);
return Owner.Panel.DesiredSize;
}
/// <inheritdoc/> /// <inheritdoc/>
public override void UpdateControls() public override void UpdateControls()
{ {
@ -481,9 +519,19 @@ namespace Avalonia.Controls.Presenters
{ {
layoutManager.ExecuteLayoutPass(); layoutManager.ExecuteLayoutPass();
if (!new Rect(panel.Bounds.Size).Contains(container.Bounds)) if (panel.ScrollDirection == Orientation.Vertical)
{ {
OffsetValue += 1; if (container.Bounds.Y < panel.Bounds.Y || container.Bounds.Bottom > panel.Bounds.Bottom)
{
OffsetValue += 1;
}
}
else
{
if (container.Bounds.X < panel.Bounds.X || container.Bounds.Right > panel.Bounds.Right)
{
OffsetValue += 1;
}
} }
} }

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

@ -91,24 +91,15 @@ namespace Avalonia.Controls.Presenters
_virtualizer?.ScrollIntoView(item); _virtualizer?.ScrollIntoView(item);
} }
/// <inheritdoc/>
protected override Size MeasureOverride(Size availableSize) protected override Size MeasureOverride(Size availableSize)
{ {
// If infinity is passed as the available size and we're virtualized then we need to return _virtualizer?.MeasureOverride(availableSize) ?? Size.Empty;
// fill the available space, but to do that we *don't* want to materialize all our }
// items! Take a look at the root of the tree for a MaxClientSize and use that as
// the available size.
if (availableSize == Size.Infinity && VirtualizationMode != ItemVirtualizationMode.None)
{
var window = VisualRoot as TopLevel;
if (window != null)
{
availableSize = window.PlatformImpl.MaxClientSize;
}
}
Panel.Measure(availableSize); protected override Size ArrangeOverride(Size finalSize)
return Panel.DesiredSize; {
return _virtualizer?.ArrangeOverride(finalSize) ?? Size.Empty;
} }
/// <inheritdoc/> /// <inheritdoc/>

2
src/Avalonia.Layout/LayoutManager.cs

@ -168,7 +168,7 @@ namespace Avalonia.Layout
private void QueueLayoutPass() private void QueueLayoutPass()
{ {
if (!_queued) if (!_queued && !_running)
{ {
Dispatcher.UIThread.InvokeAsync(ExecuteLayoutPass, DispatcherPriority.Render); Dispatcher.UIThread.InvokeAsync(ExecuteLayoutPass, DispatcherPriority.Render);
_queued = true; _queued = true;

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

@ -111,7 +111,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
target.Arrange(new Rect(0, 0, 100, 100)); target.Arrange(new Rect(0, 0, 100, 100));
var scroll = (ScrollContentPresenter)target.Parent; var scroll = (ScrollContentPresenter)target.Parent;
Assert.Equal(new Size(0, 20), scroll.Extent); Assert.Equal(new Size(10, 20), scroll.Extent);
Assert.Equal(new Size(0, 10), scroll.Viewport); Assert.Equal(new Size(0, 10), scroll.Viewport);
} }
@ -212,8 +212,8 @@ namespace Avalonia.Controls.UnitTests.Presenters
target.Arrange(new Rect(0, 0, 100, 100)); target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(10, target.Panel.Children.Count); Assert.Equal(10, target.Panel.Children.Count);
Assert.Equal(new Size(0, 20), scroll.Extent); Assert.Equal(new Size(10, 20), scroll.Extent);
Assert.Equal(new Size(0, 10), scroll.Viewport); Assert.Equal(new Size(100, 10), scroll.Viewport);
} }
[Fact] [Fact]
@ -253,7 +253,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
scroll.Arrange(new Rect(0, 0, 100, 100)); scroll.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(10, target.Panel.Children.Count); Assert.Equal(10, target.Panel.Children.Count);
Assert.Equal(new Size(0, 20), scroll.Extent); Assert.Equal(new Size(10, 20), scroll.Extent);
Assert.Equal(new Size(0, 10), scroll.Viewport); Assert.Equal(new Size(0, 10), scroll.Viewport);
target.VirtualizationMode = ItemVirtualizationMode.None; target.VirtualizationMode = ItemVirtualizationMode.None;

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

@ -49,7 +49,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
target.Measure(new Size(100, 100)); target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100)); target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(new Size(0, 10), ((ILogicalScrollable)target).Viewport); Assert.Equal(new Size(100, 10), ((ILogicalScrollable)target).Viewport);
} }
[Fact] [Fact]
@ -61,7 +61,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
target.Measure(new Size(100, 100)); target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100)); target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(new Size(10, 0), ((ILogicalScrollable)target).Viewport); Assert.Equal(new Size(10, 100), ((ILogicalScrollable)target).Viewport);
} }
[Fact] [Fact]
@ -146,7 +146,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
target.Measure(new Size(100, 95)); target.Measure(new Size(100, 95));
target.Arrange(new Rect(0, 0, 100, 95)); target.Arrange(new Rect(0, 0, 100, 95));
Assert.Equal(new Size(0, 9), ((ILogicalScrollable)target).Viewport); Assert.Equal(new Size(100, 9), ((ILogicalScrollable)target).Viewport);
} }
[Fact] [Fact]
@ -772,6 +772,19 @@ namespace Avalonia.Controls.UnitTests.Presenters
Assert.Same(target.Panel.Children[0], result); Assert.Same(target.Panel.Children[0], result);
} }
} }
[Fact]
public void Should_Return_Horizontal_Extent_And_Viewport()
{
var target = CreateTarget();
target.ApplyTemplate();
target.Measure(new Size(5, 100));
target.Arrange(new Rect(0, 0, 5, 100));
Assert.Equal(new Size(10, 20), ((ILogicalScrollable)target).Extent);
Assert.Equal(new Size(5, 10), ((ILogicalScrollable)target).Viewport);
}
} }
public class Horizontal public class Horizontal

Loading…
Cancel
Save