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

简介: 重叠的子项Layout 类可以在其子项上调用Layout方法,以便子项重叠吗?是的,但这可能会在你的脑海中提出另一个问题:什么决定孩子们的呈现顺序?哪些孩子看似坐在前台,可能部分或完全掩盖了背景中显示的其他孩子?在某些图形环境中,程序员可以访问名为Z-index的值。

重叠的子项
Layout 类可以在其子项上调用Layout方法,以便子项重叠吗?是的,但这可能会在你的脑海中提出另一个问题:什么决定孩子们的呈现顺序?哪些孩子看似坐在前台,可能部分或完全掩盖了背景中显示的其他孩子?
在某些图形环境中,程序员可以访问名为Z-index的值。该名称来自于在二维计算机屏幕上可视化三维坐标系。 X和Y轴定义屏幕的水平表面,而Z轴垂直于屏幕。具有较高Z-index的视觉元素看起来更接近前景中的观看者,因此可能使背景中具有较低Z-index的元素模糊。
Xamarin.Forms中没有明确的Z-index。您可能会猜测布局类在其子项上调用Layout方法的顺序隐含了Z索引,但事实并非如此。布局类可以按照您想要的顺序在其子项上调用Layout方法,而不会在显示中进行任何更改。这些调用为每个子项提供相对于其父项的大小和位置,但子项不按该顺序呈现。
相反,孩子们在儿童系列中按顺序呈现。 集合中较早的子项首先被渲染,因此它们出现在背景中,这意味着集合中较晚的子节点似乎位于前景中,并且可能会遮挡早期的子节点。
Layout类定义了两种方法,允许您将子项移动到Children集合的开头或结尾。 这些方法是:

  • LowerChild - 将子项移动到Children集合的开头,并在视觉上移动到背景。
  • RaiseChild - 将子项移动到Children集合的末尾,并在视觉上移动到前台。

孩子必须已经成为儿童系列的一部分才能使这些方法起作用。这些调用导致调用VisualElement定义的受保护OnChildrenReordered方法并触发ChildrenReordered事件。
在编写本章时,LowerChild和RaiseChild方法不适用于各种Windows平台。但是,Layout 定义的Children属性属于IList 类型,因此您还可以通过调用Add,Insert,Remove和RemoveAt将子项移入和移出集合。无论您如何操作,对Children集合内容的任何更改都会导致对LayoutInvalidated的调用和新的布局周期。
当您想要编写与其子项重叠的布局类时,会出现这些问题,但您还希望选项可以隐藏部分模糊的子项,可能需要点击。要将孩子移动到视觉前景,您需要操纵Children集合,但是您还需要确保这些操作不会干扰孩子的渲染。
您将在OverlapLayout类中看到一个可能的解决方案。此布局类在垂直或水平堆栈中显示其子项但重叠。每个子节点位于前一个子节点的稍低(或右侧),由OverlapLayout定义的属性指定为Offset。
这是一个名为StudentCardFile的程序,它使用ScrollView中的OverlapLayout通过使用卡片文件隐喻来显示美术学院的学生:
2019_05_22_091502
学生按姓氏排序。 iOS屏幕显示列表的顶部。 Android屏幕滚动到中间的某个位置,Windows 10 Mobile屏幕滚动到最后。 唯一完全可见的学生是儿童系列末尾的学生,其姓氏在字母表中很晚。
要查看学生,您可以点按学生卡的顶部:
2019_05_22_091550
通过调用两个模拟RaiseChild调用的方法将子进入前台:

overlapLayout.Children.Remove(tappedChild);
overlapLayout.Children.Add(tappedChild);

您现在可以像平常一样滚动列表。所有的孩子从上到下按顺序排列。您可以通过再次点击该子项或通过点击另一个孩子,将该子项恢复到Children集合中的初始位置。
如果您在本章前面考虑VerticalStack的逻辑,您可以想象如果您只是在不执行任何其他操作的情况下调用RaiseChild可能会出现一些问题。 RaiseChild将子节点发送到Children集合的末尾,因此它通常会最后呈现并显示在列表的底部。我们需要一些方法来重新排序Children集合,同时保持渲染顺序不变。
OverlapLayout使用的解决方案是附加的可绑定属性,可以由应用程序在每个子节点上设置。此属性称为RenderOrder,您将很快看到它的工作原理。
以下是如何在布局类中定义附加的可绑定属性。它与常规可绑定属性略有不同:

namespace Xamarin.FormsBook.Toolkit
{
    public class OverlapLayout : Layout<View>
    {
        __
        // Attached bindable property.
        public static readonly BindableProperty RenderOrderProperty =
            BindableProperty.CreateAttached("RenderOrder",
                                            typeof(int),
                                            typeof(OverlapLayout),
                                            0);
        // Helper methods for attached bindable property.
        public static void SetRenderOrder(BindableObject bindable, int order)
        {
            bindable.SetValue(RenderOrderProperty, order);
        }
        public static int GetRenderOrder(BindableObject bindable)
        {
            return (int)bindable.GetValue(RenderOrderProperty);
        }
        __
    }
}

public static read-only字段的定义类似于定义常规可绑定属性,除了您使用静态Bindable.CreateAttached方法,至少定义文本名称
属性,属性的类型,定义属性的类的类型以及默认值。
但是,与常规可绑定属性不同,您不定义C#属性。而是定义两个静态方法来设置和获取属性。这两个静态助手方法(称为SetRenderOrder和GetRenderOrder)并不是严格要求的。任何使用附加的可绑定属性的代码都可以简单地调用SetValue和GetValue,正如方法体所示。但它们是习惯性的。
正如您将看到的,使用OverlapLayout的代码或标记会在每个布局的子项上设置此RenderOrder属性。您将很快看到的StudentCardFile示例在首次创建子项时设置属性,并且永远不会更改它。但是,在一般情况下,在子项上设置的附加可绑定属性可以更改,在这种情况下,需要另一个布局传递。
因此,实现附加可绑定属性的布局应覆盖OnAdded和OnRemoved方法,以便在布局的Children集合中的每个子项上附加(和分离)PropertyChanged事件的处理程序。然后,此处理程序检查附加的可绑定属性中的更改,并在属性值更改时使布局无效:


namespace Xamarin.FormsBook.Toolkit
{
    public class OverlapLayout : Layout<View>
    {
        __
        // Monitor PropertyChanged events for items in the Children collection.
        protected override void OnAdded(View view)
        {
            base.OnAdded(view);
            view.PropertyChanged += OnChildPropertyChanged;
        }
        protected override void OnRemoved(View view)
        {
            base.OnRemoved(view);
            view.PropertyChanged -= OnChildPropertyChanged;
        }
        void OnChildPropertyChanged(object sender, PropertyChangedEventArgs args)
        {
            if (args.PropertyName == "RenderOrder")
            {
                InvalidateLayout();
            }
        }
        __
    }
}

您可以选择引用RenderOrderProperty可绑定属性对象的PropertyName属性,而不是在PropertyChanged处理程序中显式引用属性的文本名称(并可能拼写错误)。
OverlapLayout还定义了两个常规可绑定属性。 Orientation属性基于现有的StackOrientation枚举(因为布局非常类似于堆栈),而Offset指示每个连续子节点之间的差异:

namespace Xamarin.FormsBook.Toolkit
{
    public class OverlapLayout : Layout<View>
    {
        public static readonly BindableProperty OrientationProperty =
            BindableProperty.Create(
                "Orientation",
                typeof(StackOrientation),
                typeof(OverlapLayout),
                StackOrientation.Vertical,
                propertyChanged: (bindable, oldValue, newValue) =>
                {
                    ((OverlapLayout)bindable).InvalidateLayout();
                });
        public static readonly BindableProperty OffsetProperty =
            BindableProperty.Create(
                "Offset",
                typeof(double),
                typeof(OverlapLayout),
                20.0,
                propertyChanged: (bindable, oldvalue, newvalue) =>
                {
                    ((OverlapLayout)bindable).InvalidateLayout();
                });
        __
        public StackOrientation Orientation
        {
            set { SetValue(OrientationProperty, value); }
            get { return (StackOrientation)GetValue(OrientationProperty); }
        }
        public double Offset
        {
            set { SetValue(OffsetProperty, value); }
            get { return (double)GetValue(OffsetProperty); }
        }
        __
    }
}

与本章中的一些其他布局类相比,这两个必需的方法覆盖非常简单。 OnSizeRequest只是确定子项的最大大小,并根据一个子项的大小计算请求的大小 - 因为最初只有一个子项是完全可见的 - 加上Offset值和子项减去1的乘积:

namespace Xamarin.FormsBook.Toolkit
{
    public class OverlapLayout : Layout<View>
    {
        __
        protected override SizeRequest OnSizeRequest(double widthConstraint,
                                                     double heightConstraint)
        {
            int visibleChildCount = 0;
            Size maxChildSize = new Size();
            foreach (View child in Children)
            {
                if (!child.IsVisible)
                    continue;
                visibleChildCount++;
                // Get the child's desired size.
                SizeRequest childSizeRequest = new SizeRequest();
                if (Orientation == StackOrientation.Vertical)
                {
                    childSizeRequest = child.GetSizeRequest(widthConstraint,
                                                            Double.PositiveInfinity);
                }
                else // Orientation == StackOrientation.Horizontal
                {
                    childSizeRequest = child.GetSizeRequest(Double.PositiveInfinity,
                                                            heightConstraint);
                }
                // Find the maximum child width and height.
                maxChildSize.Width = Math.Max(maxChildSize.Width,
                                              childSizeRequest.Request.Width);
                maxChildSize.Height = Math.Max(maxChildSize.Height,
                                               childSizeRequest.Request.Height);
            }
            if (visibleChildCount == 0)
            {
                return new SizeRequest();
            }
            else if (Orientation == StackOrientation.Vertical)
            {
                return new SizeRequest(
                    new Size(maxChildSize.Width,
                             maxChildSize.Height + Offset * (visibleChildCount - 1)));
            }
            else // Orientation == StackOrientation.Horizontal)
            {
                return new SizeRequest(
                    new Size(maxChildSize.Width + Offset * (visibleChildCount - 1),
                             maxChildSize.Height));
            }
        }
        __
    }
}

如果我们不需要担心将隐藏的子项带到前台,LayoutChildren方法将通过Offset单位递增x或y(取决于方向)来定位每个连续的子项。 相反,该方法通过将Offset属性乘以RenderOrder属性来计算每个子项的childOffset值:

namespace Xamarin.FormsBook.Toolkit
{
    public class OverlapLayout : Layout<View>
    {
        __
        protected override void LayoutChildren(double x, double y, double width, double height)
        {
            foreach (View child in Children)
            {
                if (!child.IsVisible)
                    continue;
                SizeRequest childSizeRequest = child.GetSizeRequest(width, height);
                double childOffset = Offset * GetRenderOrder(child);
                if (Orientation == StackOrientation.Vertical)
                {
                    LayoutChildIntoBoundingRegion(child,
                        new Rectangle(x, y + childOffset,
                                      width, childSizeRequest.Request.Height));
                }
                else // Orientation == StackOrientation.Horizontal
                {
                    LayoutChildIntoBoundingRegion(child,
                        new Rectangle(x + childOffset, y,
                                      childSizeRequest.Request.Width, height));
                }
            }
        }
    }
}

执行Offset和RenderOrder属性相乘的语句是

double childOffset = Offset * GetRenderOrder(child);

通过使用GetValue,您可以在没有静态GetRenderOrder属性的情况下执行相同的操作:

double childOffset = Offset * (int)child.GetValue(RenderOrderProperty);

但GetRenderOrder方法肯定更容易。
StudentCardFile程序在ScrollView中定义一个带有OverlapLayout的页面:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:toolkit=
                "clr-namespace:Xamarin.FormsBook.Toolkit;assembly=Xamarin.FormsBook.Toolkit"
             x:Class="StudentCardFile.StudentCardFilePage"
             BackgroundColor="Yellow">
    <ContentPage.Padding>
        <OnPlatform x:TypeArguments="Thickness"
                    iOS="0, 20, 0, 0" />
    </ContentPage.Padding>
    <ScrollView>
        <toolkit:OverlapLayout x:Name="overlapLayout"
                               Padding="10" />
    </ScrollView>
</ContentPage>

代码隐藏文件实例化SchoolViewModel并使用PropertyChanged事件来确定StudentBody属性何时有效。 学生首先按姓氏排序。 然后,对于每个Student对象,代码创建一个StudentView(您很快就会看到)并将Student对象分配给视图的BindingContext:

public partial class StudentCardFilePage : ContentPage
{
    __
    public StudentCardFilePage()
    {
        InitializeComponent();
        // Set a platform-specific Offset on the OverlapLayout.
        overlapLayout.Offset = 2 * Device.GetNamedSize(NamedSize.Large, typeof(Label));
        SchoolViewModel viewModel = new SchoolViewModel();
        viewModel.PropertyChanged += (sender, args) =>
        {
            if (args.PropertyName == "StudentBody")
            {
                // Sort the students by last name.
                var students =
                viewModel.StudentBody.Students.OrderBy(student => student.LastName);
                Device.BeginInvokeOnMainThread(() =>
                {
                    int index = 0;
                    // Loop through the students.
                    foreach (Student student in students)
                    {
                        // Create a StudentView for each.
                        StudentView studentView = new StudentView
                        {
                            BindingContext = student
                        };
                        // Set the Order attached bindable property.
                        OverlapLayout.SetRenderOrder(studentView, index++);
                        // Attach a Tap gesture handler.
                        TapGestureRecognizer tapGesture = new TapGestureRecognizer();
                        tapGesture.Tapped += OnStudentViewTapped;
                        studentView.GestureRecognizers.Add(tapGesture);
                        // Add it to the OverlapLayout.
                        overlapLayout.Children.Add(studentView);
                    }
                });
            }
        };
    }
    __
}

RenderOrder属性只是设置为顺序值:

OverlapLayout.SetRenderOrder(studentView, index++);

它看起来并不多,但是当儿童系列被改变时,它对于维持学生的渲染顺序至关重要。
在Tapped处理程序中更改了Children集合。 请记住,代码需要处理三种不同(但相关)的情况:点击学生卡需要通过操纵Children集合将卡移动到前台,相当于调用RaiseChild - 除非是学生 卡已经在前台,在这种情况下,卡需要放回原处。 如果在点击另一张卡时一张卡已经在前台,则必须将第一张卡移回并将第二张卡移至前台:

public partial class StudentCardFilePage : ContentPage
{
    View exposedChild = null;
    __
    void OnStudentViewTapped(object sender, EventArgs args)
    {
        View tappedChild = (View)sender;
        bool retractOnly = tappedChild == exposedChild;
        // Retract the exposed child.
        if (exposedChild != null)
        {
            overlapLayout.Children.Remove(exposedChild);
            overlapLayout.Children.Insert(
            OverlapLayout.GetRenderOrder(exposedChild), exposedChild);
            exposedChild = null;
        }
        // Expose a new child.
        if (!retractOnly)
        {
            // Raise child.
            overlapLayout.Children.Remove(tappedChild);
            overlapLayout.Children.Add(tappedChild);
            exposedChild = tappedChild;
        }
    }
}

StudentView类派生自ContentView,意味着类似于索引卡。 边框是薄的BoxView元素,另一个BoxView在卡片顶部的名称下绘制一条水平线:

<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="StudentCardFile.StudentView"
             BackgroundColor="White">
    <ContentView.Resources>
        <ResourceDictionary>
            <x:Double x:Key="thickness">3</x:Double>

            <Style TargetType="Label">
                <Setter Property="TextColor" Value="Black" />
            </Style>
            <Style TargetType="BoxView">
                <Setter Property="Color" Value="Black" />
            </Style>
        </ResourceDictionary>
    </ContentView.Resources>

    <Grid>
        <BoxView VerticalOptions="Start"
                 HeightRequest="{StaticResource thickness}" />

        <BoxView VerticalOptions="End"
                 HeightRequest="{StaticResource thickness}" />
        <BoxView HorizontalOptions="Start"
                 WidthRequest="{StaticResource thickness}" />
        <BoxView HorizontalOptions="End"
                 WidthRequest="{StaticResource thickness}" />
        <StackLayout Padding="5">
            <StackLayout Orientation="Horizontal">
                <Label Text="{Binding LastName, StringFormat='{0},'}"
                       FontSize="Large" />
                <Label Text="{Binding FirstName}"
                       FontSize="Large" />
                <Label Text="{Binding MiddleName}"
                       FontSize="Large" />
            </StackLayout>

            <BoxView Color="Accent"
                     HeightRequest="2" />
            <Image Source="{Binding PhotoFilename}" />
            <Label Text="{Binding GradePointAverage, StringFormat='G.P.A. = {0:F2}'}"
                   HorizontalTextAlignment="Center" />
        </StackLayout>
    </Grid>
</ContentView>

你已经看过截图了。

目录
相关文章
|
JavaScript Android开发
第二十六章:自定义布局(十二)
更多附加的可绑定属性附加的可绑定属性也可以在XAML中设置并使用Style设置。 为了了解它是如何工作的,让我们检查一个名为CartesianLayout的类,它模仿一个二维的,四象限的笛卡尔坐标系。
538 0
|
Android开发
第二十六章:自定义布局(十三)
Layout和LayoutToVisualElement定义了一组转换属性。这些是AnchorX,AnchorY,Rotation,RotationX,RotationY,Scale,TranslationX和TranslationY,它们根本不影响布局。
754 0
|
JavaScript Android开发
第二十六章:自定义布局(十)
不允许无约束的尺寸!有时您希望在屏幕上看到所有内容,可能是一系列大小统一的行和列。您可以使用带有星号定义的所有行和列定义的Grid执行类似的操作,以使它们具有相同的大小。唯一的问题是您可能还希望行数和列数基于子节点数,并针对屏幕空间的最佳使用进行了优化。
832 0
|
存储 缓存 JavaScript
第二十六章:自定义布局(九)
编码的一些规则从上面的讨论中,您可以为自己的Layout 衍生物制定几个规则:规则1:如果布局类定义了诸如间距或方向等属性,则这些属性应由可绑定属性支持。 在大多数情况下,这些可绑定属性的属性更改处理程序应调用InvalidateLayout。
2067 0
|
JavaScript Android开发
第二十六章:自定义布局(八)
失效假设您已在页面上组装了一些布局和视图,并且由于某种原因,代码隐藏文件(或者可能是触发器或行为)会更改Button的文本,或者可能只是字体大小或属性。 该更改可能会影响按钮的大小,这可能会对页面其余部分的布局更改产生连锁反应。
3417 0
|
JavaScript Android开发
第二十六章:自定义布局(七)
垂直和水平定位简化在VerticalStack中,LayoutChildren覆盖的末尾是一个switch语句,它有助于根据子级的HorizontalOptions属性设置水平定位每个子级。 这是整个方法: public class VerticalStack : Layout<View> { ...
869 0
|
JavaScript Android开发
第二十六章:自定义布局(六)
从Layout派生 我们现在拥有足够的知识来创建我们自己的布局类。布局中涉及的大多数公共和受保护方法都是由非泛型布局类定义的。 Layout 类派生自Layout,并将泛型类型约束为View及其派生类。
764 0
|
JavaScript Android开发 iOS开发
第二十六章:自定义布局(五)
内视过程中本章到目前为止提供的大部分信息都是从包含派生自各种元素(如StackLayout,ScrollView和Label)的类的测试程序汇编而来,覆盖虚拟方法(如GetSizeRequest,OnSizeRequest,OnSizeAllocated和LayoutChildren) ,并使用System.Diagnostics命名空间中的Debug.WriteLine方法在Visual Studio或Xamarin Studio的“输出”窗口中显示信息。
762 0
|
Android开发
第二十六章:自定义布局(四)
无限约束现在这里有一些标记,起初看起来与前面的例子非常相似,但有很大的不同: <ContentPage __ Padding="20"> <StackLayout> <Label Text="Sample text" /> __ </StackLayout> </ContentPage> ContentPage仍然使用参数(0,0,360,640)进行初始布局调用,而LayoutChildren覆盖的参数是(20,20,320,600)。
703 0