第二十六章:自定义布局(六)

简介: 从Layout派生我们现在拥有足够的知识来创建我们自己的布局类。布局中涉及的大多数公共和受保护方法都是由非泛型布局类定义的。 Layout 类派生自Layout,并将泛型类型约束为View及其派生类。

从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设置的逻辑似乎有效:
2019_05_16_163232
显然,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使用该高度及其自身高度(基于屏幕大小)来滚动其内容:2019_05_16_163644

目录
相关文章
|
6月前
|
定位技术 iOS开发
自动布局xib页面的机型匹配精典问题及解决方案
自动布局xib页面的机型匹配精典问题及解决方案
42 0
|
iOS开发
iOS动画开发之三——UIView的转场切换
iOS动画开发之三——UIView的转场切换
378 0
|
JavaScript Android开发
第二十六章:自定义布局(十)
不允许无约束的尺寸!有时您希望在屏幕上看到所有内容,可能是一系列大小统一的行和列。您可以使用带有星号定义的所有行和列定义的Grid执行类似的操作,以使它们具有相同的大小。唯一的问题是您可能还希望行数和列数基于子节点数,并针对屏幕空间的最佳使用进行了优化。
835 0
|
存储 缓存 JavaScript
第二十六章:自定义布局(九)
编码的一些规则从上面的讨论中,您可以为自己的Layout 衍生物制定几个规则:规则1:如果布局类定义了诸如间距或方向等属性,则这些属性应由可绑定属性支持。 在大多数情况下,这些可绑定属性的属性更改处理程序应调用InvalidateLayout。
2070 0
|
JavaScript Android开发
第二十六章:自定义布局(八)
失效假设您已在页面上组装了一些布局和视图,并且由于某种原因,代码隐藏文件(或者可能是触发器或行为)会更改Button的文本,或者可能只是字体大小或属性。 该更改可能会影响按钮的大小,这可能会对页面其余部分的布局更改产生连锁反应。
3421 0
|
JavaScript Android开发
第二十六章:自定义布局(七)
垂直和水平定位简化在VerticalStack中,LayoutChildren覆盖的末尾是一个switch语句,它有助于根据子级的HorizontalOptions属性设置水平定位每个子级。 这是整个方法: public class VerticalStack : Layout<View> { ...
872 0
|
JavaScript Android开发
第二十六章:自定义布局(十二)
更多附加的可绑定属性附加的可绑定属性也可以在XAML中设置并使用Style设置。 为了了解它是如何工作的,让我们检查一个名为CartesianLayout的类,它模仿一个二维的,四象限的笛卡尔坐标系。
540 0
|
JavaScript Android开发
第二十六章:自定义布局(十一)
重叠的子项Layout 类可以在其子项上调用Layout方法,以便子项重叠吗?是的,但这可能会在你的脑海中提出另一个问题:什么决定孩子们的呈现顺序?哪些孩子看似坐在前台,可能部分或完全掩盖了背景中显示的其他孩子?在某些图形环境中,程序员可以访问名为Z-index的值。
643 0
|
JavaScript Android开发 iOS开发
第二十六章:自定义布局(五)
内视过程中本章到目前为止提供的大部分信息都是从包含派生自各种元素(如StackLayout,ScrollView和Label)的类的测试程序汇编而来,覆盖虚拟方法(如GetSizeRequest,OnSizeRequest,OnSizeAllocated和LayoutChildren) ,并使用System.Diagnostics命名空间中的Debug.WriteLine方法在Visual Studio或Xamarin Studio的“输出”窗口中显示信息。
762 0