不允许无约束的尺寸!
有时您希望在屏幕上看到所有内容,可能是一系列大小统一的行和列。您可以使用带有星号定义的所有行和列定义的Grid执行类似的操作,以使它们具有相同的大小。唯一的问题是您可能还希望行数和列数基于子节点数,并针对屏幕空间的最佳使用进行了优化。
让我们编写一个名为UniformGridLayout的自定义布局。与WrapLayout一样,UniformGridLayout需要Orientation,RowSpacing和ColumnSpacing属性,所以让我们通过从WrapLayout派生UniformGridLayout来消除重新定义属性所涉及的一些工作。
因为UniformGridLayout对无约束维度没有意义,所以OnSizeRequest覆盖检查无限约束并在遇到这样的事情时引发异常。
为了帮助UniformGridLayout优化屏幕空间的使用,我们给它一个名为AspectRatio的属性AspectRatio。此属性指示子项的预期宽高比为double值。例如,值1.33表示纵横比为4:3,宽度比高度长33%。但是,默认情况下,UniformGridLayout将计算其子项的平均宽高比。
此AspectRatio结构类似于为Grid类定义的GridLength结构,因为它允许双值和“自动”选项强制UniformGridLayout计算平均宽高比:
namespace Xamarin.FormsBook.Toolkit
{
[TypeConverter(typeof(AspectRatioTypeConverter))]
public struct AspectRatio
{
public AspectRatio(double value)
{
if (value < 0)
throw new FormatException("AspectRatio value must be greater than 0, " +
"or set to 0 to indicate Auto");
Value = value;
}
public static AspectRatio Auto
{
get
{
return new AspectRatio();
}
}
public double Value { private set; get; }
public bool IsAuto { get { return Value == 0; } }
public override string ToString()
{
return Value == 0 ? "Auto" : Value.ToString();
}
}
}
“自动”选项由Value属性0指示。使用UniformGridLayout的应用程序可以使用无参数构造函数创建此类AspectRatio值,或者将0传递给定义的构造函数,或者使用静态Auto属性。
我确定您希望能够在XAML中设置AspectRatio属性,因此结构将使用TypeConverter属性进行标记。 AspectRatioTypeConverter类可以处理带有“Auto”或double的字符串:
namespace Xamarin.FormsBook.Toolkit
{
public class AspectRatioTypeConverter : TypeConverter
{
public override bool CanConvertFrom(Type sourceType)
{
return sourceType == typeof(string);
}
public override object ConvertFrom(CultureInfo culture, object value)
{
string str = value as string;
if (String.IsNullOrWhiteSpace(str))
return null;
str = str.Trim();
double aspectValue;
if (String.Compare(str, "auto", StringComparison.OrdinalIgnoreCase) == 0)
return AspectRatio.Auto;
if (Double.TryParse(str, NumberStyles.Number,
CultureInfo.InvariantCulture, out aspectValue))
return new AspectRatio(aspectValue);
throw new FormatException("AspectRatio must be Auto or numeric");
}
}
}
UniformGridLayout类派生自WrapLayout,仅用于继承WrapLayout定义的三个可绑定属性。 对于这些属性,UniformGridLayout添加了AspectRatio属性:
namespace Xamarin.FormsBook.Toolkit
{
public class UniformGridLayout : WrapLayout
{
public static readonly BindableProperty AspectRatioProperty =
BindableProperty.Create(
"AspectRatio",
typeof(AspectRatio),
typeof(UniformGridLayout),
AspectRatio.Auto,
propertyChanged: (bindable, oldvalue, newvalue) =>
{
((UniformGridLayout)bindable).InvalidateLayout();
});
public AspectRatio AspectRatio
{
set { SetValue(AspectRatioProperty, value); }
get { return (AspectRatio)GetValue(AspectRatioProperty); }
}
__
}
}
OnSizeRequest覆盖首先检查约束是否是无限的,如果是这种情况则引发异常。 否则,它会请求整个区域,除非它没有可见的孩子:
namespace Xamarin.FormsBook.Toolkit
{
public class UniformGridLayout : WrapLayout
{
__
protected override SizeRequest OnSizeRequest(double widthConstraint,
double heightConstraint)
{
if (Double.IsInfinity(widthConstraint) || Double.IsInfinity(heightConstraint))
throw new InvalidOperationException(
"UniformGridLayout cannot be used with unconstrained dimensions.");
// Just check to see if there aren't any visible children.
int childCount = 0;
foreach (View view in Children)
childCount += view.IsVisible ? 1 : 0;
if (childCount == 0)
return new SizeRequest();
// Then request the entire (noninfinite) size.
return new SizeRequest(new Size(widthConstraint, heightConstraint));
}
__
}
}
困难的部分是LayoutChildren覆盖,它有三个主要部分:
namespace Xamarin.FormsBook.Toolkit
{
public class UniformGridLayout : WrapLayout
{
__
protected override void LayoutChildren(double x, double y, double width, double height)
{
int childCount = 0;
foreach (View view in Children)
childCount += view.IsVisible ? 1 : 0;
if (childCount == 0)
return;
double childAspect = AspectRatio.Value;
// If AspectRatio is Auto, calculate an average aspect ratio
if (AspectRatio.IsAuto)
{
int nonZeroChildCount = 0;
double accumAspectRatio = 0;
foreach (View view in Children)
{
if (view.IsVisible)
{
SizeRequest sizeRequest = view.GetSizeRequest(Double.PositiveInfinity,
Double.PositiveInfinity);
if (sizeRequest.Request.Width > 0 && sizeRequest.Request.Height > 0)
{
nonZeroChildCount++;
accumAspectRatio += sizeRequest.Request.Width /
sizeRequest.Request.Height;
}
}
}
if (nonZeroChildCount > 0)
{
childAspect = accumAspectRatio / nonZeroChildCount;
}
else
{
childAspect = 1;
}
}
int bestRowsCount = 0;
int bestColsCount = 0;
double bestUsage = 0;
double bestChildWidth = 0;
double bestChildHeight = 0;
// Test various possibilities of the number of columns.
for (int colsCount = 1; colsCount <= childCount; colsCount++)
{
// Find the number of rows for that many columns.
int rowsCount = (int)Math.Ceiling((double)childCount / colsCount);
// Determine if we have more rows or columns than we need.
if ((rowsCount - 1) * colsCount >= childCount ||
rowsCount * (colsCount - 1) >= childCount)
{
continue;
}
// Get the aspect ratio of the resultant cells.
double cellWidth = (width - ColumnSpacing * (colsCount - 1)) / colsCount;
double cellHeight = (height - RowSpacing * (rowsCount - 1)) / rowsCount;
double cellAspect = cellWidth / cellHeight;
double usage = 0;
// Compare with the average aspect ratio of the child.
if (cellAspect > childAspect)
{
usage = childAspect / cellAspect;
}
else
{
usage = cellAspect / childAspect;
}
// If we're using more space, save the numbers.
if (usage > bestUsage)
{
bestRowsCount = rowsCount;
bestColsCount = colsCount;
bestUsage = usage;
bestChildWidth = cellWidth;
bestChildHeight = cellHeight;
}
}
int colIndex = 0;
int rowIndex = 0;
double xChild = x;
double yChild = y;
foreach (View view in Children)
{
// Position and size the child.
LayoutChildIntoBoundingRegion(view,
new Rectangle(xChild, yChild, bestChildWidth, bestChildHeight));
// Increment the coordinates and indices.
if (Orientation == WrapOrientation.HorizontalThenVertical)
{
xChild += bestChildWidth + ColumnSpacing;
if (++colIndex == bestColsCount)
{
colIndex = 0;
xChild = x;
yChild += bestChildHeight + RowSpacing;
}
}
else // Orientation == WrapOrientation.VerticalThenHorizontal
{
yChild += bestChildHeight + RowSpacing;
if (++rowIndex == bestRowsCount)
{
rowIndex = 0;
xChild += bestChildWidth + ColumnSpacing;
yChild = y;
}
}
}
}
}
}
如果已指定“自动”选项,则第一部分计算子项的平均纵横比。
第二部分循环遍历行和列的不同组合,并确定哪种组合可以最好地利用可用空间。 关键的计算是这样的:
if (cellAspect > childAspect)
{
usage = childAspect / cellAspect;
}
else
{
usage = cellAspect / childAspect;
}
例如,假设基于所有子项的平均值计算的childAspect为1.5,对于行和列的特定组合,cellAspect值为2.纵横比为1.5的子项将仅占用75% 一个纵横比为2的单元格。如果cellAspect是0.75,那么该子单元将只占该单元格的50%。
然后,第三部分为每个孩子提供网格中的大小和位置。 这需要基于Orientation属性的不同处理。
我们来试试吧。 PhotoGrid XAML文件使用UniformGridLayout填充页面(iPhone上的顶部填充除外),其中设置了两个属性:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:toolkit=
"clr-namespace:Xamarin.FormsBook.Toolkit;assembly=Xamarin.FormsBook.Toolkit"
x:Class="PhotoGrid.PhotoGridPage">
<ContentPage.Padding>
<OnPlatform x:TypeArguments="Thickness"
iOS="0, 20, 0, 0" />
</ContentPage.Padding>
<toolkit:UniformGridLayout x:Name="uniformGridLayout"
Orientation="VerticalThenHorizontal"
AspectRatio="Auto" />
</ContentPage>
代码隐藏文件几乎与PhotoWrap中的相同,结果如下:
同样,当Image元素加载位图时,您可能会看到行和列的某些移位。
在Windows桌面上运行它并更改窗口的大小和宽高比以查看位图如何重新排序为行和列是很有趣的。 这也是检查代码中的一些错误的好方法。