天天看點

第二十六章:自定義布局(六)從Layout派生

從Layout派生

我們現在擁有足夠的知識來建立我們自己的布局類。

布局中涉及的大多數公共和受保護方法都是由非泛型布局類定義的。 Layout 類派生自Layout,并将泛型類型限制為View及其派生類。 Layout 定義了一個名為Children類型為IList 的公共屬性,以及一些簡短描述的受保護方法。

自定義布局類幾乎總是從布局<視圖>派生。 如果要将子項限制為某些類型,可以從Layout 或Layout 派生,但這并不常見。 (您将在本章末尾看到一個示例。)

自定義布局類隻有兩個職責:

  • 重寫OnSizeRequest以在所有布局的子節點上調用GetSizeRequest。 傳回布局本身的請求大小。
  • 重寫LayoutChildren以在所有布局的子項上調用Layout。

這兩種方法通常使用foreach或枚舉自定義布局的Children集合中的所有子項。

布局類在每個子節點上調用Layout特别重要。否則,孩子永遠不會獲得适當的大小或位置,也不會被看見。

但是,OnSizeRequest和LayoutChildren覆寫中的子項枚舉應該跳過IsVisible屬性設定為false的任何子項。這樣的孩子無論如何都不會被看見,但如果你不故意跳過這些孩子,你的布局課可能會為這些看不見的孩子留下空間,這是不正确的行為。

如您所見,無法保證将調用OnSizeRequest覆寫。如果布局的大小由其父級而不是其子級控制,則不需要調用該方法。如果一個或兩個限制是無限的,或者布局類具有VerticalOptions或Horizo​​ntalOptions的非預設設定,則肯定會調用該方法。否則,無法保證對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無法識别其子項的Horizo​​ntalOptions和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覆寫傳回的寬度或高度。否則,它取決于Horizo​​ntalOptions和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需要根據子請求的高度垂直定位每個子項。如果子項的Horizo​​ntalOptions設定為Fill,則每個子項的寬度與VerticalStack的寬度相同(減去填充)。否則,子寬度是其請求的寬度,并且堆棧必須将該子對象放置在其自己的寬度内。

為了執行這些計算,LayoutChildren再次對其子項調用GetSizeRequest,但這次使用LayoutChildren的實際width和height參數,而不是OnSizeRequest中使用的限制參數。然後它調用每個孩子的布局。 Rectangle構造函數的height參數始終是子項的高度。 width參數可以是子節點的寬度,也可以是傳遞給LayoutChildren覆寫的VerticalStack的寬度,具體取決于子節點上的Horizo​​ntalOptions設定。請注意,每個子項位于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設定的邏輯似乎有效:

第二十六章:自定義布局(六)從Layout派生

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

第二十六章:自定義布局(六)從Layout派生

繼續閱讀