从Layout派生
我们现在拥有足够的知识来创建我们自己的布局类。
布局中涉及的大多数公共和受保护方法都是由非泛型布局类定义的。 Layout 类派生自Layout,并将泛型类型约束为View及其派生类。 Layout 定义了一个名为Children类型为IList 的公共属性,以及一些简短描述的受保护方法。
自定义布局类几乎总是从布局<视图>派生。 如果要将子项限制为某些类型,可以从Layout 或Layout 派生,但这并不常见。 (您将在本章末尾看到一个示例。)
自定义布局类只有两个职责:
- 重写OnSizeRequest以在所有布局的子节点上调用GetSizeRequest。 返回布局本身的请求大小。
- 重写LayoutChildren以在所有布局的子项上调用Layout。
这两种方法通常使用foreach或枚举自定义布局的Children集合中的所有子项。
布局类在每个子节点上调用Layout特别重要。否则,孩子永远不会获得适当的大小或位置,也不会被看见。
但是,OnSizeRequest和LayoutChildren覆盖中的子项枚举应该跳过IsVisible属性设置为false的任何子项。这样的孩子无论如何都不会被看见,但如果你不故意跳过这些孩子,你的布局课可能会为这些看不见的孩子留下空间,这是不正确的行为。
如您所见,无法保证将调用OnSizeRequest覆盖。如果布局的大小由其父级而不是其子级控制,则不需要调用该方法。如果一个或两个约束是无限的,或者布局类具有VerticalOptions或HorizontalOptions的非默认设置,则肯定会调用该方法。否则,无法保证对OnSizeRequest的调用,您不应该依赖它。
您还看到OnSizeRequest调用可能将约束参数设置为Double.PositiveInfinity。但是,OnSizeRequest无法返回具有无限维度的请求大小。有时候会以一种非常简单的方式实现OnSizeRequest的诱惑:
// This is very bad code!
protected override SizeRequest OnSizeRequest(double widthConstraint, double heightConstraint)
{
return new SizeRequest(new Size(widthConstraint, heightConstraint));
}
不要这样做! 如果您的Layout 派生因某些原因无法处理无限约束 - 您将在本章后面看到一个示例 - 然后引发一个异常来指示这一点。
通常,LayoutChildren覆盖还需要知道子节点的大小。 在调用Layout之前,LayoutChildren方法还可以对所有子项调用GetSizeRequest。 可以缓存在OnSizeRequest覆盖中获得的子节点的大小,以避免在LayoutChildren覆盖中稍后进行GetSizeRequest调用,但布局类需要知道何时需要再次获取大小。 你很快就会看到一些指导原则。
一个简单的例子
学习如何编写自定义布局的一种好方法是复制现有布局的功能,但稍微简化一下。
下面描述的VerticalStack类用于模拟StackingLayout,其Orientation设置为Vertical。因此,VerticalStack类没有Orientation属性,为了简单起见,VerticalStack也没有Spacing属性。此外,VerticalStack无法识别其子项的HorizontalOptions和VerticalOptions设置上的Expands标志。忽略Expands标志极大地简化了堆叠逻辑。
因此,VerticalStack只定义了两个成员:OnSizeRequest和LayoutChildren方法的覆盖。通常,两种方法都通过Layout 定义的Children属性进行枚举,通常这两种方法都会调用子项的GetSizeRequest。应跳过任何IsVisible属性设置为false的子项。
VerticalStack中的OnSizeRequest覆盖在每个子节点上调用GetSizeRequest,其约束宽度等于覆盖的widthConstraint参数,约束高度等于Double.PositiveInfinity。这会将子项的宽度限制为VerticalStack的宽度,但允许每个子项尽可能高。这是垂直堆栈的基本特征:
public class VerticalStack : Layout<View>
{
protected override SizeRequest OnSizeRequest(double widthConstraint,
double heightConstraint)
{
Size reqSize = new Size();
Size minSize = new Size();
// Enumerate through all the children.
foreach (View child in Children)
{
// Skip the invisible children.
if (!child.IsVisible)
continue;
// Get the child's requested size.
SizeRequest childSizeRequest = child.GetSizeRequest(widthConstraint,
Double.PositiveInfinity);
// Find the maximum width and accumulate the height.
reqSize.Width = Math.Max(reqSize.Width, childSizeRequest.Request.Width);
reqSize.Height += childSizeRequest.Request.Height;
// Do the same for the minimum size request.
minSize.Width = Math.Max(minSize.Width, childSizeRequest.Minimum.Width);
minSize.Height += childSizeRequest.Minimum.Height;
}
return new SizeRequest(reqSize, minSize);
}
__
}
Children集合上的foreach循环分别为子项返回的SizeRequest对象的Request和Minimum属性累积子项的大小。这些累积涉及两个Size值,名为reqSize和minSize。因为这是一个垂直堆栈,所以reqSize.Width和minSize.Width值设置为子宽度的最大值,而reqSize.Height和minSize.Height值则设置为子高度的总和。
OnSizeRequest的widthConstraint参数可能是Double.PositiveInfinity,在这种情况下,子项的GetSizeRequest调用的参数都是无限的。 (例如,VerticalStack可能是具有水平方向的StackLayout的子级。)通常,OnSizeRequest的主体不需要担心这种情况,因为从GetSizeRequest返回的SizeRequest值永远不会包含无限值。
自定义布局中的第二种方法 - LayoutChildren的覆盖 - 如下所示。这通常被称为父对Layout方法的调用。
LayoutChildren的width和height参数指示可用于其子项的布局区域的大小。两个值都是有限的。如果OnSizeRequest的参数是无限的,则LayoutChildren的相应参数将是从OnSizeRequest覆盖返回的宽度或高度。否则,它取决于HorizontalOptions和VerticalOptions设置。对于Fill,LayoutChildren的参数与OnSizeRequest的相应参数相同。否则,它是从OnSizeRequest返回的请求宽度或高度。
LayoutChildren还有x和y参数,它们反映了布局上设置的Padding属性。例如,如果左边距为20,顶部边距为50,则x为20,y为50.这些通常表示布局的孩子:
public class VerticalStack : Layout<View>
{
__
protected override void LayoutChildren(double x, double y, double width, double height)
{
// Enumerate through all the children.
foreach (View child in Children)
{
// Skip the invisible children.
if (!child.IsVisible)
continue;
// Get the child's requested size.
SizeRequest childSizeRequest = child.GetSizeRequest(width, Double.PositiveInfinity);
// Initialize child position and size.
double xChild = x;
double yChild = y;
double childWidth = childSizeRequest.Request.Width;
double childHeight = childSizeRequest.Request.Height;
// Adjust position and size based on HorizontalOptions.
switch (child.HorizontalOptions.Alignment)
{
case LayoutAlignment.Start:
break;
case LayoutAlignment.Center:
xChild += (width - childWidth) / 2;
break;
case LayoutAlignment.End:
xChild += (width - childWidth);
break;
case LayoutAlignment.Fill:
childWidth = width;
break;
}
// Layout the child.
child.Layout(new Rectangle(xChild, yChild, childWidth, childHeight));
// Get the next child’s vertical position.
y += childHeight;
}
}
}
这是一个垂直堆栈,因此LayoutChildren需要根据子请求的高度垂直定位每个子项。如果子项的HorizontalOptions设置为Fill,则每个子项的宽度与VerticalStack的宽度相同(减去填充)。否则,子宽度是其请求的宽度,并且堆栈必须将该子对象放置在其自己的宽度内。
为了执行这些计算,LayoutChildren再次对其子项调用GetSizeRequest,但这次使用LayoutChildren的实际width和height参数,而不是OnSizeRequest中使用的约束参数。然后它调用每个孩子的布局。 Rectangle构造函数的height参数始终是子项的高度。 width参数可以是子节点的宽度,也可以是传递给LayoutChildren覆盖的VerticalStack的宽度,具体取决于子节点上的HorizontalOptions设置。请注意,每个子项位于VerticalStack左侧的x个单位,第一个子项位于VerticalStack顶部的y个单位。然后根据孩子的身高,在循环的底部增加y变量。这会创建堆栈。
VerticalStack类是VerticalStackDemo程序的一部分,该程序包含一个导航到两个页面以测试它的主页。当然,您可以添加更多测试页面(这是您应该为您开发的任何Layout 类做的事情)。
这两个测试页面在主页中实例化:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:local="clr-namespace:VerticalStackDemo;assembly=VerticalStackDemo"
x:Class="VerticalStackDemo.VerticalStackDemoHomePage"
Title="VerticalStack Demo">
<ListView ItemSelected="OnListViewItemSelected">
<ListView.ItemsSource>
<x:Array Type="{x:Type Page}">
<local:LayoutOptionsTestPage />
<local:ScrollTestPage />
</x:Array>
</ListView.ItemsSource>
<ListView.ItemTemplate>
<DataTemplate>
<TextCell Text="{Binding Title}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</ContentPage>
js
代码隐藏文件导航到所选页面:
public partial class VerticalStackDemoHomePage : ContentPage
{
public VerticalStackDemoHomePage()
{
InitializeComponent();
}
async void OnListViewItemSelected(object sender, SelectedItemChangedEventArgs args)
{
((ListView)sender).SelectedItem = null;
if (args.SelectedItem != null)
{
Page page = (Page)args.SelectedItem;
await Navigation.PushAsync(page);
}
}
}
第一个测试页面使用VerticalStack显示五个具有不同HorizontalOptions设置的Button元素。 VerticalStack本身具有VerticalOptions设置,应将其放置在页面中间:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:VerticalStackDemo;assembly=VerticalStackDemo"
x:Class="VerticalStackDemo.LayoutOptionsTestPage"
Title="Test Layout Options">
<local:VerticalStack Padding="50, 0"
VerticalOptions="Center">
<Button Text="Default" />
<Button Text="Start"
HorizontalOptions="Start" />
<Button Text="Center"
HorizontalOptions="Center" />
<Button Text="End"
HorizontalOptions="End" />
<Button Text="Fill"
HorizontalOptions="Fill" />
</local:VerticalStack>
</ContentPage>
果然,VerticalStack子节点上各种HorizontalOptions设置的逻辑似乎有效:
显然,Windows 10移动平台将受益于按钮之间的一些间距!
如果删除VerticalStack上的VerticalOptions设置,则VerticalStack将根本不会调用其OnSizeRequest覆盖。 没有必要。 LayoutChildren的参数将反映页面的整个大小而不是Padding,页面不需要知道VerticalStack需要多少空间。
第二个测试程序将VerticalStack放在ScrollView中:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:VerticalStackDemo;assembly=VerticalStackDemo"
x:Class="VerticalStackDemo.ScrollTestPage"
Title="Test Scrolling">
<ScrollView>
<local:VerticalStack x:Name="stack" />
</ScrollView>
</ContentPage>
代码隐藏文件使用125个常规StackLayout实例填充VerticalStack,每个实例包含一个BoxView,另一个VerticalStack包含三个Label元素:
public partial class ScrollTestPage : ContentPage
{
public ScrollTestPage()
{
InitializeComponent();
for (double r = 0; r <= 1.0; r += 0.25)
for (double g = 0; g <= 1.0; g += 0.25)
for (double b = 0; b <= 1.0; b += 0.25)
{
stack.Children.Add(new StackLayout
{
Orientation = StackOrientation.Horizontal,
Padding = 6,
Children =
{
new BoxView
{
Color = Color.FromRgb(r, g, b),
WidthRequest = 100,
HeightRequest = 100
},
new VerticalStack
{
VerticalOptions = LayoutOptions.Center,
Children =
{
new Label { Text = "Red = " + r.ToString("F2") },
new Label { Text = "Green = " + g.ToString("F2") },
new Label { Text = "Blue = " + b.ToString("F2") }
}
}
}
});
}
}
}
VerticalStack是具有垂直滚动方向的ScrollView的子节点,因此它接收高度为Double.PositiveInfinity的OnSizeRequest调用。 VerticalStack响应的高度包含其所有孩子。 ScrollView使用该高度及其自身高度(基于屏幕大小)来滚动其内容: