编码的一些规则
从上面的讨论中,您可以为自己的Layout 衍生物制定几个规则:
规则1:如果布局类定义了诸如间距或方向等属性,则这些属性应由可绑定属性支持。 在大多数情况下,这些可绑定属性的属性更改处理程序应调用InvalidateLayout。 调用InvalidateMeasure应该仅限于属性更改仅影响布局大小的情况,而不是它如何安排其子级,但现实生活中的例子很难想象。
规则2:您的布局类可以为其子级定义附加的可绑定属性,类似于Grid定义的Row,Column,RowSpan和ColumnSpan属性。 如您所知,这些属性由布局类定义,但它们旨在设置在布局的子级上。 在这种情况下,您的布局类应覆盖OnAdded方法,以向布局的每个子项添加PropertyChanged处理程序,并重写OnRemoved以删除该处理程序。 PropertyChanged处理程序应检查子项上正在更改的属性是否是您的类已定义的附加可绑定属性之一,如果是,您的布局通常应通过调用InvalidateLayout来响应。
规则3:如果要实现缓存(或保留其他信息)以尽量减少对布局子项的GetSizeRequest方法的重复处理,那么您还应该覆盖InvalidateLayout方法,以便在添加或删除子项时通知 布局和OnChildMeasureInvalidated方法,当其中一个布局的子项更改大小时,将通知该方法。 在这两种情况下,布局类都应通过清除缓存或丢弃保留的信息来响应。
当布局调用其InvalidateMeasure方法时,布局也可以清除缓存或丢弃保留的信息。 但是,通常缓存是基于传递给OnSizeRequest和LayoutChildren覆盖的大小的字典,因此这些大小无论如何都是不同的。
所有这些技术将在前面的页面中进行演示。
具有属性的布局
StackLayout当然很方便,但它只是一行或一列孩子。如果您想要多个行和列,可以使用Grid,但应用程序必须显式设置行数和列数,这需要很好地了解子项的大小。
容纳无限数量的子节点的更有用的布局将开始将子节点排成一排,就像水平StackLayout一样,但是如果需要则转到第二行,并且到第三行,继续,但是需要很多行。如果预计行数超过屏幕高度,则可以将布局设置为ScrollView的子级。
这就是WrapLayout背后的想法。它将其子项水平排列在屏幕上,直到它到达边缘,此时它将后续子项的显示包装到下一行,依此类推。
但是让它变得更加通用:让我们给它一个像StackLayout这样的Orientation属性。这允许使用WrapLayout的程序指定它通过在屏幕上按行排列其子项开始,然后应该在必要时转到第二列。使用此替代方向,WrapLayout可以水平滚动。
让我们给WrapLayout两个属性,名为ColumnSpacing和RowSpacing,就像Grid一样。
如果它真的允许各种不同大小的孩子,WrapLayout有可能在算法上相当复杂。第一行可能有四个子节点,第二行可能有三个子节点,依此类推。
根据子项的最大大小为每个子项分配相同的空间量。 这有时称为单元格大小,WrapLayout将为每个孩子计算足够大的单元格大小。 小于单元格大小的子项可以根据其HorizontalOptions和VerticalOptions设置放置在该单元格内。
WrapLayout足以证明它包含在Xamarin.FormsBook.Toolkit库中。 以下枚举包含两个方向选项,其中包含冗长但明确的描述:
namespace Xamarin.FormsBook.Toolkit
{
public enum WrapOrientation
{
HorizontalThenVertical,
VerticalThenHorizontal
}
}
WrapLayout定义了三个由可绑定属性支持的属性。 每个可绑定属性的属性更改处理程序只是调用InvalidateLayout来触发布局上的新布局传递:
namespace Xamarin.FormsBook.Toolkit
{
public class WrapLayout : Layout<View>
{
__
public static readonly BindableProperty OrientationProperty =
BindableProperty.Create(
"Orientation",
typeof(WrapOrientation),
typeof(WrapLayout),
WrapOrientation.HorizontalThenVertical,
propertyChanged: (bindable, oldValue, newValue) =>
{
((WrapLayout)bindable).InvalidateLayout();
});
public static readonly BindableProperty ColumnSpacingProperty =
BindableProperty.Create(
"ColumnSpacing",
typeof(double),
typeof(WrapLayout),
6.0,
propertyChanged: (bindable, oldvalue, newvalue) =>
{
((WrapLayout)bindable).InvalidateLayout();
});
public static readonly BindableProperty RowSpacingProperty =
BindableProperty.Create(
"RowSpacing",
typeof(double),
typeof(WrapLayout),
6.0,
propertyChanged: (bindable, oldvalue, newvalue) =>
{
((WrapLayout)bindable).InvalidateLayout();
});
public WrapOrientation Orientation
{
set { SetValue(OrientationProperty, value); }
get { return (WrapOrientation)GetValue(OrientationProperty); }
}
public double ColumnSpacing
{
set { SetValue(ColumnSpacingProperty, value); }
get { return (double)GetValue(ColumnSpacingProperty); }
}
public double RowSpacing
{
set { SetValue(RowSpacingProperty, value); }
get { return (double)GetValue(RowSpacingProperty); }
}
__
}
}
WrapLayout还定义了一个私有结构,用于存储有关特定子集合的信息。 CellSize属性是所有子项的最大大小,但已调整为布局的大小。 Rows和Cols属性是行数和列数。
namespace Xamarin.FormsBook.Toolkit
{
public class WrapLayout : Layout<View>
{
struct LayoutInfo
{
public LayoutInfo(int visibleChildCount, Size cellSize, int rows, int cols) : this()
{
VisibleChildCount = visibleChildCount;
CellSize = cellSize;
Rows = rows;
Cols = cols;
}
public int VisibleChildCount { private set; get; }
public Size CellSize { private set; get; }
public int Rows { private set; get; }
public int Cols { private set; get; }
}
Dictionary<Size, LayoutInfo> layoutInfoCache = new Dictionary<Size, LayoutInfo>();
__
}
}
另请注意,Dictionary的定义用于存储多个LayoutInfo值。 Size键是OnSizeRequest覆盖的约束参数,或LayoutChildren覆盖的width和height参数。
如果WrapLayout在受约束的ScrollView中(通常就是这种情况),那么其中一个约束参数将是无限的,但对于LayoutChildren的width和height参数则不是这种情况。在这种情况下,将有两个字典条目。
如果你然后侧身转动手机,WrapLayout将获得另一个具有无限约束的OnSizeRequest调用,以及另一个LayoutChildren调用。那是另外两个字典条目。但是,如果您将手机恢复为纵向模式,则无需进一步计算,因为缓存已经具有该情况。
下面是WrapLayout中的GetLayoutInfo方法,它根据特定大小计算LayoutInfo结构的属性。请注意,该方法首先检查计算的LayoutInfo值是否已在缓存中可用。在GetLayoutInfo方法的末尾,新的LayoutInfo值存储在缓存中:
namespace Xamarin.FormsBook.Toolkit
{
public class WrapLayout : Layout<View>
{
__
LayoutInfo GetLayoutInfo(double width, double height)
{
Size size = new Size(width, height);
// Check if cached information is available.
if (layoutInfoCache.ContainsKey(size))
{
return layoutInfoCache[size];
}
int visibleChildCount = 0;
Size maxChildSize = new Size();
int rows = 0;
int cols = 0;
LayoutInfo layoutInfo = new LayoutInfo();
// Enumerate through all the children.
foreach (View child in Children)
{
// Skip invisible children.
if (!child.IsVisible)
continue;
// Count the visible children.
visibleChildCount++;
// Get the child's requested size.
SizeRequest childSizeRequest = child.GetSizeRequest(Double.PositiveInfinity,
Double.PositiveInfinity);
// Accumulate the maximum child size.
maxChildSize.Width =
Math.Max(maxChildSize.Width, childSizeRequest.Request.Width);
maxChildSize.Height =
Math.Max(maxChildSize.Height, childSizeRequest.Request.Height);
}
if (visibleChildCount != 0)
{
// Calculate the number of rows and columns.
if (Orientation == WrapOrientation.HorizontalThenVertical)
{
if (Double.IsPositiveInfinity(width))
{
cols = visibleChildCount;
rows = 1;
}
else
{
cols = (int)((width + ColumnSpacing) /
(maxChildSize.Width + ColumnSpacing));
cols = Math.Max(1, cols);
rows = (visibleChildCount + cols - 1) / cols;
}
}
else // WrapOrientation.VerticalThenHorizontal
{
if (Double.IsPositiveInfinity(height))
{
rows = visibleChildCount;
cols = 1;
}
else
{
rows = (int)((height + RowSpacing) /
(maxChildSize.Height + RowSpacing));
rows = Math.Max(1, rows);
cols = (visibleChildCount + rows - 1) / rows;
}
}
// Now maximize the cell size based on the layout size.
Size cellSize = new Size();
if (Double.IsPositiveInfinity(width))
{
cellSize.Width = maxChildSize.Width;
}
else
{
cellSize.Width = (width - ColumnSpacing * (cols - 1)) / cols;
}
if (Double.IsPositiveInfinity(height))
{
cellSize.Height = maxChildSize.Height;
}
else
{
cellSize.Height = (height - RowSpacing * (rows - 1)) / rows;
}
layoutInfo = new LayoutInfo(visibleChildCount, cellSize, rows, cols);
}
layoutInfoCache.Add(size, layoutInfo);
return layoutInfo;
}
__
}
}
GetLayoutInfo的逻辑分为三个主要部分:
第一部分是一个foreach循环,它枚举所有子节点,调用具有无限宽度和高度的GetSizeRequest,并确定最大子节点大小。
仅当存在至少一个可见子项时才执行第二和第三部分。第二部分基于Orientation属性进行不同的处理,并计算行数和列数。通常情况下,具有默认Orientation设置(HorizontalThenVertical)的WrapPanel将是垂直ScrollView的子项,在这种情况下,OnSizeRequest覆盖的heightConstraint参数将是无限的。也可能是OnSizeRequest(和GetLayoutInfo)的widthConstraint参数也是无限的,这导致所有子节点都显示在一行中。但这不寻常。
然后,第三部分根据WrapLayout的尺寸计算子项的单元格大小。对于HorizontalThenVertical的方向,此单元格大小通常比最大子大小略宽,但如果WrapLayout对于最宽的孩子不够宽或者对于最高的孩子而言足够高则可能更小。
当布局接收到对InvalidateLayout的调用时(当将子集添加到集合或从集合中删除时,或者当WrapLayout的某个属性更改值时)或OnChildMeasureInvalidated时,必须完全销毁缓存。这只是清除字典的问题:
namespace Xamarin.FormsBook.Toolkit
{
public class WrapLayout : Layout<View>
{
__
protected override void InvalidateLayout()
{
base.InvalidateLayout();
// Discard all layout information for children added or removed.
layoutInfoCache.Clear();
}
protected override void OnChildMeasureInvalidated()
{
base.OnChildMeasureInvalidated();
// Discard all layout information for child size changed.
layoutInfoCache.Clear();
}
}
}
最后,我们准备查看两种必需的方法。 OnSizeRequest覆盖只调用GetLayoutInfo并从返回的信息以及RowSpacing和ColumnSpacing属性构造SizeRequest值:
namespace Xamarin.FormsBook.Toolkit
{
public class WrapLayout : Layout<View>
{
__
protected override SizeRequest OnSizeRequest(double widthConstraint,
double heightConstraint)
{
LayoutInfo layoutInfo = GetLayoutInfo(widthConstraint, heightConstraint);
if (layoutInfo.VisibleChildCount == 0)
{
return new SizeRequest();
}
Size totalSize = new Size(layoutInfo.CellSize.Width * layoutInfo.Cols +
ColumnSpacing * (layoutInfo.Cols - 1),
layoutInfo.CellSize.Height * layoutInfo.Rows +
RowSpacing * (layoutInfo.Rows - 1));
return new SizeRequest(totalSize);
}
__
}
}
LayoutChildren覆盖以对GetLayoutInfo的调用开始,然后枚举所有要调整大小的子节点并将它们放置在每个子节点的单元格中。 此逻辑还需要基于Orientation属性进行单独处理:
namespace Xamarin.FormsBook.Toolkit
{
public class WrapLayout : Layout<View>
{
__
protected override void LayoutChildren(double x, double y, double width, double height)
{
LayoutInfo layoutInfo = GetLayoutInfo(width, height);
if (layoutInfo.VisibleChildCount == 0)
return;
double xChild = x;
double yChild = y;
int row = 0;
int col = 0;
foreach (View child in Children)
{
if (!child.IsVisible)
continue;
LayoutChildIntoBoundingRegion(child,
new Rectangle(new Point(xChild, yChild), layoutInfo.CellSize));
if (Orientation == WrapOrientation.HorizontalThenVertical)
{
if (++col == layoutInfo.Cols)
{
col = 0;
row++;
xChild = x;
yChild += RowSpacing + layoutInfo.CellSize.Height;
}
else
{
xChild += ColumnSpacing + layoutInfo.CellSize.Width;
}
}
else // Orientation == WrapOrientation.VerticalThenHorizontal
{
if (++row == layoutInfo.Rows)
{
col++;
row = 0;
xChild += ColumnSpacing + layoutInfo.CellSize.Width;
yChild = y;
}
else
{
yChild += RowSpacing + layoutInfo.CellSize.Height;
}
}
}
}
__
}
}
我们来试试吧! PhotoWrap程序的XAML文件只包含一个在ScrollView中具有默认属性设置的WrapPanel:
<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="PhotoWrap.PhotoWrapPage">
<ContentPage.Padding>
<OnPlatform x:TypeArguments="Thickness"
iOS="0, 20, 0, 0" />
</ContentPage.Padding>
<ScrollView>
<toolkit:WrapLayout x:Name="wrapLayout" />
</ScrollView>
</ContentPage>
代码隐藏文件访问JSON文件,该文件包含以前在本书的几个示例程序中使用的库存照片列表。 构造函数为列表中的每个位图创建一个Image元素,并将其添加到WrapLayout:
public partial class PhotoWrapPage : ContentPage
{
[DataContract]
class ImageList
{
[DataMember(Name = "photos")]
public List<string> Photos = null;
}
WebRequest request;
static readonly int imageDimension = Device.OnPlatform(240, 240, 120);
static readonly string urlSuffix =
String.Format("?width={0}&height={0}&mode=max", imageDimension);
public PhotoWrapPage()
{
InitializeComponent();
// Get list of stock photos.
Uri uri = new Uri("http://docs.xamarin.com/demo/stock.json");
request = WebRequest.Create(uri);
request.BeginGetResponse(WebRequestCallback, null);
}
void WebRequestCallback(IAsyncResult result)
{
try
{
Stream stream = request.EndGetResponse(result).GetResponseStream();
// Deserialize the JSON into imageList.
var jsonSerializer = new DataContractJsonSerializer(typeof(ImageList));
ImageList imageList = (ImageList)jsonSerializer.ReadObject(stream);
Device.BeginInvokeOnMainThread(() =>
{
foreach (string filepath in imageList.Photos)
{
Image image = new Image
{
Source = ImageSource.FromUri(new Uri(filepath + urlSuffix))
};
wrapLayout.Children.Add(image);
}
});
}
catch (Exception)
{
}
}
}
每行中的列数取决于位图的大小,屏幕宽度以及每个与设备无关的单位的像素数:
将手机侧身转动,你会看到一些不同的东西:
ScrollView允许布局垂直滚动。 如果要检查WrapPanel的不同方向,则还需要更改ScrollView的方向:
<ScrollView Orientation="Horizontal">
<toolkit:WrapLayout x:Name="wrapLayout"
Orientation="VerticalThenHorizontal" />
</ScrollView>
现在屏幕水平滚动:
Image元素在后台加载位图,因此WrapLayout类将获得对其Layout方法的大量调用,因为每个Image元素都会根据加载的位图获得新的大小。 因此,在加载位图时,您可能会看到行和列的某些移位。