Browse Source

Add support for layout that the children have multi span but also have desired size.

pull/1517/head
walterlv 8 years ago
parent
commit
163744b89b
  1. 1
      src/Avalonia.Controls/Avalonia.Controls.csproj
  2. 83
      src/Avalonia.Controls/Utils/GridLayout.cs
  3. 35
      tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs
  4. 29
      tests/Avalonia.Controls.UnitTests/GridMocks.cs
  5. 27
      tests/Avalonia.Controls.UnitTests/GridTests.cs

1
src/Avalonia.Controls/Avalonia.Controls.csproj

@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">

83
src/Avalonia.Controls/Utils/GridLayout.cs

@ -68,7 +68,7 @@ namespace Avalonia.Controls.Utils
{ {
var index = i; var index = i;
var convention = _conventions[index]; var convention = _conventions[index];
if (convention.Length.IsAuto) if (convention.Length.IsAuto || convention.Length.IsStar)
{ {
foreach (var pair in source.Where(x => foreach (var pair in source.Where(x =>
x.Value.index <= index && index < x.Value.index + x.Value.span)) x.Value.index <= index && index < x.Value.index + x.Value.span))
@ -99,10 +99,10 @@ namespace Avalonia.Controls.Utils
var aggregatedLength = 0.0; var aggregatedLength = 0.0;
double starUnitLength; double starUnitLength;
// M2/6. Exclude all the pixel lengths, so that we can calculate the star lengths. // M2/7. Exclude all the pixel lengths, so that we can calculate the star lengths.
aggregatedLength += conventions.Where(x => x.Length.IsAbsolute).Sum(x => x.Length.Value); aggregatedLength += conventions.Where(x => x.Length.IsAbsolute).Sum(x => x.Length.Value);
// M3/6. Exclude all the * lengths that have reached min value. // M3/7. Exclude all the * lengths that have reached min value.
var shouldTestStarMin = true; var shouldTestStarMin = true;
while (shouldTestStarMin) while (shouldTestStarMin)
{ {
@ -126,7 +126,7 @@ namespace Avalonia.Controls.Utils
shouldTestStarMin = @fixed; shouldTestStarMin = @fixed;
} }
// M4/6. Exclude all the Auto lengths that have not-zero desired size. // M4/7. Exclude all the Auto lengths that have not-zero desired size.
var shouldTestAuto = true; var shouldTestAuto = true;
while (shouldTestAuto) while (shouldTestAuto)
{ {
@ -140,26 +140,7 @@ namespace Avalonia.Controls.Utils
continue; continue;
} }
var index = i; var more = ApplyAdditionalConventionsForAuto(conventions, i, starUnitLength);
var more = 0.0;
foreach (var additional in _additionalConventions)
{
// If the additional conventions contains the Auto column/row, try to determine the Auto column/row length.
if (additional.Index <= index && index < additional.Index + additional.Span)
{
var starUnit = starUnitLength;
var min = Enumerable.Range(additional.Index, additional.Span)
.Select(x =>
{
var c = conventions[x];
if (c.Length.IsAbsolute) return c.Length.Value;
if (c.Length.IsStar) return c.Length.Value * starUnit;
return 0.0;
}).Sum();
more = Math.Max(additional.Min - min, more);
}
}
convention.Fix(more); convention.Fix(more);
aggregatedLength += more; aggregatedLength += more;
@fixed = true; @fixed = true;
@ -169,12 +150,17 @@ namespace Avalonia.Controls.Utils
shouldTestAuto = @fixed; shouldTestAuto = @fixed;
} }
// M5/6. Determine the desired length of the grid for current contaienr length. Its value stores in desiredLength. // M5/7. Expand the stars according to the additional conventions (usually the child desired length).
var desiredStarMin = AggregateAdditionalConventionsForStars(conventions);
aggregatedLength += desiredStarMin;
// M6/7. Determine the desired length of the grid for current contaienr length. Its value stores in desiredLength.
// But if the container has infinite length, the grid desired length is stored in greedyDesiredLength. // But if the container has infinite length, the grid desired length is stored in greedyDesiredLength.
var desiredLength = containerLength - aggregatedLength >= 0.0 ? aggregatedLength : containerLength; var desiredLength = containerLength - aggregatedLength >= 0.0 ? aggregatedLength : containerLength;
var greedyDesiredLength = aggregatedLength; var greedyDesiredLength = aggregatedLength;
// M6/6. Expand all the left stars. These stars have no conventions or only have max value so they can be expanded from zero to constrant. // M7/7. Expand all the left stars. These stars have no conventions or only have max value so they can be expanded from zero to constrant.
var dynamicConvention = ExpandStars(conventions, containerLength); var dynamicConvention = ExpandStars(conventions, containerLength);
Clip(dynamicConvention, containerLength); Clip(dynamicConvention, containerLength);
@ -202,6 +188,51 @@ namespace Avalonia.Controls.Utils
return new ArrangeResult(measure.LengthList); return new ArrangeResult(measure.LengthList);
} }
[Pure]
private double ApplyAdditionalConventionsForAuto(IReadOnlyList<LengthConvention> conventions,
int index, double starUnitLength)
{
var more = 0.0;
foreach (var additional in _additionalConventions)
{
// If the additional conventions contains the Auto column/row, try to determine the Auto column/row length.
if (additional.Index <= index && index < additional.Index + additional.Span)
{
var min = Enumerable.Range(additional.Index, additional.Span)
.Select(x =>
{
var c = conventions[x];
if (c.Length.IsAbsolute) return c.Length.Value;
if (c.Length.IsStar) return c.Length.Value * starUnitLength;
return 0.0;
}).Sum();
more = Math.Max(additional.Min - min, more);
}
}
return more;
}
[Pure]
private double AggregateAdditionalConventionsForStars(
IReadOnlyList<LengthConvention> conventions)
{
// +-----------------------------------------------------------+
// | * | P | * | P | P | * | P | * | * |
// +-----------------------------------------------------------+
// |<- x ->| |<- z ->|
// |<- y ->|
// conveniences 是上面看到的那个列表,所有能够确定的 A、P 和 * 都已经转换成了 P;剩下的 * 只有 Max 是没确定的。
// _additionalConventions 是上面的 x、y、z…… 集合,只有最小值是可用的。
// 需要返回所有标记为 * 的方格的累加和的最小值。
var additionalConventions = _additionalConventions;
// TODO Calculate the min length of all the desired size.
return 150;
}
[Pure] [Pure]
private static List<double> ExpandStars(IEnumerable<LengthConvention> conventions, double constraint) private static List<double> ExpandStars(IEnumerable<LengthConvention> conventions, double constraint)
{ {

35
tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs

@ -115,7 +115,7 @@ namespace Avalonia.Controls.UnitTests
} }
[Theory] [Theory]
[InlineData("Auto,*,*", new[] { 100d, 100d, 100d }, 600d, 100d, new[] { 100d, 250d, 250d })] [InlineData("Auto,*,*", new[] { 100d, 100d, 100d }, 600d, 300d, new[] { 100d, 250d, 250d })]
public void MeasureArrange_ChildHasSize_Correct(string length, public void MeasureArrange_ChildHasSize_Correct(string length,
IList<double> childLengthList, double containerLength, IList<double> childLengthList, double containerLength,
double expectedDesiredLength, IList<double> expectedLengthList) double expectedDesiredLength, IList<double> expectedLengthList)
@ -136,5 +136,38 @@ namespace Avalonia.Controls.UnitTests
var arrange = layout.Arrange(containerLength, measure); var arrange = layout.Arrange(containerLength, measure);
Assert.Equal(expectedLengthList, arrange.LengthList); Assert.Equal(expectedLengthList, arrange.LengthList);
} }
[Theory]
[InlineData(Inf, 250d, new[] { 100d, Inf, Inf }, new[] { 100d, 50d, 100d })]
[InlineData(400d, 250d, new[] { 100d, 100d, 200d }, new[] { 100d, 100d, 200d })]
[InlineData(325d, 250d, new[] { 100d, 75d, 150d }, new[] { 100d, 75d, 150d })]
[InlineData(250d, 250d, new[] { 100d, 50d, 100d }, new[] { 100d, 50d, 100d })]
[InlineData(160d, 160d, new[] { 100d, 20d, 40d }, new[] { 100d, 20d, 40d })]
public void MeasureArrange_ChildHasSizeAndHasMultiSpan_Correct(
double containerLength, double expectedDesiredLength,
IList<double> expectedMeasureLengthList, IList<double> expectedArrangeLengthList)
{
var length = "100,*,2*";
var childLengthList = new[] { 150d, 150d, 150d };
var spans = new[] { 1, 2, 1 };
// Arrange
var lengthList = new ColumnDefinitions(length);
var layout = new GridLayout(lengthList);
layout.AppendMeasureConventions(
Enumerable.Range(0, lengthList.Count).ToDictionary(x => x, x => (x, spans[x])),
x => childLengthList[x]);
// Measure - Action & Assert
var measure = layout.Measure(containerLength);
Assert.Equal(expectedDesiredLength, measure.DesiredLength);
Assert.Equal(expectedMeasureLengthList, measure.LengthList);
// Arrange - Action & Assert
var arrange = layout.Arrange(
double.IsInfinity(containerLength) ? measure.DesiredLength : containerLength,
measure);
Assert.Equal(expectedArrangeLengthList, arrange.LengthList);
}
} }
} }

29
tests/Avalonia.Controls.UnitTests/GridMocks.cs

@ -6,6 +6,21 @@ namespace Avalonia.Controls.UnitTests
{ {
internal static class GridMock internal static class GridMock
{ {
/// <summary>
/// Create a mock grid to test its row layout.
/// This method contains Arrange (`new Grid()`) and Action (`Measure()`/`Arrange()`).
/// </summary>
/// <param name="measure">The measure height of this grid. PositiveInfinity by default.</param>
/// <param name="arrange">The arrange height of this grid. DesiredSize.Height by default.</param>
/// <returns>The mock grid that its children bounds will be tested.</returns>
internal static Grid New(Size measure = default, Size arrange = default)
{
var grid = new Grid();
grid.Measure(measure == default ? new Size(double.PositiveInfinity, double.PositiveInfinity) : measure);
grid.Arrange(new Rect(default, arrange == default ? grid.DesiredSize : arrange));
return grid;
}
/// <summary> /// <summary>
/// Create a mock grid to test its row layout. /// Create a mock grid to test its row layout.
/// This method contains Arrange (`new Grid()`) and Action (`Measure()`/`Arrange()`). /// This method contains Arrange (`new Grid()`) and Action (`Measure()`/`Arrange()`).
@ -25,7 +40,12 @@ namespace Avalonia.Controls.UnitTests
} }
grid.Measure(new Size(double.PositiveInfinity, measure == default ? double.PositiveInfinity : measure)); grid.Measure(new Size(double.PositiveInfinity, measure == default ? double.PositiveInfinity : measure));
grid.Arrange(new Rect(0, 0, 0, arrange == default ? grid.DesiredSize.Width : arrange)); if (arrange == default)
{
arrange = measure == default ? grid.DesiredSize.Width : measure;
}
grid.Arrange(new Rect(0, 0, 0, arrange));
return grid; return grid;
} }
@ -49,7 +69,12 @@ namespace Avalonia.Controls.UnitTests
} }
grid.Measure(new Size(measure == default ? double.PositiveInfinity : measure, double.PositiveInfinity)); grid.Measure(new Size(measure == default ? double.PositiveInfinity : measure, double.PositiveInfinity));
grid.Arrange(new Rect(0, 0, arrange == default ? grid.DesiredSize.Width : arrange, 0)); if (arrange == default)
{
arrange = measure == default ? grid.DesiredSize.Width : measure;
}
grid.Arrange(new Rect(0, 0, arrange, 0));
return grid; return grid;
} }

27
tests/Avalonia.Controls.UnitTests/GridTests.cs

@ -83,12 +83,23 @@ namespace Avalonia.Controls.UnitTests
GridAssert.ChildrenWidth(columnGrid, 50, 100, 150); GridAssert.ChildrenWidth(columnGrid, 50, 100, 150);
} }
[Fact]
public void Layout_NoRowColumn_BoundsCorrect()
{
// Arrange & Action
var grid = GridMock.New(arrange: new Size(600, 200));
// Assert
GridAssert.ChildrenHeight(grid, 600);
GridAssert.ChildrenWidth(grid, 200);
}
[Fact] [Fact]
public void Layout_StarRowColumn_BoundsCorrect() public void Layout_StarRowColumn_BoundsCorrect()
{ {
// Arrange & Action // Arrange & Action
var rowGrid = GridMock.New(new RowDefinitions("1*,2*,3*"), arrange: 600); var rowGrid = GridMock.New(new RowDefinitions("1*,2*,3*"), 600);
var columnGrid = GridMock.New(new ColumnDefinitions("*,*,2*"), arrange: 600); var columnGrid = GridMock.New(new ColumnDefinitions("*,*,2*"), 600);
// Assert // Assert
GridAssert.ChildrenHeight(rowGrid, 100, 200, 300); GridAssert.ChildrenHeight(rowGrid, 100, 200, 300);
@ -99,8 +110,8 @@ namespace Avalonia.Controls.UnitTests
public void Layout_MixPixelStarRowColumn_BoundsCorrect() public void Layout_MixPixelStarRowColumn_BoundsCorrect()
{ {
// Arrange & Action // Arrange & Action
var rowGrid = GridMock.New(new RowDefinitions("1*,2*,150"), arrange: 600); var rowGrid = GridMock.New(new RowDefinitions("1*,2*,150"), 600);
var columnGrid = GridMock.New(new ColumnDefinitions("1*,2*,150"), arrange: 600); var columnGrid = GridMock.New(new ColumnDefinitions("1*,2*,150"), 600);
// Assert // Assert
GridAssert.ChildrenHeight(rowGrid, 150, 300, 150); GridAssert.ChildrenHeight(rowGrid, 150, 300, 150);
@ -116,13 +127,13 @@ namespace Avalonia.Controls.UnitTests
new RowDefinition(1, GridUnitType.Star) { MinHeight = 200 }, new RowDefinition(1, GridUnitType.Star) { MinHeight = 200 },
new RowDefinition(1, GridUnitType.Star), new RowDefinition(1, GridUnitType.Star),
new RowDefinition(1, GridUnitType.Star), new RowDefinition(1, GridUnitType.Star),
}, arrange: 300); }, 300);
var columnGrid = GridMock.New(new ColumnDefinitions var columnGrid = GridMock.New(new ColumnDefinitions
{ {
new ColumnDefinition(1, GridUnitType.Star) { MinWidth = 200 }, new ColumnDefinition(1, GridUnitType.Star) { MinWidth = 200 },
new ColumnDefinition(1, GridUnitType.Star), new ColumnDefinition(1, GridUnitType.Star),
new ColumnDefinition(1, GridUnitType.Star), new ColumnDefinition(1, GridUnitType.Star),
}, arrange: 300); }, 300);
// Assert // Assert
GridAssert.ChildrenHeight(rowGrid, 200, 50, 50); GridAssert.ChildrenHeight(rowGrid, 200, 50, 50);
@ -138,13 +149,13 @@ namespace Avalonia.Controls.UnitTests
new RowDefinition(1, GridUnitType.Star) { MaxHeight = 200 }, new RowDefinition(1, GridUnitType.Star) { MaxHeight = 200 },
new RowDefinition(1, GridUnitType.Star), new RowDefinition(1, GridUnitType.Star),
new RowDefinition(1, GridUnitType.Star), new RowDefinition(1, GridUnitType.Star),
}, arrange: 800); }, 800);
var columnGrid = GridMock.New(new ColumnDefinitions var columnGrid = GridMock.New(new ColumnDefinitions
{ {
new ColumnDefinition(1, GridUnitType.Star) { MaxWidth = 200 }, new ColumnDefinition(1, GridUnitType.Star) { MaxWidth = 200 },
new ColumnDefinition(1, GridUnitType.Star), new ColumnDefinition(1, GridUnitType.Star),
new ColumnDefinition(1, GridUnitType.Star), new ColumnDefinition(1, GridUnitType.Star),
}, arrange: 800); }, 800);
// Assert // Assert
GridAssert.ChildrenHeight(rowGrid, 200, 300, 300); GridAssert.ChildrenHeight(rowGrid, 200, 300, 300);

Loading…
Cancel
Save