让我们知道你对此的感受
最后更新:4个月前
Xamarin.Forms定义了四个布局类 - StackLayout,AbsoluteLayout,RelativeLayout和Grid,并且每个布局类别都以不同的方式排列它们的子类。但是,有时需要使用Xamarin.Forms不提供的布局来组织页面内容。本文介绍了如何编写自定义布局类,并演示了一种方向敏感的WrapLayout类,它将子级别横跨页面排列,然后将后续子项的显示包装到其他行。
在Xamarin.Forms中,所有布局类都从类派生,Layout
并将泛型类型限制View
为其派生类型。反过来,这个Layout
阶级从课堂中得到Layout
,它提供了定位和调整儿童元素大小的机制。
每个视觉元素负责确定自己的首选大小,这被称为请求的大小。Page
,Layout
和Layout
衍生类型负责确定他们的孩子或孩子相对于自己的位置和大小。因此,布局涉及父子关系,其中父级确定其子级的大小应该是多少,但会尝试容纳所请求的小孩大小。
创建自定义布局需要彻底了解Xamarin.Forms布局和无效循环。现在将讨论这些周期。
布局从页面的视觉树顶部开始,并通过视觉树的所有分支进行,以包含页面上的每个视觉元素。父母对其他因素的要素负责相对于自己的尺寸和定位他们的孩子。
的VisualElement
类定义了一个Measure
测量用于布局操作的元素,和一种方法Layout
,用于指定矩形区域的元件将在被渲染方法。当应用程序启动并显示第一个页面时,首先调用一个布局循环Measure
,然后Layout
调用,从该Page
对象开始:
Measure
其子对象的方法。Layout
孩子的方法。 这个循环保证了每个页面上的视觉元素接收到来电Measure
和Layout
方法。该过程如下图所示:
请注意,如果某些更改影响布局,布局周期也可能发生在视觉树的子集上。这包括被添加或删除的项目从集合诸如在
StackLayout
,在一个变化IsVisible
的元件,或在元件的尺寸变化的属性。
每个具有一个Content
或一个Children
属性的Xamarin.Forms类具有可覆盖的LayoutChildren
方法。派生的自定义布局类Layout
必须覆盖此方法,并确保在所有元素的子项上调用Measure
和Layout
方法,以便提供所需的自定义布局。
另外,派生Layout
或者Layout
必须覆盖OnMeasure
方法的每个类,这是布局类决定通过调用Measure
其子对象的方式所需的大小。
元素基于约束来确定它们的大小,这些约束指示元素的父元素内的元素有多少空间。传递给约束
Measure
和OnMeasure
方法的范围可从0到Double.PositiveInfinity
。当元素被接收到具有非无限参数的方法的调用时,元素被约束或完全约束Measure
- 元素被约束到特定的大小。当一个元素接收到对其方法的调用时,它至少有一个参数等于- 无限约束可以被认为是表示自动调整,这个元素是无约束的或部分受限的。Measure
Double.PositiveInfinity
无效是页面上的元素更改触发新的布局循环的过程。当元素不再具有正确的大小或位置时,元素被认为是无效的。例如,如果FontSize
某个属性Button
发生更改,则说明该属性Button
将不再具有正确的大小。调整大小Button
可能会通过页面的其余部分产生布局变化的波纹效应。
元素通过调用该InvalidateMeasure
方法无效,通常当元素的属性更改时可能会导致元素的新大小。该方法触发MeasureInvalidated
事件,元素的父处理器触发新的布局循环。
将Layout
类设置为一个处理器MeasureInvalidated
上添加到它的每一个孩子的事件Content
属性或Children
集合,当孩子被删除分离的处理程序。因此,每当其中一个子节点改变大小时,每个可视树中的具有子节点的元素就会被提醒。下图说明了视觉树中元素的大小如何改变可能会导致纹理变化的变化:
但是,Layout
该类试图限制一个孩子大小的变化对页面布局的影响。如果布局大小受限,则子视图树中的父布局不会影响任何高度的子尺寸更改。但是,通常情况下,布局大小的变化会影响布局如何布置其子项。因此,布局大小的任何更改都将为布局开始布局循环,布局将接收对其OnMeasure
和LayoutChildren
方法的调用。
的Layout
类也定义一个InvalidateLayout
具有类似目的的方法InvalidateMeasure
的方法。InvalidateLayout
每当进行更改时,都应调用该方法,影响其子项的布局位置和大小。例如,每当将子添加到布局或从布局中移除时,Layout
该类将调用该InvalidateLayout
方法。
将InvalidateLayout
可以覆盖来实现高速缓存,以尽量减少重复调用Measure
布局的孩子的方法。覆盖该InvalidateLayout
方法将提供一个关于什么时候将子添加到布局或从布局中移除的通知。类似地,OnChildMeasureInvalidated
当其中一个布局的子代改变大小时,该方法可被覆盖以提供通知。对于这两种方法覆盖,自定义布局应通过清除缓存来进行响应。有关更多信息,请参阅计算和缓存数据。
创建自定义布局的过程如下:
Layout
类的类。有关详细信息,请参阅创建WrapLayout。OnMeasure
调用Measure
方法的方法,并返回所需的布局大小。有关更多信息,请参阅覆盖OnMeasure方法。 覆盖在所有布局的子项上LayoutChildren
调用Layout
方法的方法。未能Layout
在布局中调用每个孩子的方法将导致孩子从未收到正确的大小或位置,因此该小孩将不会在页面上显示。有关更多信息,请参阅覆盖LayoutChildren方法。
当枚举子进程
OnMeasure
并LayoutChildren
覆盖时,请跳过其IsVisible
属性设置为的任何子级别false
。这将确保自定义布局不会为不可见的孩子留出空间。
[ 可选 ]覆盖将InvalidateLayout
子添加到布局或从布局中移除时通知的方法。有关更多信息,请参阅覆盖InvalidateLayout方法。
OnChildMeasureInvalidated
当其中一个布局的子节点更改大小时要通知的方法。有关更多信息,请参阅覆盖OnChildMeasureInvalidated方法。请注意,
OnMeasure
如果布局的大小由其父级而不是其子级控制,则不会调用override。但是,如果一个或两个约束是无限的,或者如果布局类具有非默认值HorizontalOptions
或VerticalOptions
属性值,则将调用override 。因此,LayoutChildren
覆盖不能依赖于OnMeasure
方法调用期间获得的子大小。而是在调用该方法之前LayoutChildren
必须调用Measure
布局的子节点上的Layout
方法。或者,OnMeasure
可以缓存在覆盖中获取的子项的大小,以避免Measure
在LayoutChildren
覆盖中稍后调用,但布局类将需要知道何时需要再次获取大小。有关详细信息,请参阅计算和缓存布局数据。
然后可以通过将布局类添加到一个Page
,并通过将子项添加到布局来消费。有关更多信息,请参阅使用WrapLayout。
示例应用程序演示了一个方向敏感WrapLayout
类,将其子级别横跨页面排列,然后将后续子项的显示包装到其他行。
根据WrapLayout
孩子的最大大小,该类为每个孩子分配相同数量的空间,称为单元格大小。小于细胞大小的小孩可以根据其细胞的数量HorizontalOptions
和VerticalOptions
属性值定位在细胞内。
所述WrapLayout
类的定义示于下面的代码示例:
public class WrapLayout : Layout<View>
{
Dictionary<Size, LayoutData> layoutDataCache = new Dictionary<Size, LayoutData>();
...
}
该LayoutData
结构存储有关的一些属性的子项的集合数据:
VisibleChildCount
- 在布局中可见的子项数。CellSize
- 所有孩子的最大大小,调整到布局的大小。Rows
- 行数。Columns
- 列数。 该layoutDataCache
字段用于存储多个LayoutData
值。当应用程序启动时,两个LayoutData
对象将被缓存到layoutDataCache
字典中用于当前方向 - 一个用于OnMeasure
覆盖的约束参数,另一个用于覆盖的参数width
和height
参数LayoutChildren
。当将设备旋转为横向时,将再次调用OnMeasure
覆盖和LayoutChildren
覆盖,这将导致另外两个LayoutData
对象被缓存到字典中。然而,当将设备返回到纵向方向时,不需要进一步的计算,因为layoutDataCache
已经具有所需的数据。
以下代码示例显示了基于特定大小GetLayoutData
计算LayoutData
结构化属性的方法:
LayoutData GetLayoutData(double width, double height)
{
Size size = new Size(width, height);
// Check if cached information is available.
if (layoutDataCache.ContainsKey(size))
{
return layoutDataCache[size];
}
int visibleChildCount = 0;
Size maxChildSize = new Size();
int rows = 0;
int columns = 0;
LayoutData layoutData = new LayoutData();
// 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.Measure(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 (Double.IsPositiveInfinity(width))
{
columns = visibleChildCount;
rows = 1;
}
else
{
columns = (int)((width + ColumnSpacing) / (maxChildSize.Width + ColumnSpacing));
columns = Math.Max(1, columns);
rows = (visibleChildCount + columns - 1) / columns;
}
// 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 * (columns - 1)) / columns;
if (Double.IsPositiveInfinity(height))
cellSize.Height = maxChildSize.Height;
else
cellSize.Height = (height - RowSpacing * (rows - 1)) / rows;
layoutData = new LayoutData(visibleChildCount, cellSize, rows, columns);
}
layoutDataCache.Add(size, layoutData);
return layoutData;
}
该GetLayoutData
方法执行以下操作:
LayoutData
值是否已经在缓存中,并在可用时返回它。Measure
每个孩子的方法无限宽和高,并确定最大的子大小。WrapLayout
。请注意,单元格大小通常比最大小孩大小略宽,但如果WrapLayout
最宽的小孩不够宽,或者最高的孩子足够高,那么它也可以更小。LayoutData
值存储在缓存中。
在WrapLayout
类定义ColumnSpacing
和RowSpacing
属性,其值用于在布局中的行和列中分离,并且通过绑定属性被备份。可绑定属性显示在以下代码示例中:
public static readonly BindableProperty ColumnSpacingProperty = BindableProperty.Create(
"ColumnSpacing",
typeof(double),
typeof(WrapLayout),
5.0,
propertyChanged: (bindable, oldvalue, newvalue) =>
{
((WrapLayout)bindable).InvalidateLayout();
});
public static readonly BindableProperty RowSpacingProperty = BindableProperty.Create(
"RowSpacing",
typeof(double),
typeof(WrapLayout),
5.0,
propertyChanged: (bindable, oldvalue, newvalue) =>
{
((WrapLayout)bindable).InvalidateLayout();
});
每个可绑定属性的属性更改的处理程序调用InvalidateLayout
方法覆盖以触发新的布局传递WrapLayout
。有关更多信息,请参阅覆盖InvalidateLayout方法并覆盖OnChildMeasureInvalidated方法。
的OnMeasure
倍率显示在下面的代码示例:
protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint)
{
LayoutData layoutData = GetLayoutData(widthConstraint, heightConstraint);
if (layoutData.VisibleChildCount == 0)
{
return new SizeRequest();
}
Size totalSize = new Size(layoutData.CellSize.Width * layoutData.Columns + ColumnSpacing * (layoutData.Columns - 1),
layoutData.CellSize.Height * layoutData.Rows + RowSpacing * (layoutData.Rows - 1));
return new SizeRequest(totalSize);
}
覆盖调用该GetLayoutData
方法并SizeRequest
从返回的数据构造一个对象,同时考虑RowSpacing
和ColumnSpacing
属性值。有关该GetLayoutData
方法的更多信息,请参阅计算和缓存数据。
⚠️该
Measure
和OnMeasure
方法不应该通过返回的请求的无穷维SizeRequest
与属性设置为值Double.PositiveInfinity
。但是,至少有一个约束参数OnMeasure
可以是Double.PositiveInfinity
。
的LayoutChildren
倍率显示在下面的代码示例:
protected override void LayoutChildren(double x, double y, double width, double height)
{
LayoutData layoutData = GetLayoutData(width, height);
if (layoutData.VisibleChildCount == 0)
{
return;
}
double xChild = x;
double yChild = y;
int row = 0;
int column = 0;
foreach (View child in Children)
{
if (!child.IsVisible)
{
continue;
}
LayoutChildIntoBoundingRegion(child, new Rectangle(new Point(xChild, yChild), layoutData.CellSize));
if (++column == layoutData.Columns)
{
column = 0;
row++;
xChild = x;
yChild += RowSpacing + layoutData.CellSize.Height;
}
else
{
xChild += ColumnSpacing + layoutData.CellSize.Width;
}
}
}
覆盖开始于对该GetLayoutData
方法的调用,然后枚举所有的孩子的大小并将它们放置在每个孩子的单元格内。这是通过调用LayoutChildIntoBoundingRegion
方法来实现的,该方法用于根据其HorizontalOptions
和VerticalOptions
属性值将一个子项定位在一个矩形内。这相当于调用孩子的Layout
方法。
请注意,传递给该
LayoutChildIntoBoundingRegion
方法的矩形包括小孩所在的整个区域。
有关该GetLayoutData
方法的更多信息,请参阅计算和缓存数据。
InvalidateLayout
当孩子被添加到布局中或从布局中移除时,或当其中一个WrapLayout
属性更改值时,将调用该覆盖,如以下代码示例所示:
protected override void InvalidateLayout()
{
base.InvalidateLayout();
layoutInfoCache.Clear();
}
覆盖使布局无效,并丢弃所有缓存的布局信息。
要停止
Layout
类调用InvalidateLayout
每当孩子添加或删除从布局方法,覆盖ShouldInvalidateOnChildAdded
和ShouldInvalidateOnChildRemoved
方法,并返回false
。然后,当添加或删除子项时,布局类可以实现自定义进程。
OnChildMeasureInvalidated
当其中一个布局的子节点更改大小时,将调用该覆盖,并显示在以下代码示例中:
protected override void OnChildMeasureInvalidated()
{
base.OnChildMeasureInvalidated();
layoutInfoCache.Clear();
}
该覆盖使子版面无效,并丢弃所有缓存的布局信息。
的WrapLayout
类可以通过将其放置在被消耗Page
派生类型,如下面的XAML代码示例表明:
<ContentPage ... xmlns:local="clr-namespace:ImageWrapLayout">
<ScrollView Margin="0,20,0,20">
<local:WrapLayout x:Name="wrapLayout" />
</ScrollView>
</ContentPage>
等效的C#代码如下所示:
public class ImageWrapLayoutPageCS : ContentPage
{
WrapLayout wrapLayout;
public ImageWrapLayoutPageCS()
{
wrapLayout = new WrapLayout();
Content = new ScrollView
{
Margin = new Thickness(0, 20, 0, 20),
Content = wrapLayout
};
}
...
}
然后可以WrapLayout
根据需要将儿童加入。以下代码示例显示Image
要添加到的元素WrapLayout
:
protected override async void OnAppearing()
{
base.OnAppearing();
var images = await GetImageListAsync();
foreach (var photo in images.Photos)
{
var image = new Image
{
Source = ImageSource.FromUri(new Uri(photo + string.Format("?width={0}&height={0}&mode=max", Device.OnPlatform(240, 240, 120))))
};
wrapLayout.Children.Add(image);
}
}
async Task<ImageList> GetImageListAsync()
{
var requestUri = "https://docs.xamarin.com/demo/stock.json";
using (var client = new HttpClient())
{
var result = await client.GetStringAsync(requestUri);
return JsonConvert.DeserializeObject<ImageList>(result);
}
}
当包含WrapLayout
出现的页面时,示例应用程序异步访问包含照片列表的远程JSON文件,Image
为每张照片创建一个元素,并将其添加到WrapLayout
。这将导致以下屏幕截图中显示的外观:
以下屏幕截图显示了WrapLayout
它被旋转到横向:
每行中的列数取决于照片大小,屏幕宽度和每个与设备无关的单元的像素数。该Image
元件异步加载的照片,并且因此,WrapLayout
类将接收到其频繁调用LayoutChildren
每个方法Image
元件接收基于所加载的照片中的新的大小。
本文介绍了如何编写自定义布局类,并演示了一种方向敏感WrapLayout
类,将其子级别横跨页面排列,然后将后续子项的显示包装到其他行。