Browse Source

Order the grid codes and make most unit test passed.

pull/1517/head
walterlv 8 years ago
parent
commit
2052d53d68
  1. 443
      src/Avalonia.Controls/Grid.cs
  2. 22
      src/Avalonia.Controls/Utils/GridLayout.cs
  3. 1
      tests/Avalonia.Controls.UnitTests/GridMocks.cs
  4. 25
      tests/Avalonia.Controls.UnitTests/GridTests.cs

443
src/Avalonia.Controls/Grid.cs

@ -48,10 +48,6 @@ namespace Avalonia.Controls
private RowDefinitions _rowDefinitions;
private Segment[,] _rowMatrix;
private Segment[,] _colMatrix;
/// <summary>
/// Gets or sets the columns definitions for the grid.
/// </summary>
@ -186,7 +182,16 @@ namespace Avalonia.Controls
element.SetValue(RowSpanProperty, value);
}
/// <summary>
/// Gets the result of last column measuring produce.
/// Use this result to reduce the arrange calculation.
/// </summary>
private GridLayout.MeasureResult _columnMeasureCache;
/// <summary>
/// Gets the result of last row measuring produce.
/// Use this result to reduce the arrange calculation.
/// </summary>
private GridLayout.MeasureResult _rowMeasureCache;
/// <summary>
@ -196,11 +201,32 @@ namespace Avalonia.Controls
/// <returns>The desired size of the control.</returns>
protected override Size MeasureOverride(Size constraint)
{
// If the grid doesn't have any column/row definitions, it behaviors like a nomal panel.
if (ColumnDefinitions.Count == 0 && RowDefinitions.Count == 0)
{
var maxWidth = 0.0;
var maxHeight = 0.0;
foreach (var child in Children.OfType<Control>())
{
child.Measure(constraint);
maxWidth = Math.Max(maxWidth, child.DesiredSize.Width);
maxHeight = Math.Max(maxHeight, child.DesiredSize.Height);
}
maxWidth = Math.Min(maxWidth, constraint.Width);
maxHeight = Math.Min(maxHeight, constraint.Height);
return new Size(maxWidth, maxHeight);
}
// If the grid defines some columns or rows.
var measureCache = new Dictionary<Control, Size>();
var (safeColumns, safeRows) = GetSafeColumnRows();
var columnLayout = new GridLayout(ColumnDefinitions);
var rowLayout = new GridLayout(RowDefinitions);
// Note: If a child stays in a * or Auto column/row, use constraint to measure it.
columnLayout.AppendMeasureConventions(safeColumns, child => MeasureOnce(child, constraint).Width);
rowLayout.AppendMeasureConventions(safeRows, child => MeasureOnce(child, constraint).Height);
@ -221,6 +247,8 @@ namespace Avalonia.Controls
_rowMeasureCache = rowResult;
return new Size(columnResult.DesiredLength, rowResult.DesiredLength);
// Measure each child only once.
// If a child has been measured, it will just return the desired size.
Size MeasureOnce(Control child, Size size)
{
if (measureCache.TryGetValue(child, out var desiredSize))
@ -242,6 +270,20 @@ namespace Avalonia.Controls
/// <returns>The space taken.</returns>
protected override Size ArrangeOverride(Size finalSize)
{
// If the grid doesn't have any column/row definitions, it behaviors like a nomal panel.
if (ColumnDefinitions.Count == 0 && RowDefinitions.Count == 0)
{
foreach (var child in Children.OfType<Control>())
{
child.Arrange(new Rect(finalSize));
}
return finalSize;
}
// If the grid defines some columns or rows.
var (safeColumns, safeRows) = GetSafeColumnRows();
var columnLayout = new GridLayout(ColumnDefinitions);
@ -263,6 +305,10 @@ namespace Avalonia.Controls
return finalSize;
}
/// <summary>
/// Get the safe column/columnspan and safe row/rowspan.
/// The result of this method ensure that none of the children has a column/row out of the definitions.
/// </summary>
private (Dictionary<Control, (int index, int span)> safeColumns,
Dictionary<Control, (int index, int span)> safeRows) GetSafeColumnRows()
{
@ -301,22 +347,6 @@ namespace Avalonia.Controls
return (index, span);
}
private static double Clamp(double val, double min, double max)
{
if (val < min)
{
return min;
}
else if (val > max)
{
return max;
}
else
{
return val;
}
}
private static int ValidateColumn(AvaloniaObject o, int value)
{
if (value < 0)
@ -336,376 +366,5 @@ namespace Avalonia.Controls
return value;
}
private void CreateMatrices(int rowCount, int colCount)
{
if (_rowMatrix == null || _colMatrix == null ||
_rowMatrix.GetLength(0) != rowCount ||
_colMatrix.GetLength(0) != colCount)
{
_rowMatrix = new Segment[rowCount, rowCount];
_colMatrix = new Segment[colCount, colCount];
}
else
{
Array.Clear(_rowMatrix, 0, _rowMatrix.Length);
Array.Clear(_colMatrix, 0, _colMatrix.Length);
}
}
private void ExpandStarCols(Size availableSize)
{
int matrixCount = _colMatrix.GetLength(0);
int columnsCount = ColumnDefinitions.Count;
double width = availableSize.Width;
for (int i = 0; i < matrixCount; i++)
{
if (_colMatrix[i, i].Type == GridUnitType.Star)
{
_colMatrix[i, i].OfferedSize = 0;
}
else
{
width = Math.Max(width - _colMatrix[i, i].OfferedSize, 0);
}
}
AssignSize(_colMatrix, 0, matrixCount - 1, ref width, GridUnitType.Star, false);
width = Math.Max(0, width);
if (columnsCount > 0)
{
for (int i = 0; i < matrixCount; i++)
{
if (_colMatrix[i, i].Type == GridUnitType.Star)
{
ColumnDefinitions[i].ActualWidth = _colMatrix[i, i].OfferedSize;
}
}
}
}
private void ExpandStarRows(Size availableSize)
{
int matrixCount = _rowMatrix.GetLength(0);
int rowCount = RowDefinitions.Count;
double height = availableSize.Height;
// When expanding star rows, we need to zero out their height before
// calling AssignSize. AssignSize takes care of distributing the
// available size when there are Mins and Maxs applied.
for (int i = 0; i < matrixCount; i++)
{
if (_rowMatrix[i, i].Type == GridUnitType.Star)
{
_rowMatrix[i, i].OfferedSize = 0.0;
}
else
{
height = Math.Max(height - _rowMatrix[i, i].OfferedSize, 0);
}
}
AssignSize(_rowMatrix, 0, matrixCount - 1, ref height, GridUnitType.Star, false);
if (rowCount > 0)
{
for (int i = 0; i < matrixCount; i++)
{
if (_rowMatrix[i, i].Type == GridUnitType.Star)
{
RowDefinitions[i].ActualHeight = _rowMatrix[i, i].OfferedSize;
}
}
}
}
private void AssignSize(
Segment[,] matrix,
int start,
int end,
ref double size,
GridUnitType type,
bool desiredSize)
{
double count = 0;
bool assigned;
// Count how many segments are of the correct type. If we're measuring Star rows/cols
// we need to count the number of stars instead.
for (int i = start; i <= end; i++)
{
double segmentSize = desiredSize ? matrix[i, i].DesiredSize : matrix[i, i].OfferedSize;
if (segmentSize < matrix[i, i].Max)
{
count += type == GridUnitType.Star ? matrix[i, i].Stars : 1;
}
}
do
{
double contribution = size / count;
assigned = false;
for (int i = start; i <= end; i++)
{
double segmentSize = desiredSize ? matrix[i, i].DesiredSize : matrix[i, i].OfferedSize;
if (!(matrix[i, i].Type == type && segmentSize < matrix[i, i].Max))
{
continue;
}
double newsize = segmentSize;
newsize += contribution * (type == GridUnitType.Star ? matrix[i, i].Stars : 1);
double newSizeIgnoringMinMax = newsize;
newsize = Math.Min(newsize, matrix[i, i].Max);
newsize = Math.Max(newsize, matrix[i, i].Min);
assigned |= !Equals(newsize, newSizeIgnoringMinMax);
size -= newsize - segmentSize;
if (desiredSize)
{
matrix[i, i].DesiredSize = newsize;
}
else
{
matrix[i, i].OfferedSize = newsize;
}
}
}
while (assigned);
}
private void AllocateDesiredSize(int rowCount, int colCount)
{
// First allocate the heights of the RowDefinitions, then allocate
// the widths of the ColumnDefinitions.
for (int i = 0; i < 2; i++)
{
Segment[,] matrix = i == 0 ? _rowMatrix : _colMatrix;
int count = i == 0 ? rowCount : colCount;
for (int row = count - 1; row >= 0; row--)
{
for (int col = row; col >= 0; col--)
{
bool spansStar = false;
for (int j = row; j >= col; j--)
{
spansStar |= matrix[j, j].Type == GridUnitType.Star;
}
// This is the amount of pixels which must be available between the grid rows
// at index 'col' and 'row'. i.e. if 'row' == 0 and 'col' == 2, there must
// be at least 'matrix [row][col].size' pixels of height allocated between
// all the rows in the range col -> row.
double current = matrix[row, col].DesiredSize;
// Count how many pixels have already been allocated between the grid rows
// in the range col -> row. The amount of pixels allocated to each grid row/column
// is found on the diagonal of the matrix.
double totalAllocated = 0;
for (int k = row; k >= col; k--)
{
totalAllocated += matrix[k, k].DesiredSize;
}
// If the size requirement has not been met, allocate the additional required
// size between 'pixel' rows, then 'star' rows, finally 'auto' rows, until all
// height has been assigned.
if (totalAllocated < current)
{
double additional = current - totalAllocated;
if (spansStar)
{
AssignSize(matrix, col, row, ref additional, GridUnitType.Star, true);
}
else
{
AssignSize(matrix, col, row, ref additional, GridUnitType.Pixel, true);
AssignSize(matrix, col, row, ref additional, GridUnitType.Auto, true);
}
}
}
}
}
int rowMatrixDim = _rowMatrix.GetLength(0);
int colMatrixDim = _colMatrix.GetLength(0);
for (int r = 0; r < rowMatrixDim; r++)
{
_rowMatrix[r, r].OfferedSize = _rowMatrix[r, r].DesiredSize;
}
for (int c = 0; c < colMatrixDim; c++)
{
_colMatrix[c, c].OfferedSize = _colMatrix[c, c].DesiredSize;
}
}
private void SaveMeasureResults()
{
int rowMatrixDim = _rowMatrix.GetLength(0);
int colMatrixDim = _colMatrix.GetLength(0);
for (int i = 0; i < rowMatrixDim; i++)
{
for (int j = 0; j < rowMatrixDim; j++)
{
_rowMatrix[i, j].OriginalSize = _rowMatrix[i, j].OfferedSize;
}
}
for (int i = 0; i < colMatrixDim; i++)
{
for (int j = 0; j < colMatrixDim; j++)
{
_colMatrix[i, j].OriginalSize = _colMatrix[i, j].OfferedSize;
}
}
}
private void RestoreMeasureResults()
{
int rowMatrixDim = _rowMatrix.GetLength(0);
int colMatrixDim = _colMatrix.GetLength(0);
for (int i = 0; i < rowMatrixDim; i++)
{
for (int j = 0; j < rowMatrixDim; j++)
{
_rowMatrix[i, j].OfferedSize = _rowMatrix[i, j].OriginalSize;
}
}
for (int i = 0; i < colMatrixDim; i++)
{
for (int j = 0; j < colMatrixDim; j++)
{
_colMatrix[i, j].OfferedSize = _colMatrix[i, j].OriginalSize;
}
}
}
/// <summary>
/// Stores the layout values of of <see cref="RowDefinitions"/> of <see cref="ColumnDefinitions"/>.
/// </summary>
private struct Segment
{
/// <summary>
/// Gets or sets the base size of this segment.
/// The value is from the user's code or from the stored measuring values.
/// </summary>
public double OriginalSize;
/// <summary>
/// Gets the maximum size of this segment.
/// The value is from the user's code.
/// </summary>
public readonly double Max;
/// <summary>
/// Gets the minimum size of this segment.
/// The value is from the user's code.
/// </summary>
public readonly double Min;
/// <summary>
/// Gets or sets the row/column partial desired size of the <see cref="Grid"/>.
/// </summary>
public double DesiredSize;
/// <summary>
/// Gets or sets the row/column offered size that will be used to measure the children.
/// </summary>
public double OfferedSize;
/// <summary>
/// Gets or sets the star unit size if the <see cref="Type"/> is <see cref="GridUnitType.Star"/>.
/// </summary>
public double Stars;
/// <summary>
/// Gets the segment size unit type.
/// </summary>
public readonly GridUnitType Type;
public Segment(double offeredSize, double min, double max, GridUnitType type)
{
OriginalSize = 0;
Min = min;
Max = max;
DesiredSize = 0;
OfferedSize = offeredSize;
Stars = 0;
Type = type;
}
}
private struct GridNode
{
public readonly int Row;
public readonly int Column;
public readonly double Size;
public readonly Segment[,] Matrix;
public GridNode(Segment[,] matrix, int row, int col, double size)
{
Matrix = matrix;
Row = row;
Column = col;
Size = size;
}
}
private class GridWalker
{
public GridWalker(Grid grid, Segment[,] rowMatrix, Segment[,] colMatrix)
{
int rowMatrixDim = rowMatrix.GetLength(0);
int colMatrixDim = colMatrix.GetLength(0);
foreach (Control child in grid.Children)
{
bool starCol = false;
bool starRow = false;
bool autoCol = false;
bool autoRow = false;
int col = Math.Min(GetColumn(child), colMatrixDim - 1);
int row = Math.Min(GetRow(child), rowMatrixDim - 1);
int colspan = Math.Min(GetColumnSpan(child), colMatrixDim - 1);
int rowspan = Math.Min(GetRowSpan(child), rowMatrixDim - 1);
for (int r = row; r < row + rowspan; r++)
{
starRow |= rowMatrix[r, r].Type == GridUnitType.Star;
autoRow |= rowMatrix[r, r].Type == GridUnitType.Auto;
}
for (int c = col; c < col + colspan; c++)
{
starCol |= colMatrix[c, c].Type == GridUnitType.Star;
autoCol |= colMatrix[c, c].Type == GridUnitType.Auto;
}
HasAutoAuto |= autoRow && autoCol && !starRow && !starCol;
HasStarAuto |= starRow && autoCol;
HasAutoStar |= autoRow && starCol;
}
}
public bool HasAutoAuto { get; }
public bool HasStarAuto { get; }
public bool HasAutoStar { get; }
}
}
}
}

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

@ -20,7 +20,9 @@ namespace Avalonia.Controls.Utils
internal GridLayout([NotNull] ColumnDefinitions columns)
{
if (columns == null) throw new ArgumentNullException(nameof(columns));
_conventions = columns.Select(x => new LengthConvention(x.Width, x.MinWidth, x.MaxWidth)).ToList();
_conventions = columns.Count == 0
? new List<LengthConvention> { new LengthConvention() }
: columns.Select(x => new LengthConvention(x.Width, x.MinWidth, x.MaxWidth)).ToList();
}
/// <summary>
@ -30,7 +32,9 @@ namespace Avalonia.Controls.Utils
internal GridLayout([NotNull] RowDefinitions rows)
{
if (rows == null) throw new ArgumentNullException(nameof(rows));
_conventions = rows.Select(x => new LengthConvention(x.Height, x.MinHeight, x.MaxHeight)).ToList();
_conventions = rows.Count == 0
? new List<LengthConvention> { new LengthConvention() }
: rows.Select(x => new LengthConvention(x.Height, x.MinHeight, x.MaxHeight)).ToList();
}
/// <summary>
@ -49,7 +53,8 @@ namespace Avalonia.Controls.Utils
/// Gets all the length conventions that come from the grid children.
/// </summary>
[NotNull]
private readonly List<AdditionalLengthConvention> _additionalConventions = new List<AdditionalLengthConvention>();
private readonly List<AdditionalLengthConvention> _additionalConventions =
new List<AdditionalLengthConvention>();
/// <summary>
/// Some elements are not only in a single grid cell, they have one or more column/row spans,
@ -451,6 +456,7 @@ namespace Avalonia.Controls.Utils
{
return;
}
var measureLength = 0.0;
for (var i = 0; i < lengthList.Count; i++)
{
@ -474,6 +480,16 @@ namespace Avalonia.Controls.Utils
/// </summary>
internal class LengthConvention : ICloneable
{
/// <summary>
/// Initialize a new instance of <see cref="LengthConvention"/>.
/// </summary>
public LengthConvention()
{
Length = new GridLength(1.0, GridUnitType.Star);
MinLength = 0.0;
MaxLength = double.PositiveInfinity;
}
/// <summary>
/// Initialize a new instance of <see cref="LengthConvention"/>.
/// </summary>

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

@ -16,6 +16,7 @@ namespace Avalonia.Controls.UnitTests
internal static Grid New(Size measure = default, Size arrange = default)
{
var grid = new Grid();
grid.Children.Add(new Border());
grid.Measure(measure == default ? new Size(double.PositiveInfinity, double.PositiveInfinity) : measure);
grid.Arrange(new Rect(default, arrange == default ? grid.DesiredSize : arrange));
return grid;

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

@ -1,13 +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 System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using Avalonia.Controls;
using Xunit;
namespace Avalonia.Controls.UnitTests
@ -72,26 +65,26 @@ namespace Avalonia.Controls.UnitTests
}
[Fact]
public void Layout_PixelRowColumn_BoundsCorrect()
public void Layout_EmptyColumnRow_LayoutLikeANormalPanel()
{
// Arrange & Action
var rowGrid = GridMock.New(new RowDefinitions("100,200,300"));
var columnGrid = GridMock.New(new ColumnDefinitions("50,100,150"));
var grid = GridMock.New(arrange: new Size(600, 200));
// Assert
GridAssert.ChildrenHeight(rowGrid, 100, 200, 300);
GridAssert.ChildrenWidth(columnGrid, 50, 100, 150);
GridAssert.ChildrenWidth(grid, 600);
GridAssert.ChildrenHeight(grid, 200);
}
[Fact]
public void Layout_NoRowColumn_BoundsCorrect()
public void Layout_PixelRowColumn_BoundsCorrect()
{
// Arrange & Action
var grid = GridMock.New(arrange: new Size(600, 200));
var rowGrid = GridMock.New(new RowDefinitions("100,200,300"));
var columnGrid = GridMock.New(new ColumnDefinitions("50,100,150"));
// Assert
GridAssert.ChildrenHeight(grid, 600);
GridAssert.ChildrenWidth(grid, 200);
GridAssert.ChildrenHeight(rowGrid, 100, 200, 300);
GridAssert.ChildrenWidth(columnGrid, 50, 100, 150);
}
[Fact]

Loading…
Cancel
Save