從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使用該高度及其自身高度(基于螢幕大小)來滾動其内容: