Xamarin.Forms之页面及导航

参考链接:

Xamarin. Forms 页面

Xamarin.Forms 导航

Xamarin.Forms 第04局:页面

Xamarin.Forms页面代表跨平台的移动应用程序屏幕。

下文描述的所有页面类型均来自Xamarin.Forms Page类。 这些视觉元素占据了整个或大部分屏幕。 Page对象代表iOS中的ViewController和Universal Windows Platform中的Page。 在Android上,每个页面都像一个Activity一样占据屏幕,但是Xamarin.Forms页面不是Activity对象。

 

ContentPage

ContentPage是最简单,最常见的页面类型,将Content属性设置为单个View对象,该对象通常是Layout,例如StackLayout,Grid或ScrollView。

MasterDetailPage

MasterDetailPage有两个ContentPage组成:Master母版页面和Detail详情页面; 将Master属性设置为通常显示列表或菜单的页面。 将Detail属性设置为显示从母版页中选择的项目的页面,IsPresented属性控制母版页或详细页是否可见。

项目列表的位置在每个平台上都是相同的,选择其中一个项目将导航到相应的详细信息页面。

此外,母版页还具有一个导航栏,其中包含一个按钮,可用于导航到活动详细信息页面:

  • 在iOS上,导航栏位于页面顶部,并且具有一个导航到详细信息页面的按钮。此外,可以通过向左滑动母版页来导航到活动详细信息页。
  • 在Android上,导航栏位于页面顶部,并显示标题,图标和导航到详细信息页面的按钮。该图标在[Activity]属性中定义,该属性用于装饰特定于Android平台的项目中的MainActivity类。此外,可以通过以下方式导航到活动的详细信息页面:向左滑动母版页,点击屏幕最右侧的详细信息页面,然后点击屏幕底部的“后退”按钮。
  • 在通用Windows平台(UWP)上,导航栏位于页面顶部,并且具有一个导航到详细信息页面的按钮。

注:母版页的按钮(Page Button,却是显示在详情页的顶部)在android和uwp是自带的,在ios上显示,必须设置IconImageSource="hamburger.png"才能显示,否则只显示成Title

设置了IconImageSource,则Title不会显示(但是必须设置Title)。

以下是设置IconImageSource与否在Ios上的表现:

     

所选择的主页上对应于该项目的细节页面显示数据,并且将详细信息页面的主要部件示于下面的屏幕截图:

详细信息页面包含一个导航栏,其内容取决于平台:

  • 在iOS上,导航栏位于页面顶部,并显示标题(Page Title),并具有返回主页面的按钮,前提是详细页面实例包装在NavigationPage实例中。 此外,可以通过向右滑动详细信息页面来返回母版页。
  • 在Android上,导航栏位于页面顶部,并显示标题,图标和返回主页面的按钮。 该图标在[Activity]属性中定义,该属性用于装饰特定于Android平台的项目中的MainActivity类。
  • 在UWP上,导航栏位于页面顶部,并显示标题,并具有一个返回主页面的按钮。

导航行为
母版页和详细页之间的导航体验行为取决于平台:

  • 在iOS上,详细信息页面向右滑动,而母版页面从左侧滑动,并且详细信息页面的左侧部分仍然可见。
  • 在Android上,详细信息页和母版页彼此重叠。
  • 在UWP上,如果将MasterBehavior属性设置为Popover,则母版页将从详细信息页面的左侧滑动。 有关更多信息,请参见控制明细页显示行为

在横向模式下将观察到类似的行为,除了iOS和Android上的母版页与纵向模式下的母版页具有相似的宽度外,因此更多详细信息页将可见。

创建MasterDetailPage

注意:MasterDetailPage被设计为根页面,在其他页面类型中将其用作子页面可能会导致意外和不一致的行为。 另外,建议MasterDetailPage的母版页应始终为ContentPage实例,并且详细信息页应仅填充TabbedPage,NavigationPage和ContentPage实例,这将有助于确保在所有平台上的一致用户体验

<MasterDetailPage xmlns="http://xamarin.com/schemas/2014/forms"
                  xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                  xmlns:local="clr-namespace:MasterDetailPageNavigation;assembly=MasterDetailPageNavigation"
                  x:Class="MasterDetailPageNavigation.MainPage">
    <MasterDetailPage.Master>
        <local:MasterPage x:Name="masterPage" />
    </MasterDetailPage.Master>
    <MasterDetailPage.Detail>
        <NavigationPage>
            <x:Arguments>
                <local:ContactsPage />
            </x:Arguments>
        </NavigationPage>
    </MasterDetailPage.Detail>
</MasterDetailPage>
View Code

MasterDetailPage.Master属性设置为ContentPage实例。 MasterDetailPage.Detail属性设置为包含ContentPage实例的NavigationPage。

创建母版页

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="using:MasterDetailPageNavigation"
             x:Class="MasterDetailPageNavigation.MasterPage"
             Padding="0,40,0,0"
             IconImageSource="hamburger.png"
             Title="Personal Organiser">
    <StackLayout>
        <ListView x:Name="listView" x:FieldModifier="public">
           <ListView.ItemsSource>
                <x:Array Type="{x:Type local:MasterPageItem}">
                    <local:MasterPageItem Title="Contacts" IconSource="contacts.png" TargetType="{x:Type local:ContactsPage}" />
                    <local:MasterPageItem Title="TodoList" IconSource="todo.png" TargetType="{x:Type local:TodoListPage}" />
                    <local:MasterPageItem Title="Reminders" IconSource="reminders.png" TargetType="{x:Type local:ReminderPage}" />
                </x:Array>
            </ListView.ItemsSource>
            <ListView.ItemTemplate>
                <DataTemplate>
                    <ViewCell>
                        <Grid Padding="5,10">
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="30"/>
                                <ColumnDefinition Width="*" />
                            </Grid.ColumnDefinitions>
                            <Image Source="{Binding IconSource}" />
                            <Label Grid.Column="1" Text="{Binding Title}" />
                        </Grid>
                    </ViewCell>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </StackLayout>
</ContentPage>
View Code

该页面设置了Title和IconImageSource属性,该图标将显示在详细信息页面上,前提是该详细信息页面具有标题栏。 必须在iOS上启用此功能,方法是将详细信息页面实例包装在NavigationPage实例中。

注:MasterDetailPage.Master页必须设置其Title属性,否则将发生异常

创建详情页

MasterPage实例包含一个ListView属性,该属性公开其ListView实例,以便MainPage MasterDetailPage实例可以注册一个事件处理程序来处理ItemSelected事件。 这使MainPage实例可以将Detail属性设置为表示所选ListView项的页面。 以下代码示例显示了事件处理程序:

public partial class MainPage : MasterDetailPage
{
    public MainPage ()
    {
        ...
        masterPage.listView.ItemSelected += OnItemSelected;
    }

    void OnItemSelected (object sender, SelectedItemChangedEventArgs e)
    {
        var item = e.SelectedItem as MasterPageItem;
        if (item != null) {
            Detail = new NavigationPage ((Page)Activator.CreateInstance (item.TargetType));
            masterPage.listView.SelectedItem = null;
            IsPresented = false;
        }
    }
}
View Code

OnItemSelected方法执行以下操作:

  • 它从ListView实例检索SelectedItem,并提供它(如果不为null的话),则将详细信息页面设置为存储在MasterPageItem的TargetType属性中的页面类型的新实例,页面类型包装在NavigationPage实例中,以确保通过MasterPage上的IconImageSource属性引用的图标显示在iOS的详细信息页面中
  • 在母版页ListView中选定的项目设置为空,以确保下次进入母版页时没有ListView的项目将被选中。
  • 通过将MasterDetailPage.IsPresented属性设置为false,可以向用户显示详细信息页面。 此属性控制是否显示母版页或详细页,应该将其设置为true以显示母版页,并设置为false以显示详细信息页。

控制详细信息页面的显示行为
MasterDetailPage如何管理母版页和详细信息页取决于应用程序是在手机还是平板电脑上运行,设备的方向以及MasterBehavior属性的值,此属性确定详细信息页面的显示方式。 可能的值为:

  • Default-使用平台默认显示页面。
  • Popover–详细信息页面覆盖或部分覆盖了母版页
  • Split–主页面显示在左侧,详细信息页面显示在右侧。
  • SplitOnLandscape –当设备处于横向时,将使用拆分屏幕。
  • SplitOnPortrait –设备处于纵向时使用拆分屏幕。
<?xml version="1.0" encoding="UTF-8"?>
<MasterDetailPage xmlns="http://xamarin.com/schemas/2014/forms"
                  xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                  x:Class="MasterDetailPageNavigation.MainPage"
                  MasterBehavior="Popover">
  ...
</MasterDetailPage>

但是,MasterBehavior属性的值仅影响在平板电脑或台式机上运行的应用程序,手机上运行的应用程序始终具有Popover行为

NavigationPage

NavigationPage使用基于堆栈的体系结构管理其他页面之间的导航。 在应用程序中使用页面导航时,应将主页的实例传递给NavigationPage对象的构造函数

NavigationPage不可用于Xaml创建。

C#方式

注意:这里只有将ContentPage用NavigationPage包裹起来,才能使用Navigation属性进行导航

分层导航

NavigationPage类提供了分层的导航体验,其中用户可以根据需要在页面之间进行导航。 该类将导航实现为Page对象的后进先出(LIFO)堆栈。 本文演示了如何使用NavigationPage类在页面堆栈中执行导航。

要从一个页面移动到另一个页面,应用程序会将新页面推入导航堆栈,在该页面中它将变为活动页面,如下图所示:

 

要返回上一页,应用程序将从导航堆栈中弹出当前页面,新的最顶层页面变为活动页面。

导航方法由任何Page派生类型上的Navigation属性公开,这些方法提供了将页面推入导航堆栈,从导航堆栈弹出页面以及执行堆栈操作的能力。

执行导航
在分层导航中,NavigationPage类用于通过ContentPage对象的堆栈来导航。 以下屏幕快照显示了每个平台上NavigationPage的主要组件:

NavigationPage的布局取决于平台:

  • 在iOS上,页面顶部显示导航栏,该导航栏显示标题,并且具有返回上一页的“后退”按钮。
  • 在Android上,页面顶部显示导航栏,其中显示标题,图标和返回上一页的“后退”按钮。 该图标在[Activity]属性中定义,该属性用于装饰特定于Android平台的项目中的MainActivity类。
  • 在通用Windows平台上,页面顶部显示导航栏,显示标题。

在所有平台上,Page.Title属性的值将显示为页面标题

注:建议仅用ContentPage实例填充NavigationPage

创建根页面
添加到导航堆栈的第一页称为应用程序的根页面,下面的代码示例显示了如何完成此操作:

public App ()
{
  MainPage = new NavigationPage (new Page1Xaml ());
}

这导致Page1Xaml ContentPage实例被推入导航堆栈,在该堆栈中它成为应用程序的活动页面和根页面。 

 注:NavigationPage实例的RootPage属性提供对导航堆栈中第一页的访问

将页面推送到导航堆栈
要导航到Page2Xaml,必须在当前页面的Navigation属性上调用PushAsync方法,如以下代码示例所示:

async void OnNextPageButtonClicked (object sender, EventArgs e)
{
  await Navigation.PushAsync (new Page2Xaml ());
}

这导致Page2Xaml实例被推入导航堆栈,在该堆栈中它成为活动页面,此页面顶部会带有返回按钮

调用PushAsync方法时,将发生以下事件:

  • 调用PushAsync的页面将调用其OnDisappearing覆盖。
  • 导航到的页面将调用其OnAppearing覆盖。
  • PushAsync任务完成。

但是,这些事件发生的确切顺序取决于平台。 有关更多信息,请参见Charles Petzold的Xamarin.Forms书的第24章

从导航堆栈弹出页面
通过按设备上的“后退”按钮,可以从导航堆栈中弹出活动页面,而不管这是设备上的物理按钮还是屏幕上的按钮。

要以编程方式返回到原始页面,Page2Xaml实例必须调用PopAsync方法。

除了PushAsync和PopAsync方法之外,每个页面的Navigation属性还提供了PopToRootAsync方法,如以下代码示例所示:

async void OnRootPageButtonClicked (object sender, EventArgs e)
{
  await Navigation.PopToRootAsync ();
}

此方法从导航堆栈中弹出除根页面以外的所有内容,因此使应用程序的根页面成为活动页面。

动画页面过渡
每个页面的Navigation属性还提供了重载的push和pop方法,其中包括一个布尔参数,该布尔参数控制是否在导航期间显示页面动画,如以下代码示例所示:

await Navigation.PushAsync (new Page2Xaml (), false);

将boolean参数设置为false将禁用页面转换动画,而将参数设置为true则将启用页面转换动画,前提是基础平台支持该动画。 但是,缺少此参数的push和pop方法默认情况下会启用动画

导航时传递数据

有时,页面在导航期间必须将数据传递到另一个页面。两种技术实现这一点:通过一个页面构造函数传递数据;通过新的一页的BindingContext中设置的数据。 

通过页面构造器传递数据
在导航期间将数据传递到另一个页面的最简单技术是通过页面构造函数参数,

通过BindingContext传递数据
将新页面的BindingContext设置为需要传递的数据,如以下代码示例所示:

async void OnNavigateButtonClicked (object sender, EventArgs e)
{
  var contact = new Contact {
    Name = "Jane Doe",
    Age = 30,
    Occupation = "Developer",
    Country = "USA"
  };

  var secondPage = new SecondPage ();
  secondPage.BindingContext = contact;
  await Navigation.PushAsync (secondPage);
}
View Code

此代码将SecondPage实例的BindingContext设置为Contact实例,然后导航到SecondPage。

然后,SecondPage使用数据绑定来显示Contact实例数据,如以下XAML代码示例所示:

操纵导航堆栈

Navigation属性公开了NavigationStack属性,从中可以获取导航堆栈中的页面,Xamarin.Forms保留对导航堆栈的访问权限,而Navigation属性提供了InsertPageBefore和RemovePage方法,用于通过插入页面或删除页面来操纵堆栈

InsertPageBefore方法将指定的页面在导航堆栈中插入现有的指定页面之前,如下图所示:

 

 这些方法可实现自定义导航体验,例如在成功登录后用新页面替换登录页面。 下面的代码示例演示了这种情况:

async void OnLoginButtonClicked (object sender, EventArgs e)
{
  ...
  var isValid = AreCredentialsCorrect (user);
  if (isValid) {
    App.IsUserLoggedIn = true;
    Navigation.InsertPageBefore (new MainPage (), this);
    await Navigation.PopAsync ();
  } else {
    // Login failed
  }
}
View Code

如果用户的凭据正确,则将MainPage实例插入当前页面之前的导航堆栈中。 然后,PopAsync方法从导航堆栈中删除当前页面,而MainPage实例成为活动页面。【此种方式 可以将登陆页面从堆栈中删除,而不是压入栈里】

在导航栏中显示视图

任何Xamarin.Forms视图都可以显示在NavigationPage的导航栏中,这是通过将NavigationPage.TitleView附加属性设置为View来完成的。 可以在任何页面上设置此附加属性,并且当将页面推入NavigationPage时,NavigationPage将遵循该属性的值。

以下示例摘自Title View示例,显示了如何从XAML设置NavigationPage.TitleView附加属性:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="NavigationPageTitleView.TitleViewPage">
    <NavigationPage.TitleView>
        <Slider HeightRequest="44" WidthRequest="300" />
    </NavigationPage.TitleView>
    ...
</ContentPage>
View Code

注:除非使用WidthRequest和HeightRequest属性指定视图的大小,否则许多视图不会出现在导航栏中。 另外,也可以将View包裹在StackLayout中,并将HorizontalOptions和VerticalOptions属性设置为适当的值

请注意,由于Layout类是从View类派生的,因此可以将TitleView附加属性设置为显示包含多个视图的布局类。在iOS和通用Windows平台(UWP)上,无法更改导航栏的高度,因此,如果导航栏中显示的视图大于导航栏的默认大小,则会发生裁剪。但是,在Android上,可以通过将NavigationPage.BarHeight可绑定属性设置为表示新高度的double来更改导航栏的高度。 有关更多信息,请参见在NavigationPage上设置导航栏高度

或者,可以通过将某些内容放置在导航栏中,而将某些内容放置在页面顶部的视图中(颜色与导航栏颜色匹配)来扩展导航栏。 另外,在iOS上,可以通过将NavigationPage.HideNavigationBarSeparator bindable属性设置为true来删除导航栏底部的分隔线和阴影。 有关更多信息,请参见将导航栏分隔符隐藏在NavigationPage上

注:BackButtonTitle,Title,TitleIcon和TitleView属性都可以定义占用导航栏上空间的值。 尽管导航栏的大小随平台和屏幕的大小而变化,但由于可用空间有限,所有这些属性的设置都会导致冲突。 您可能会发现,仅设置TitleView属性,可以更好地实现所需的导航栏设计,而不是尝试使用这些属性的组合

也就是说在TabbedPage中若放置的是包含的内容页的NavigationPage,则在内容页中设置Title 会显示在导航栏中;若放置的是内容页,设置Title则不会显示

局限性
在NavigationPage的导航栏中显示View时,要注意许多限制:

  • 在iOS上,放置在NavigationPage导航栏中的视图会以不同的位置显示,这取决于是否启用大标题。有关启用大标题的更多信息,请参见显示大标题
  • 在Android上,只能在使用app-compat的应用程序中将视图放置在NavigationPage的导航栏中。
  • 不建议将大而复杂的视图(例如ListView和TableView)放置在NavigationPage的导航栏中。

TabbedPage

TabbedPage派生自抽象的MultiPage类,并允许使用选项卡在子页面之间导航,将Children属性设置为页面的集合,或者将ItemsSource属性设置为数据对象的集合,并将ItemTemplate属性设置为DataTemplate,以描述如何直观地表示每个对象。

Xamarin.Forms TabbedPage由一个选项卡列表和一个较大的详细区域组成,每个选项卡都将内容加载到详细区域。 本文演示了如何使用TabbedPage浏览页面集合。

TabbedPage及其选项卡的布局取决于平台:

  • 在iOS上,标签列表显示在屏幕底部,详细信息区域在上方。 每个选项卡还具有一个图标图像,该图像应为30x30 PNG,正常分辨率为透明,高分辨率为60x60,iPhone 6 Plus分辨率为90x90。 如果有五个以上的选项卡,则会出现一个“更多”选项卡,可用于访问其他选项卡。  有关图标要求的更多信息,请参见创建选项卡式应用程序

注:iOS版TabbedRenderer具有可重写的GetIcon方法,可用于从指定源加载选项卡图标。通过此覆盖,可以将SVG图像用作TabbedPage上的图标。另外,可以提供图标的选定和未选定版本。

  • 在Android上,默认情况下,标签列表显示在屏幕顶部,而详细信息区域在下面。 但是,可以将特定于平台的选项卡列表移动到屏幕底部。 有关更多信息,请参见设置TabbedPage工具栏的位置和颜色

注:测试中发现,若不设置在底部时On<Android>().SetToolbarPlacement(ToolbarPlacement.Bottom),xaml中设置的BarBackgroundColor不会生效,此时单击tab图标不会上下跳动(没设置Title情况下);而设置在底部时,BarBackgroundColor会生效,此时单击tab会上下跳动(没设置Title情况下),解决这个问题需要特定平台重新渲染)

注:在Android上使用AppCompat 时,每个选项卡也将显示一个图标。 此外,Android AppCompat的TabbedPageRenderer具有可重写的GetIconDrawable方法,该方法可用于从自定义Drawable加载选项卡图标。 通过此覆盖,可以将SVG图像用作TabbedPage上的图标,并且可以与顶部和底部的标签栏一起使用。 另外,可重写的SetTabIcon方法可用于从自定义Drawable中为顶部标签栏加载标签图标。

  • 在Windows平板电脑上,选项卡并非始终可见,用户需要向下滑动(或单击鼠标右键(如果已连接鼠标),以查看TabbedPage中的选项卡(如下所示)。

创建TabbedPage

TabbedPage定义以下属性:

  • BarBackgroundColor of type Color, the background color of the tab bar.
  • BackgroundColor  设置的大块详情区域的背景颜色
  • BarTextColor of type Color, the color of text on the tab bar.
  • SelectedTabColor of type Color, the color of the tab when it's selected.
  • UnselectedTabColor of type Color, the color of the tab when it's unselected.

所有这些属性都由BindableProperty对象支持,这意味着可以对它们进行样式设置,并且这些属性可以成为数据绑定的目标

可以使用两种方法来创建TabbedPage:

  • 用子页面对象的集合(例如ContentPage实例的集合)填充TabbedPage。
  • 将一个集合分配给ItemsSource属性,将一个DataTemplate分配给ItemTemplate属性,以返回该集合中对象的页面。

通过这两种方法,TabbedPage将在用户选择每个选项卡时显示每个页面。

注:建议仅使用NavigationPage和ContentPage实例填充TabbedPage。 这将有助于确保在所有平台上的一致用户体验。 

1、第一种方式:

添加了两个子页面,一个是ContentPage,一个是包含了内容页的NavigationPage

<TabbedPage xmlns="http://xamarin.com/schemas/2014/forms"
            xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
            xmlns:local="clr-namespace:TabbedPageWithNavigationPage;assembly=TabbedPageWithNavigationPage"
            x:Class="TabbedPageWithNavigationPage.MainPage">
    <local:TodayPage />
    <NavigationPage Title="Schedule" IconImageSource="schedule.png">
        <x:Arguments>
            <local:SchedulePage />
        </x:Arguments>
    </NavigationPage>
</TabbedPage>
View Code

注:TabbedPage不支持UI虚拟化。 因此,如果TabbedPage包含太多子元素,则性能可能会受到影响。

注:尽管可以将NavigationPage放置在TabbedPage中,但不建议在NavigationPage中放置TabbedPage。 这是因为,在iOS上,UITabBarController始终充当UINavigationController的包装器。 有关更多信息,请参见iOS开发者库中的组合视图控制器接口

Tab选项内的导航
可以通过调用ContentPage实例的Navigation属性上的PushAsync方法从第二个选项卡执行导航,如以下代码示例所示:

async void OnUpcomingAppointmentsButtonClicked (object sender, EventArgs e)
{
  await Navigation.PushAsync (new UpcomingAppointmentsPage ());
}

这导致UpcomingAppointmentsPage实例被推到导航堆栈上,在该堆栈中成为活动页面。

2、第二种方式:用模板填充TabbedPage

以下XAML代码示例显示通过将DataTemplate分配给ItemTemplate属性以返回集合中对象的页面而构造的TabbedPage:

<TabbedPage xmlns="http://xamarin.com/schemas/2014/forms"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:local="clr-namespace:TabbedPageDemo;assembly=TabbedPageDemo"
            x:Class="TabbedPageDemo.TabbedPageDemoPage">
  <TabbedPage.Resources>
    <ResourceDictionary>
      <local:NonNullToBooleanConverter x:Key="booleanConverter" />
    </ResourceDictionary>
  </TabbedPage.Resources>
  <TabbedPage.ItemTemplate>
    <DataTemplate>
      <ContentPage Title="{Binding Name}" IconImageSource="monkeyicon.png">
        <StackLayout Padding="5, 25">
          <Label Text="{Binding Name}" Font="Bold,Large" HorizontalOptions="Center" />
          <Image Source="{Binding PhotoUrl}" WidthRequest="200" HeightRequest="200" />
          <StackLayout Padding="50, 10">
            <StackLayout Orientation="Horizontal">
              <Label Text="Family:" HorizontalOptions="FillAndExpand" />
              <Label Text="{Binding Family}" Font="Bold,Medium" />
            </StackLayout>
            ...
          </StackLayout>
        </StackLayout>
      </ContentPage>
    </DataTemplate>
  </TabbedPage.ItemTemplate>
</TabbedPage>
View Code

通过在代码隐藏文件的构造函数中设置ItemsSource属性,用数据填充TabbedPage。

TabbedPage的重写文章参考:用Xamarin实现选项卡应用框架

TemplatedPage

TemplatedPage显示带有控件模板的全屏内容,并且是ContentPage的基类。参考控件模板

使用控件模板可清晰地将页面外观及其内容分离,从而能够创建主题明确的页面。

介绍

Xamarin.Forms控件模板提供了在运行时轻松地对应用程序页面进行主题设置和重新主题设置的功能。

控件具有不同的属性,例如BackgroundColor和TextColor,可以定义控件外观的各个方面,可以使用样式设置这些属性,可以在运行时更改样式以实现基本主题。但是,样式不能在页面的外观与其内容之间保持清晰的分隔,并且通过设置此类属性可以进行的更改受到限制。

控件模板在页面外观及其内容之间提供了清晰的分隔,因此可以创建易于主题化的页面。例如,一个应用程序可能包含提供深色主题和浅色主题的应用程序级控制模板。可以通过应用控制模板之一来主题化应用程序中的每个ContentPage,而无需更改每个页面显示的内容。此外,控件模板提供的主题不仅限于更改控件的属性,他们还可以更改用于实现主题的控件。

ControlTemplate指定页面或视图的外观,并包含根布局,在布局内还包含实现模板的控件。 通常,ControlTemplate将利用ContentPresenter来标记要在页面或视图中显示的内容出现的位置(占位符)。 然后,使用ControlTemplate的页面或视图将定义要由ContentPresenter显示的内容(替换那个占位符)。 

不同颜色主题设置多个ControlTemplate,使用这个模板的内容页中放置主题不变的部分。

通过设置控件模板的ControlTemplate属性,可以将它们应用于以下类型:

创建ControlTemplate并将其分配给这些类型后,所有现有外观都会替换为ControlTemplate中定义的外观。 此外,除了通过使用ControlTemplate属性设置外观外,还可以通过使用样式来应用控件模板以进一步扩展主题功能。

 注:什么是TemplatedPage和TemplatedView类型?

TemplatedPage是ContentPage的基类,并且是Xamarin.Forms提供的最基本的页面类型。 与ContentPage不同,TemplatedPage没有Content属性。 因此,内容不能直接添加到TemplatedPage实例,而是通过为TemplatedPage实例设置控件模板来添加内容。 同样,TemplatedView是ContentView的基类,与ContentView不同,TemplatedView没有Content属性,因此,内容不能直接添加到TemplatedView实例,而是通过为TemplatedView实例设置控件模板来添加内容。

可以在XAML和C#中创建控件模板:

  • 在XAML中创建的控件模板是在ResourceDictionary中定义的,该ResourceDictionary分配给页面的Resources集合,或更常见的是分配给应用程序的Resources集合。
  • 用C#创建的控件模板通常在页面的类中定义,或者在可以全局访问的类中定义。

选择在哪里定义ControlTemplate实例会影响可以在哪里使用它:

  • 在页面级别定义的ControlTemplate实例只能应用于页面。
  • 在应用程序级别定义的ControlTemplate实例可以应用于整个应用程序的页面。

视图层次结构中较低的控件模板优先于较高的控件模板。例如,在页面级别定义的名为DarkTheme的ControlTemplate将优先于在应用程序级别定义的同名模板。因此,应该在应用程序级别定义一个定义要应用于应用程序中每个页面的主题的控件模板。

创建控件模板

可以在应用程序级别或页面级别定义控件模板。

在XAML中创建ControlTemplate
若要在应用程序级别定义ControlTemplate,必须将ResourceDictionary添加到App类。 默认情况下,从模板创建的所有Xamarin.Forms应用程序都使用App类来实现Application子类。 若要在应用程序级别声明ControlTemplate,在使用XAML的应用程序的ResourceDictionary中,必须将默认的App类替换为XAML App类并在其后面添加相关代码,如以下代码示例所示:

<Application xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="SimpleTheme.App">
    <Application.Resources>
        <ResourceDictionary>
            <ControlTemplate x:Key="TealTemplate">
                <Grid>
                    ...
                    <BoxView ... />
                    <Label Text="Control Template Demo App"
                           TextColor="White"
                           VerticalOptions="Center" ... />
                    <ContentPresenter ... />
                    <BoxView Color="Teal" ... />
                    <Label Text="(c) Xamarin 2016"
                           TextColor="White"
                           VerticalOptions="Center" ... />
                </Grid>
            </ControlTemplate>
            <ControlTemplate x:Key="AquaTemplate">
                ...
            </ControlTemplate>
        </ResourceDictionary>
    </Application.Resources>
</Application>
View Code

每个ControlTemplate实例都在ResourceDictionary中创建为可重用对象,这是通过为每个声明赋予唯一的x:Key属性来实现的,该属性在ResourceDictionary中为其提供了描述性的键。

以下代码示例显示了将TealTemplate应用于ContentView的ContentPage:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="SimpleTheme.HomePage">
    <ContentView x:Name="contentView" Padding="0,20,0,0"
                 ControlTemplate="{StaticResource TealTemplate}">
        <StackLayout VerticalOptions="CenterAndExpand">
            <Label Text="Welcome to the app!" HorizontalOptions="Center" />
            <Button Text="Change Theme" Clicked="OnButtonClicked" />
        </StackLayout>
    </ContentView>
</ContentPage>
View Code

通过使用StaticResource标记扩展,将TealTemplate分配给ContentView.ControlTemplate属性。 ContentView.Content属性设置为StackLayout,用于定义要在ContentPage上显示的内容,该内容将由TealTemplate中包含的ContentPresenter显示。 

在运行时重新定义应用程序
单击“更改主题”按钮将执行OnButtonClicked方法,如以下代码示例所示:

void OnButtonClicked (object sender, EventArgs e)
{
  originalTemplate = !originalTemplate;
  contentView.ControlTemplate = (originalTemplate) ? tealTemplate : aquaTemplate;
}
View Code

注:在ContentPage上,可以分配Content属性,也可以设置ControlTemplate属性。 发生这种情况时,如果ControlTemplate包含ContentPresenter实例,则ContentPresenter将在ControlTemplate中呈现分配给Content属性的内容

Set a ControlTemplate with a style

还可以通过样式应用ControlTemplate来进一步扩展主题功能。 这可以通过在ResourceDictionary中为目标视图创建隐式或显式样式,并在Style实例中设置目标视图的ControlTemplate属性来实现。 以下代码示例显示了已添加到应用程序级别ResourceDictionary的隐式样式:

<Style TargetType="ContentView">
    <Setter Property="ControlTemplate" Value="{StaticResource TealTemplate}" />
</Style>

由于Style实例是隐式的,因此它将应用于应用程序中的所有ContentView实例。 因此,不再需要设置ContentView.ControlTemplate属性。

从模板获取命名元素
实例化模板后,可以在控件模板中检索命名元素,这可以通过GetTemplateChild方法实现,该方法返回实例化的ControlTemplate可视树中的命名元素。

实例化控件模板后,将调用模板的OnApplyTemplate方法。 因此,应该从TemplatedPage派生页面(例如ContentPage)或TemplatedView派生视图(例如ContentView)中的OnApplyTemplate重写中调用GetTemplateChild方法。

注:仅在调用OnApplyTemplate方法之后才应调用GetTemplateChild方法

以下示例显示了自定义控件的控件模板:

<controls:MyCustomControl ...>
    <controls:MyCustomControl.ControlTemplate>
         <ControlTemplate>
              <Label x:Name="myLabel" />
         </ControlTemplate>
    <controls:MyCustomControl.ControlTemplate>
</controls:MyCustomControl>
View Code

Label元素已命名,因此可以在自定义控件的代码隐藏区中进行检索。 这是通过从OnApplyTemplate重写中的自定义控件调用GetTemplateChild方法实现的:

class MyCustomControl : ContentView
{
    Label myLabel;

    protected override void OnApplyTemplate()
    {  
        myLabel = GetTemplateChild("myLabel");
    }
    //...
}
View Code

模板绑定 

模板绑定允许控件模板中的控件将数据绑定到公共属性,从而可以轻松更改控件模板中控件的属性值。 本文演示了如何使用模板绑定从控件模板执行数据绑定。

TemplateBinding用于将控件模板中的控件属性绑定到拥有该控件模板的目标视图的父级上的可绑定属性。 例如,您可以使用模板绑定将Label.Text属性绑定到定义要显示的文本的可绑定属性,而不是定义ControlTemplate内部Label实例显示的文本。

TemplateBinding与现有的Binding类似,不同之处在于TemplateBinding的源始终自动设置为拥有控件模板的目标视图的父级。 但是,请注意,不支持在ControlTemplate之外使用TemplateBinding。

在XAML中创建TemplateBinding
在XAML中,使用TemplateBinding标记扩展来创建TemplateBinding,如以下代码示例所示:

<ControlTemplate x:Key="TealTemplate">
  <Grid>
    ...
    <Label Text="{TemplateBinding Parent.HeaderText}" ... />
    ...
    <Label Text="{TemplateBinding Parent.FooterText}" ... />
  </Grid>
</ControlTemplate>
View Code

除了将Label.Text属性设置为静态文本外,这些属性还可以使用模板绑定来绑定到拥有ControlTemplate的目标视图的父级上的可绑定属性。 但是,请注意,模板绑定绑定到Parent.HeaderText和Parent.FooterText,而不是HeaderText和FooterText。 这是因为在此示例中,可绑定属性是在目标视图的祖父母(而不是父视图)上定义的,查看官网示例。

模板绑定的源总是自动设置为拥有控件模板的目标视图的父级,这里是ContentView实例。 模板绑定使用Parent属性返回ContentView实例(即ContentPage实例)的父元素。 因此,在ControlTemplate中使用TemplateBinding绑定到Parent.HeaderText和Parent.FooterText可以找到在ContentPage上定义的可绑定属性。

 

CarouselPage

CarouselPage派生自抽象的MultiPage类,并允许通过手指滑动在子页面之间导航,将Children属性设置为ContentPage对象的集合,或者将ItemsSource属性设置为数据对象的集合,并将ItemTemplate属性设置为DataTemplate,以描述如何直观地表示每个对象。

 

posted @ 2019-11-02 15:36  peterYong  阅读(4216)  评论(0编辑  收藏  举报