重叠的子项
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通过使用卡片文件隐喻来显示美术学院的学生:
学生按姓氏排序。 iOS屏幕显示列表的顶部。 Android屏幕滚动到中间的某个位置,Windows 10 Mobile屏幕滚动到最后。 唯一完全可见的学生是儿童系列末尾的学生,其姓氏在字母表中很晚。
要查看学生,您可以点按学生卡的顶部:
通过调用两个模拟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>
你已经看过截图了。