Browse Source

Merge pull request #3771 from AvaloniaUI/feature/attachedlayout-non-virtualizing

Added NonVirtualizingStackLayout.
castxml-0.2
Steven Kirk 6 years ago
committed by GitHub
parent
commit
07c78345a1
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 104
      src/Avalonia.Layout/AttachedLayout.cs
  2. 45
      src/Avalonia.Layout/LayoutContextAdapter.cs
  3. 36
      src/Avalonia.Layout/NonVirtualizingLayout.cs
  4. 17
      src/Avalonia.Layout/NonVirtualizingLayoutContext.cs
  5. 160
      src/Avalonia.Layout/NonVirtualizingStackLayout.cs
  6. 8
      src/Avalonia.Layout/StackLayout.cs
  7. 8
      src/Avalonia.Layout/UniformGridLayout.cs
  8. 42
      src/Avalonia.Layout/VirtualLayoutContextAdapter.cs
  9. 36
      src/Avalonia.Layout/VirtualizingLayout.cs
  10. 5
      src/Avalonia.Layout/VirtualizingLayoutContext.cs
  11. 335
      tests/Avalonia.Layout.UnitTests/NonVirtualizingStackLayoutTests.cs

104
src/Avalonia.Layout/AttachedLayout.cs

@ -46,7 +46,23 @@ namespace Avalonia.Layout
/// <see cref="VirtualizingLayout.InitializeForContextCore"/> to provide the behavior for
/// this method in a derived class.
/// </remarks>
public abstract void InitializeForContext(LayoutContext context);
public void InitializeForContext(LayoutContext context)
{
if (this is VirtualizingLayout virtualizingLayout)
{
var virtualizingContext = GetVirtualizingLayoutContext(context);
virtualizingLayout.InitializeForContextCore(virtualizingContext);
}
else if (this is NonVirtualizingLayout nonVirtualizingLayout)
{
var nonVirtualizingContext = GetNonVirtualizingLayoutContext(context);
nonVirtualizingLayout.InitializeForContextCore(nonVirtualizingContext);
}
else
{
throw new NotSupportedException();
}
}
/// <summary>
/// Removes any state the layout previously stored on the ILayoutable container.
@ -55,7 +71,23 @@ namespace Avalonia.Layout
/// The context object that facilitates communication between the layout and its host
/// container.
/// </param>
public abstract void UninitializeForContext(LayoutContext context);
public void UninitializeForContext(LayoutContext context)
{
if (this is VirtualizingLayout virtualizingLayout)
{
var virtualizingContext = GetVirtualizingLayoutContext(context);
virtualizingLayout.UninitializeForContextCore(virtualizingContext);
}
else if (this is NonVirtualizingLayout nonVirtualizingLayout)
{
var nonVirtualizingContext = GetNonVirtualizingLayoutContext(context);
nonVirtualizingLayout.UninitializeForContextCore(nonVirtualizingContext);
}
else
{
throw new NotSupportedException();
}
}
/// <summary>
/// Suggests a DesiredSize for a container element. A container element that supports
@ -73,7 +105,23 @@ namespace Avalonia.Layout
/// if scrolling or other resize behavior is possible in that particular container.
/// </param>
/// <returns></returns>
public abstract Size Measure(LayoutContext context, Size availableSize);
public Size Measure(LayoutContext context, Size availableSize)
{
if (this is VirtualizingLayout virtualizingLayout)
{
var virtualizingContext = GetVirtualizingLayoutContext(context);
return virtualizingLayout.MeasureOverride(virtualizingContext, availableSize);
}
else if (this is NonVirtualizingLayout nonVirtualizingLayout)
{
var nonVirtualizingContext = GetNonVirtualizingLayoutContext(context);
return nonVirtualizingLayout.MeasureOverride(nonVirtualizingContext, availableSize);
}
else
{
throw new NotSupportedException();
}
}
/// <summary>
/// Positions child elements and determines a size for a container UIElement. Container
@ -88,7 +136,23 @@ namespace Avalonia.Layout
/// The final size that the container computes for the child in layout.
/// </param>
/// <returns>The actual size that is used after the element is arranged in layout.</returns>
public abstract Size Arrange(LayoutContext context, Size finalSize);
public Size Arrange(LayoutContext context, Size finalSize)
{
if (this is VirtualizingLayout virtualizingLayout)
{
var virtualizingContext = GetVirtualizingLayoutContext(context);
return virtualizingLayout.ArrangeOverride(virtualizingContext, finalSize);
}
else if (this is NonVirtualizingLayout nonVirtualizingLayout)
{
var nonVirtualizingContext = GetNonVirtualizingLayoutContext(context);
return nonVirtualizingLayout.ArrangeOverride(nonVirtualizingContext, finalSize);
}
else
{
throw new NotSupportedException();
}
}
/// <summary>
/// Invalidates the measurement state (layout) for all ILayoutable containers that reference
@ -102,5 +166,37 @@ namespace Avalonia.Layout
/// occurs asynchronously.
/// </summary>
protected void InvalidateArrange() => ArrangeInvalidated?.Invoke(this, EventArgs.Empty);
private VirtualizingLayoutContext GetVirtualizingLayoutContext(LayoutContext context)
{
if (context is VirtualizingLayoutContext virtualizingContext)
{
return virtualizingContext;
}
else if (context is NonVirtualizingLayoutContext nonVirtualizingContext)
{
return nonVirtualizingContext.GetVirtualizingContextAdapter();
}
else
{
throw new NotSupportedException();
}
}
private NonVirtualizingLayoutContext GetNonVirtualizingLayoutContext(LayoutContext context)
{
if (context is NonVirtualizingLayoutContext nonVirtualizingContext)
{
return nonVirtualizingContext;
}
else if (context is VirtualizingLayoutContext virtualizingContext)
{
return virtualizingContext.GetNonVirtualizingContextAdapter();
}
else
{
throw new NotSupportedException();
}
}
}
}

45
src/Avalonia.Layout/LayoutContextAdapter.cs

@ -0,0 +1,45 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
namespace Avalonia.Layout
{
internal class LayoutContextAdapter : VirtualizingLayoutContext
{
private readonly NonVirtualizingLayoutContext _nonVirtualizingContext;
public LayoutContextAdapter(NonVirtualizingLayoutContext nonVirtualizingContext)
{
_nonVirtualizingContext = nonVirtualizingContext;
}
protected override object LayoutStateCore
{
get => _nonVirtualizingContext.LayoutState;
set => _nonVirtualizingContext.LayoutState = value;
}
protected override Point LayoutOriginCore
{
get => default;
set
{
if (value != default)
{
throw new InvalidOperationException("LayoutOrigin must be at (0,0) when RealizationRect is infinite sized.");
}
}
}
protected override Rect RealizationRectCore() => new Rect(Size.Infinity);
protected override int ItemCountCore() => _nonVirtualizingContext.Children.Count;
protected override object GetItemAtCore(int index) => _nonVirtualizingContext.Children[index];
protected override ILayoutable GetOrCreateElementAtCore(int index, ElementRealizationOptions options) =>
_nonVirtualizingContext.Children[index];
protected override void RecycleElementCore(ILayoutable element) { }
}
}

36
src/Avalonia.Layout/NonVirtualizingLayout.cs

@ -17,30 +17,6 @@ namespace Avalonia.Layout
/// </remarks>
public abstract class NonVirtualizingLayout : AttachedLayout
{
/// <inheritdoc/>
public sealed override void InitializeForContext(LayoutContext context)
{
InitializeForContextCore((NonVirtualizingLayoutContext)context);
}
/// <inheritdoc/>
public sealed override void UninitializeForContext(LayoutContext context)
{
UninitializeForContextCore((NonVirtualizingLayoutContext)context);
}
/// <inheritdoc/>
public sealed override Size Measure(LayoutContext context, Size availableSize)
{
return MeasureOverride((NonVirtualizingLayoutContext)context, availableSize);
}
/// <inheritdoc/>
public sealed override Size Arrange(LayoutContext context, Size finalSize)
{
return ArrangeOverride((NonVirtualizingLayoutContext)context, finalSize);
}
/// <summary>
/// When overridden in a derived class, initializes any per-container state the layout
/// requires when it is attached to an ILayoutable container.
@ -49,7 +25,7 @@ namespace Avalonia.Layout
/// The context object that facilitates communication between the layout and its host
/// container.
/// </param>
protected virtual void InitializeForContextCore(LayoutContext context)
protected internal virtual void InitializeForContextCore(LayoutContext context)
{
}
@ -61,7 +37,7 @@ namespace Avalonia.Layout
/// The context object that facilitates communication between the layout and its host
/// container.
/// </param>
protected virtual void UninitializeForContextCore(LayoutContext context)
protected internal virtual void UninitializeForContextCore(LayoutContext context)
{
}
@ -83,7 +59,9 @@ namespace Avalonia.Layout
/// of the allocated sizes for child objects or based on other considerations such as a
/// fixed container size.
/// </returns>
protected abstract Size MeasureOverride(NonVirtualizingLayoutContext context, Size availableSize);
protected internal abstract Size MeasureOverride(
NonVirtualizingLayoutContext context,
Size availableSize);
/// <summary>
/// When implemented in a derived class, provides the behavior for the "Arrange" pass of
@ -98,6 +76,8 @@ namespace Avalonia.Layout
/// its children.
/// </param>
/// <returns>The actual size that is used after the element is arranged in layout.</returns>
protected virtual Size ArrangeOverride(NonVirtualizingLayoutContext context, Size finalSize) => finalSize;
protected internal virtual Size ArrangeOverride(
NonVirtualizingLayoutContext context,
Size finalSize) => finalSize;
}
}

17
src/Avalonia.Layout/NonVirtualizingLayoutContext.cs

@ -3,6 +3,8 @@
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System.Collections.Generic;
namespace Avalonia.Layout
{
/// <summary>
@ -10,5 +12,20 @@ namespace Avalonia.Layout
/// </summary>
public abstract class NonVirtualizingLayoutContext : LayoutContext
{
private VirtualizingLayoutContext _contextAdapter;
/// <summary>
/// Gets the collection of child controls from the container that provides the context.
/// </summary>
public IReadOnlyList<ILayoutable> Children => ChildrenCore;
/// <summary>
/// Implements the behavior for getting the return value of <see cref="Children"/> in a
/// derived or custom <see cref="NonVirtualizingLayoutContext"/>.
/// </summary>
protected abstract IReadOnlyList<ILayoutable> ChildrenCore { get; }
internal VirtualizingLayoutContext GetVirtualizingContextAdapter() =>
_contextAdapter ?? (_contextAdapter = new LayoutContextAdapter(this));
}
}

160
src/Avalonia.Layout/NonVirtualizingStackLayout.cs

@ -0,0 +1,160 @@
using System;
using System.Collections.Generic;
using System.Text;
using Avalonia.Data;
namespace Avalonia.Layout
{
public class NonVirtualizingStackLayout : NonVirtualizingLayout
{
/// <summary>
/// Defines the <see cref="Orientation"/> property.
/// </summary>
public static readonly StyledProperty<Orientation> OrientationProperty =
StackLayout.OrientationProperty.AddOwner<NonVirtualizingStackLayout>();
/// <summary>
/// Defines the <see cref="Spacing"/> property.
/// </summary>
public static readonly StyledProperty<double> SpacingProperty =
StackLayout.SpacingProperty.AddOwner<NonVirtualizingStackLayout>();
/// <summary>
/// Gets or sets the axis along which items are laid out.
/// </summary>
/// <value>
/// One of the enumeration values that specifies the axis along which items are laid out.
/// The default is Vertical.
/// </value>
public Orientation Orientation
{
get => GetValue(OrientationProperty);
set => SetValue(OrientationProperty, value);
}
/// <summary>
/// Gets or sets a uniform distance (in pixels) between stacked items. It is applied in the
/// direction of the StackLayout's Orientation.
/// </summary>
public double Spacing
{
get => GetValue(SpacingProperty);
set => SetValue(SpacingProperty, value);
}
protected internal override Size MeasureOverride(
NonVirtualizingLayoutContext context,
Size availableSize)
{
var extentU = 0.0;
var extentV = 0.0;
var childCount = context.Children.Count;
var isVertical = Orientation == Orientation.Vertical;
var spacing = Spacing;
var constraint = isVertical ?
availableSize.WithHeight(double.PositiveInfinity) :
availableSize.WithWidth(double.PositiveInfinity);
for (var i = 0; i < childCount; ++i)
{
var element = context.Children[i];
if (!element.IsVisible)
{
continue;
}
element.Measure(constraint);
if (isVertical)
{
extentU += element.DesiredSize.Height;
extentV = Math.Max(extentV, element.DesiredSize.Width);
}
else
{
extentU += element.DesiredSize.Width;
extentV = Math.Max(extentV, element.DesiredSize.Height);
}
if (i < childCount - 1)
{
extentU += spacing;
}
}
return isVertical ? new Size(extentV, extentU) : new Size(extentU, extentV);
}
protected internal override Size ArrangeOverride(
NonVirtualizingLayoutContext context,
Size finalSize)
{
var u = 0.0;
var childCount = context.Children.Count;
var isVertical = Orientation == Orientation.Vertical;
var spacing = Spacing;
var bounds = new Rect();
for (var i = 0; i < childCount; ++i)
{
var element = context.Children[i];
if (!element.IsVisible)
{
continue;
}
bounds = isVertical ?
LayoutVertical(element, u, finalSize) :
LayoutHorizontal(element, u, finalSize);
element.Arrange(bounds);
u = (isVertical ? bounds.Bottom : bounds.Right) + spacing;
}
return new Size(bounds.Right, bounds.Bottom);
}
private static Rect LayoutVertical(ILayoutable element, double y, Size constraint)
{
var x = 0.0;
var width = element.DesiredSize.Width;
switch (element.HorizontalAlignment)
{
case HorizontalAlignment.Center:
x += (constraint.Width - element.DesiredSize.Width) / 2;
break;
case HorizontalAlignment.Right:
x += constraint.Width - element.DesiredSize.Width;
break;
case HorizontalAlignment.Stretch:
width = constraint.Width;
break;
}
return new Rect(x, y, width, element.DesiredSize.Height);
}
private static Rect LayoutHorizontal(ILayoutable element, double x, Size constraint)
{
var y = 0.0;
var height = element.DesiredSize.Height;
switch (element.VerticalAlignment)
{
case VerticalAlignment.Center:
y += (constraint.Height - element.DesiredSize.Height) / 2;
break;
case VerticalAlignment.Bottom:
y += constraint.Height - element.DesiredSize.Height;
break;
case VerticalAlignment.Stretch:
height = constraint.Height;
break;
}
return new Rect(x, y, element.DesiredSize.Width, height);
}
}
}

8
src/Avalonia.Layout/StackLayout.cs

@ -234,7 +234,7 @@ namespace Avalonia.Layout
return new FlowLayoutAnchorInfo { Index = anchorIndex, Offset = offset, };
}
protected override void InitializeForContextCore(VirtualizingLayoutContext context)
protected internal override void InitializeForContextCore(VirtualizingLayoutContext context)
{
var state = context.LayoutState;
var stackState = state as StackLayoutState;
@ -254,13 +254,13 @@ namespace Avalonia.Layout
stackState.InitializeForContext(context, this);
}
protected override void UninitializeForContextCore(VirtualizingLayoutContext context)
protected internal override void UninitializeForContextCore(VirtualizingLayoutContext context)
{
var stackState = (StackLayoutState)context.LayoutState;
stackState.UninitializeForContext(context);
}
protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
protected internal override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
{
var desiredSize = GetFlowAlgorithm(context).Measure(
availableSize,
@ -275,7 +275,7 @@ namespace Avalonia.Layout
return new Size(desiredSize.Width, desiredSize.Height);
}
protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
protected internal override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
{
var value = GetFlowAlgorithm(context).Arrange(
finalSize,

8
src/Avalonia.Layout/UniformGridLayout.cs

@ -392,7 +392,7 @@ namespace Avalonia.Layout
{
}
protected override void InitializeForContextCore(VirtualizingLayoutContext context)
protected internal override void InitializeForContextCore(VirtualizingLayoutContext context)
{
var state = context.LayoutState;
var gridState = state as UniformGridLayoutState;
@ -412,13 +412,13 @@ namespace Avalonia.Layout
gridState.InitializeForContext(context, this);
}
protected override void UninitializeForContextCore(VirtualizingLayoutContext context)
protected internal override void UninitializeForContextCore(VirtualizingLayoutContext context)
{
var gridState = (UniformGridLayoutState)context.LayoutState;
gridState.UninitializeForContext(context);
}
protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
protected internal override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
{
// Set the width and height on the grid state. If the user already set them then use the preset.
// If not, we have to measure the first element and get back a size which we're going to be using for the rest of the items.
@ -442,7 +442,7 @@ namespace Avalonia.Layout
return new Size(desiredSize.Width, desiredSize.Height);
}
protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
protected internal override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
{
var value = GetFlowAlgorithm(context).Arrange(
finalSize,

42
src/Avalonia.Layout/VirtualLayoutContextAdapter.cs

@ -0,0 +1,42 @@
using System.Collections;
using System.Collections.Generic;
namespace Avalonia.Layout
{
public class VirtualLayoutContextAdapter : NonVirtualizingLayoutContext
{
private readonly VirtualizingLayoutContext _virtualizingContext;
private ChildrenCollection _children;
public VirtualLayoutContextAdapter(VirtualizingLayoutContext virtualizingContext)
{
_virtualizingContext = virtualizingContext;
}
protected override object LayoutStateCore
{
get => _virtualizingContext.LayoutState;
set => _virtualizingContext.LayoutState = value;
}
protected override IReadOnlyList<ILayoutable> ChildrenCore =>
_children ?? (_children = new ChildrenCollection(_virtualizingContext));
private class ChildrenCollection : IReadOnlyList<ILayoutable>
{
private readonly VirtualizingLayoutContext _context;
public ChildrenCollection(VirtualizingLayoutContext context) => _context = context;
public ILayoutable this[int index] => _context.GetOrCreateElementAt(index);
public int Count => _context.ItemCount;
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public IEnumerator<ILayoutable> GetEnumerator()
{
for (var i = 0; i < Count; ++i)
{
yield return this[i];
}
}
}
}
}

36
src/Avalonia.Layout/VirtualizingLayout.cs

@ -19,30 +19,6 @@ namespace Avalonia.Layout
/// </remarks>
public abstract class VirtualizingLayout : AttachedLayout
{
/// <inheritdoc/>
public sealed override void InitializeForContext(LayoutContext context)
{
InitializeForContextCore((VirtualizingLayoutContext)context);
}
/// <inheritdoc/>
public sealed override void UninitializeForContext(LayoutContext context)
{
UninitializeForContextCore((VirtualizingLayoutContext)context);
}
/// <inheritdoc/>
public sealed override Size Measure(LayoutContext context, Size availableSize)
{
return MeasureOverride((VirtualizingLayoutContext)context, availableSize);
}
/// <inheritdoc/>
public sealed override Size Arrange(LayoutContext context, Size finalSize)
{
return ArrangeOverride((VirtualizingLayoutContext)context, finalSize);
}
/// <summary>
/// Notifies the layout when the data collection assigned to the container element (Items)
/// has changed.
@ -70,7 +46,7 @@ namespace Avalonia.Layout
/// The context object that facilitates communication between the layout and its host
/// container.
/// </param>
protected virtual void InitializeForContextCore(VirtualizingLayoutContext context)
protected internal virtual void InitializeForContextCore(VirtualizingLayoutContext context)
{
}
@ -82,7 +58,7 @@ namespace Avalonia.Layout
/// The context object that facilitates communication between the layout and its host
/// container.
/// </param>
protected virtual void UninitializeForContextCore(VirtualizingLayoutContext context)
protected internal virtual void UninitializeForContextCore(VirtualizingLayoutContext context)
{
}
@ -104,7 +80,9 @@ namespace Avalonia.Layout
/// of the allocated sizes for child objects or based on other considerations such as a
/// fixed container size.
/// </returns>
protected abstract Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize);
protected internal abstract Size MeasureOverride(
VirtualizingLayoutContext context,
Size availableSize);
/// <summary>
/// When implemented in a derived class, provides the behavior for the "Arrange" pass of
@ -119,7 +97,9 @@ namespace Avalonia.Layout
/// its children.
/// </param>
/// <returns>The actual size that is used after the element is arranged in layout.</returns>
protected virtual Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) => finalSize;
protected internal virtual Size ArrangeOverride(
VirtualizingLayoutContext context,
Size finalSize) => finalSize;
/// <summary>
/// Notifies the layout when the data collection assigned to the container element (Items)

5
src/Avalonia.Layout/VirtualizingLayoutContext.cs

@ -43,6 +43,8 @@ namespace Avalonia.Layout
/// </summary>
public abstract class VirtualizingLayoutContext : LayoutContext
{
private NonVirtualizingLayoutContext _contextAdapter;
/// <summary>
/// Gets the number of items in the data.
/// </summary>
@ -186,5 +188,8 @@ namespace Avalonia.Layout
/// </summary>
/// <param name="element">The element to clear.</param>
protected abstract void RecycleElementCore(ILayoutable element);
internal NonVirtualizingLayoutContext GetNonVirtualizingContextAdapter() =>
_contextAdapter ?? (_contextAdapter = new VirtualLayoutContextAdapter(this));
}
}

335
tests/Avalonia.Layout.UnitTests/NonVirtualizingStackLayoutTests.cs

@ -0,0 +1,335 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls;
using Xunit;
namespace Avalonia.Layout.UnitTests
{
public class NonVirtualizingStackLayoutTests
{
[Fact]
public void Lays_Out_Children_Vertically()
{
var target = new NonVirtualizingStackLayout { Orientation = Orientation.Vertical };
var context = CreateContext(new[]
{
new Border { Height = 20, Width = 120 },
new Border { Height = 30 },
new Border { Height = 50 },
});
var desiredSize = target.Measure(context, Size.Infinity);
var arrangeSize = target.Arrange(context, desiredSize);
Assert.Equal(new Size(120, 100), desiredSize);
Assert.Equal(new Size(120, 100), arrangeSize);
Assert.Equal(new Rect(0, 0, 120, 20), context.Children[0].Bounds);
Assert.Equal(new Rect(0, 20, 120, 30), context.Children[1].Bounds);
Assert.Equal(new Rect(0, 50, 120, 50), context.Children[2].Bounds);
}
[Fact]
public void Lays_Out_Children_Horizontally()
{
var target = new NonVirtualizingStackLayout { Orientation = Orientation.Horizontal };
var context = CreateContext(new[]
{
new Border { Width = 20, Height = 120 },
new Border { Width = 30 },
new Border { Width = 50 },
});
var desiredSize = target.Measure(context, Size.Infinity);
var arrangeSize = target.Arrange(context, desiredSize);
Assert.Equal(new Size(100, 120), desiredSize);
Assert.Equal(new Size(100, 120), arrangeSize);
Assert.Equal(new Rect(0, 0, 20, 120), context.Children[0].Bounds);
Assert.Equal(new Rect(20, 0, 30, 120), context.Children[1].Bounds);
Assert.Equal(new Rect(50, 0, 50, 120), context.Children[2].Bounds);
}
[Fact]
public void Lays_Out_Children_Vertically_With_Spacing()
{
var target = new NonVirtualizingStackLayout
{
Orientation = Orientation.Vertical,
Spacing = 10,
};
var context = CreateContext(new[]
{
new Border { Height = 20, Width = 120 },
new Border { Height = 30 },
new Border { Height = 50 },
});
var desiredSize = target.Measure(context, Size.Infinity);
var arrangeSize = target.Arrange(context, desiredSize);
Assert.Equal(new Size(120, 120), desiredSize);
Assert.Equal(new Size(120, 120), arrangeSize);
Assert.Equal(new Rect(0, 0, 120, 20), context.Children[0].Bounds);
Assert.Equal(new Rect(0, 30, 120, 30), context.Children[1].Bounds);
Assert.Equal(new Rect(0, 70, 120, 50), context.Children[2].Bounds);
}
[Fact]
public void Lays_Out_Children_Horizontally_With_Spacing()
{
var target = new NonVirtualizingStackLayout
{
Orientation = Orientation.Horizontal,
Spacing = 10,
};
var context = CreateContext(new[]
{
new Border { Width = 20, Height = 120 },
new Border { Width = 30 },
new Border { Width = 50 },
});
var desiredSize = target.Measure(context, Size.Infinity);
var arrangeSize = target.Arrange(context, desiredSize);
Assert.Equal(new Size(120, 120), desiredSize);
Assert.Equal(new Size(120, 120), arrangeSize);
Assert.Equal(new Rect(0, 0, 20, 120), context.Children[0].Bounds);
Assert.Equal(new Rect(30, 0, 30, 120), context.Children[1].Bounds);
Assert.Equal(new Rect(70, 0, 50, 120), context.Children[2].Bounds);
}
[Fact]
public void Arranges_Vertical_Children_With_Correct_Bounds()
{
var target = new NonVirtualizingStackLayout
{
Orientation = Orientation.Vertical
};
var context = CreateContext(new[]
{
new TestControl
{
HorizontalAlignment = HorizontalAlignment.Left,
MeasureSize = new Size(50, 10),
},
new TestControl
{
HorizontalAlignment = HorizontalAlignment.Left,
MeasureSize = new Size(150, 10),
},
new TestControl
{
HorizontalAlignment = HorizontalAlignment.Center,
MeasureSize = new Size(50, 10),
},
new TestControl
{
HorizontalAlignment = HorizontalAlignment.Center,
MeasureSize = new Size(150, 10),
},
new TestControl
{
HorizontalAlignment = HorizontalAlignment.Right,
MeasureSize = new Size(50, 10),
},
new TestControl
{
HorizontalAlignment = HorizontalAlignment.Right,
MeasureSize = new Size(150, 10),
},
new TestControl
{
HorizontalAlignment = HorizontalAlignment.Stretch,
MeasureSize = new Size(50, 10),
},
new TestControl
{
HorizontalAlignment = HorizontalAlignment.Stretch,
MeasureSize = new Size(150, 10),
},
});
var desiredSize = target.Measure(context, new Size(100, 150));
Assert.Equal(new Size(100, 80), desiredSize);
target.Arrange(context, desiredSize);
var bounds = context.Children.Select(x => x.Bounds).ToArray();
Assert.Equal(
new[]
{
new Rect(0, 0, 50, 10),
new Rect(0, 10, 100, 10),
new Rect(25, 20, 50, 10),
new Rect(0, 30, 100, 10),
new Rect(50, 40, 50, 10),
new Rect(0, 50, 100, 10),
new Rect(0, 60, 100, 10),
new Rect(0, 70, 100, 10),
}, bounds);
}
[Fact]
public void Arranges_Horizontal_Children_With_Correct_Bounds()
{
var target = new NonVirtualizingStackLayout
{
Orientation = Orientation.Horizontal
};
var context = CreateContext(new[]
{
new TestControl
{
VerticalAlignment = VerticalAlignment.Top,
MeasureSize = new Size(10, 50),
},
new TestControl
{
VerticalAlignment = VerticalAlignment.Top,
MeasureSize = new Size(10, 150),
},
new TestControl
{
VerticalAlignment = VerticalAlignment.Center,
MeasureSize = new Size(10, 50),
},
new TestControl
{
VerticalAlignment = VerticalAlignment.Center,
MeasureSize = new Size(10, 150),
},
new TestControl
{
VerticalAlignment = VerticalAlignment.Bottom,
MeasureSize = new Size(10, 50),
},
new TestControl
{
VerticalAlignment = VerticalAlignment.Bottom,
MeasureSize = new Size(10, 150),
},
new TestControl
{
VerticalAlignment = VerticalAlignment.Stretch,
MeasureSize = new Size(10, 50),
},
new TestControl
{
VerticalAlignment = VerticalAlignment.Stretch,
MeasureSize = new Size(10, 150),
},
});
var desiredSize = target.Measure(context, new Size(150, 100));
Assert.Equal(new Size(80, 100), desiredSize);
target.Arrange(context, desiredSize);
var bounds = context.Children.Select(x => x.Bounds).ToArray();
Assert.Equal(
new[]
{
new Rect(0, 0, 10, 50),
new Rect(10, 0, 10, 100),
new Rect(20, 25, 10, 50),
new Rect(30, 0, 10, 100),
new Rect(40, 50, 10, 50),
new Rect(50, 0, 10, 100),
new Rect(60, 0, 10, 100),
new Rect(70, 0, 10, 100),
}, bounds);
}
[Theory]
[InlineData(Orientation.Horizontal)]
[InlineData(Orientation.Vertical)]
public void Spacing_Not_Added_For_Invisible_Children(Orientation orientation)
{
var targetThreeChildrenOneInvisble = new NonVirtualizingStackLayout
{
Orientation = orientation,
Spacing = 40,
};
var contextThreeChildrenOneInvisble = CreateContext(new[]
{
new StackPanel { Width = 10, Height= 10, IsVisible = false },
new StackPanel { Width = 10, Height= 10 },
new StackPanel { Width = 10, Height= 10 },
});
var targetTwoChildrenNoneInvisible = new NonVirtualizingStackLayout
{
Spacing = 40,
Orientation = orientation,
};
var contextTwoChildrenNoneInvisible = CreateContext(new[]
{
new StackPanel { Width = 10, Height = 10 },
new StackPanel { Width = 10, Height = 10 }
});
var desiredSize1 = targetThreeChildrenOneInvisble.Measure(contextThreeChildrenOneInvisble, Size.Infinity);
var desiredSize2 = targetTwoChildrenNoneInvisible.Measure(contextTwoChildrenNoneInvisible, Size.Infinity);
Assert.Equal(desiredSize2, desiredSize1);
}
[Theory]
[InlineData(Orientation.Horizontal)]
[InlineData(Orientation.Vertical)]
public void Only_Arrange_Visible_Children(Orientation orientation)
{
var hiddenPanel = new Panel { Width = 10, Height = 10, IsVisible = false };
var panel = new Panel { Width = 10, Height = 10 };
var target = new NonVirtualizingStackLayout
{
Spacing = 40,
Orientation = orientation,
};
var context = CreateContext(new[]
{
hiddenPanel,
panel
});
var desiredSize = target.Measure(context, Size.Infinity);
var arrangeSize = target.Arrange(context, desiredSize);
Assert.Equal(new Size(10, 10), arrangeSize);
}
private NonVirtualizingLayoutContext CreateContext(Control[] children)
{
return new TestLayoutContext(children);
}
private class TestLayoutContext : NonVirtualizingLayoutContext
{
public TestLayoutContext(Control[] children) => ChildrenCore = children;
protected override IReadOnlyList<ILayoutable> ChildrenCore { get; }
}
private class TestControl : Control
{
public Size MeasureConstraint { get; private set; }
public Size MeasureSize { get; set; }
protected override Size MeasureOverride(Size availableSize)
{
MeasureConstraint = availableSize;
return MeasureSize;
}
}
}
}
Loading…
Cancel
Save