所謂 UWP 樣式的漢堡菜單,我曾在“張高興的 UWP 開發筆記:漢堡菜單進階”里說過,也就是使用 Segoe MDL2 Assets 字體作為左側 Icon,并且左側使用填充顏色的矩形用來表示 ListView 的選中。如下圖
但怎樣通過 Xamarin.Forms ,將這一樣式的漢堡菜單帶入到 Android 與 iOS 中呢?
一、大綱-細節模式簡介
講代碼前首先來說說這種導航模式,官方稱“大綱-細節模式”(MasterDetail)。左側的漢堡菜單稱為“大綱”(Master),右側的頁面稱為“細節”(Detail)。Xamarin.Froms 為項目提供了若干種導航模式,“大綱-細節”為其中一種。
二、項目簡介
效果圖:
不多說廢話,看代碼實在些。
本示例是使用 Visual Studio 2017 創建的 Cross-Platform 項目,項目名為”HamburgerMenuDemo“,模板為空白項目。(GitHub:https://github.com/ZhangGaoxing/xamarin-forms-demo/tree/master/HamburgerMenuDemo)
待項目創建完成后,解決方案共包含四個項目:共享代碼項目、 Android 項目、 iOS 項目、 UWP 項目。共享代碼項目為存放共享頁面的地方,個人覺得和類庫還是有點區別的。
三、共享代碼項目 HamburgerMenuDemo
首先添加幾個頁面,根目錄下添加一個 MasterPage.xaml 頁面,用于”大綱視圖“。添加一個 Views 文件夾,用于存放子頁面,向其中添加3個界面:Page1、Page2、Page3。添加一個 MasterPageItem.cs 類。
1. MasterPageItem.cs
和 UWP 的漢堡菜單一樣,首先要創建一個類,作為導航的項目,用來綁定 ListView 。名字叫 MasterPageItem.cs 。
里面的屬性有頁面的標題 Title,左側的圖標 Icon,圖標的字體 FontFamily,目的頁面 DestPage,還有左側的矩形顯示 Selected 與 顏色 Color。由于要實現雙向綁定,還要實現接口 INotifyPropertyChanged。要注意的是,Color 類型為 Xamarin.Forms 中的。
代碼如下
public class MasterPageItem : INotifyPropertyChanged { // 字體路徑,用于引入 Segoe MDL2 Assets 字體 public string FontFamily { get; set; } // 字體圖標轉義 public string Icon { get; set; } // 標題 public string Title { get; set; } // 目的頁 public Type DestPage { get; set; } // 用于顯示左側填充矩形,雙向綁定 private bool selected = false; public bool Selected { get { return selected; } set { selected = value; this.OnPropertyChanged("Selected"); } } // 選中顏色,雙向綁定 ( using Xamarin.Forms ) private Color color = new Color(); public Color Color { get { return color; } set { color = value; this.OnPropertyChanged("Color"); } } public event PropertyChangedEventHandler PropertyChanged; public void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }
2. MasterPage.xaml
MasterPage 為”大綱“視圖,即左側顯示 ListView 的頁面。本項目的 MasterPage 分為兩欄,分一級菜單與二級菜單,即置頂一個 ListView 與置底一個 ListView 。 ListView 的 ItemTemplate 與 UWP 稍有不同,左側的填充矩形換成了 BoxView,二級菜單的上邊線由 Border 換成了高度為1的 BoxView。代碼如下
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="HamburgerMenuDemo.MasterPage" Icon="hamburger.png" Title=" "> <Grid> <Grid.RowDefinitions> <RowDefinition Height="*" /> <RowDefinition Height="1" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <!--一級菜單--> <ListView x:Name="PrimaryListView" VerticalOptions="StartAndExpand" SeparatorVisibility="None"> <ListView.ItemTemplate> <DataTemplate> <ViewCell> <Grid HeightRequest="48"> <Grid.ColumnDefinitions> <ColumnDefinition Width="48"/> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions> <BoxView BackgroundColor="{Binding Color}" WidthRequest="5" HeightRequest="26" HorizontalOptions="Start" VerticalOptions="Center" IsVisible="{Binding Selected}" /> <Label Text="{Binding Icon}" FontFamily="{Binding FontFamily}" TextColor="{Binding Color}" FontSize="16" HorizontalOptions="Center" VerticalOptions="Center" /> <Label Grid.Column="1" Text="{Binding Title}" TextColor="{Binding Color}" FontSize="14" VerticalOptions="Center" /> </Grid> </ViewCell> </DataTemplate> </ListView.ItemTemplate> </ListView> <!--BoxView 充當 Border--> <BoxView BackgroundColor="Gray" Grid.Row="1" HorizontalOptions="FillAndExpand" /> <!--二級菜單--> <ListView x:Name="SecondaryListView" Grid.Row="2" VerticalOptions="End" SeparatorVisibility="None" Margin="0,-6,0,0"> <ListView.ItemTemplate> <DataTemplate> <ViewCell> <Grid HeightRequest="48"> <Grid.ColumnDefinitions> <ColumnDefinition Width="48"/> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions> <BoxView BackgroundColor="{Binding Color}" WidthRequest="5" HeightRequest="26" HorizontalOptions="Start" VerticalOptions="Center" IsVisible="{Binding Selected}" /> <Label x:Name="IconLabel" Text="{Binding Icon}" FontFamily="{Binding FontFamily}" TextColor="{Binding Color}" FontSize="16" HorizontalOptions="Center" VerticalOptions="Center" /> <Label Grid.Column="1" Text="{Binding Title}" TextColor="{Binding Color}" FontSize="14" VerticalOptions="Center" /> </Grid> </ViewCell> </DataTemplate> </ListView.ItemTemplate> </ListView> </Grid> </ContentPage>
MasterPage.xaml.cs 代碼也需要講下,不知是怎么回事,以上 Xaml 代碼直接運行時兩個菜單會顯示不正常,只顯示一個菜單,<RowDefinition Height="Auto" /> 在這個 ContentPage 里好像無效。因此我在后臺代碼設置了二級菜單的高度,也就是48 * secondaryItems.Count。兩個 ListView 需要通過屬性的方式,向 MainPage 傳遞控件。字體路徑各個項目不同,需要單獨設置,我后面會說。MasterPage.xaml.cs 代碼如下
[XamlCompilation(XamlCompilationOptions.Compile)] public partial class MasterPage : ContentPage { // 向 MainPage 傳遞控件 public ListView primaryListView { get { return PrimaryListView; } } public ListView secondaryListView { get { return SecondaryListView; } } public MasterPage() { InitializeComponent(); // 設置不同平臺的字體路徑 string fontFamily; switch (Device.RuntimePlatform) { case "Android": fontFamily = "segmdl2.ttf#Segoe MDL2 Assets"; break; case "iOS": fontFamily = "Segoe MDL2 Assets"; break; case "Windows": fontFamily = "/Assets/segmdl2.ttf#Segoe MDL2 Assets"; break; case "WinPhone": fontFamily = "/Assets/segmdl2.ttf#Segoe MDL2 Assets"; break; default: fontFamily = "segmdl2.ttf#Segoe MDL2 Assets"; break; } // 列表項 var primaryItems = new List<MasterPageItem>() { new MasterPageItem { Title = "Page1", FontFamily = fontFamily, Icon = "\xE10F", Color = Color.DeepSkyBlue, Selected = true, DestPage = typeof(Page1) }, new MasterPageItem { Title = "Page2", FontFamily = fontFamily, Icon = "\xE11F", Color = Color.Black, Selected = false, DestPage = typeof(Page2) }, new MasterPageItem { Title = "Page3", FontFamily = fontFamily, Icon = "\xE12F", Color = Color.Black, Selected = false, DestPage = typeof(Page2) } }; var secondaryItems = new List<MasterPageItem>() { new MasterPageItem { Title = "設置", FontFamily = fontFamily, Icon = "\xE713", Color = Color.Black, Selected = false, DestPage = typeof(SettingPage) }, new MasterPageItem { Title = "關于", FontFamily = fontFamily, Icon = "\xE783", Color = Color.Black, Selected = false, DestPage = typeof(AboutPage) } }; // ListView 數據綁定 PrimaryListView.ItemsSource = primaryItems; SecondaryListView.ItemsSource = secondaryItems; // 設置二級菜單高度 SecondaryListView.HeightRequest = 48 * secondaryItems.Count; } }
3. MainPage.xaml
下面來修改一下 MainPage.xaml 。MainPage.xaml 為應用的入口頁面,可在 App.xaml.cs 中更改。將 MainPage 中的根元素替換為 MasterDetailPage 。注釋很詳細,不多說了
<MasterDetailPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:local="clr-namespace:HamburgerMenuDemo" x:Class="HamburgerMenuDemo.MainPage" xmlns:views="clr-namespace:HamburgerMenuDemo.Views"> <!--大綱視圖--> <MasterDetailPage.Master> <!--引入 MasterPage 并給個名稱,用于后臺設置 MasterPage 傳遞過來的 ListView--> <local:MasterPage x:Name="masterPage" /> </MasterDetailPage.Master> <!--細節視圖--> <MasterDetailPage.Detail> <NavigationPage> <x:Arguments> <!--默認顯示的頁面--> <views:Page1 /> </x:Arguments> </NavigationPage> </MasterDetailPage.Detail> </MasterDetailPage>
同樣的 MainPage.xaml.cs 中的代碼也很簡單,注釋很詳細
public MainPage() { InitializeComponent(); // ListView 點擊事件 masterPage.primaryListView.ItemSelected += MasterPageItemSelected; masterPage.secondaryListView.ItemSelected += MasterPageItemSelected; // 設置 Windows 平臺的“大綱”顯示模式為折疊 if (Device.RuntimePlatform == Device.Windows) { MasterBehavior = MasterBehavior.Popover; } } private void MasterPageItemSelected(object sender, SelectedItemChangedEventArgs e) { var item = e.SelectedItem as MasterPageItem; if (item != null) { // 遍歷 ListView 數據源,將選中項矩形顯示,字體顏色設置成未選中 foreach (MasterPageItem mpi in masterPage.primaryListView.ItemsSource) { mpi.Selected = false; mpi.Color = Color.Black; } foreach (MasterPageItem mpi in masterPage.secondaryListView.ItemsSource) { mpi.Selected = false; mpi.Color = Color.Black; } // 設置選中項 item.Selected = true; item.Color = Color.DeepSkyBlue; // 跳轉 Detail = new NavigationPage((Page)Activator.CreateInstance(item.DestPage)); // 取消 ListView 默認選中樣式 masterPage.primaryListView.SelectedItem = null; masterPage.secondaryListView.SelectedItem = null; // 關閉“大綱” IsPresented = false; } }
要注意的是 MasterPage.xaml 頁面中的 Title 一定要給,要不然會報錯,可以在后臺 cs 文件中修改 Title 屬性,也可以在 Xaml 根元素中修改 Title。Views 中的幾個頁面 Title 不給可以,但標題欄不會顯示頁面的 Title,不好看。
四、Android 項目 HamburgerMenuDemo.Android
1. 字體設置
將 segmdl2.ttf 字體文件直接放入 Assets 文件夾下即可
2. 修改 style.xml
”大綱“的默認效果是 DrawerLayout 覆蓋狀態欄的,不太美觀,需要修改樣式。在 style.xml 中添加
<item name="android:fitsSystemWindows">true</item>
同時,由于修改了樣式,變成了狀態欄覆蓋 DrawerLayout ,需要給 MasterPage.xaml 中的根 Grid 賦值一個 Padding="0,25,0,-6",但 UWP 項目卻不需要,這點我會在文末給出代碼。
五、iOS 項目 HamburgerMenuDemo.iOS
1. 字體設置
弄了好久,Xamarin 太坑了,plist 的編輯器很不和諧。。。
(1)將 segmdl2.ttf 字體文件直接放入 Resources 文件夾
(2)更改 segmdl2.ttf 屬性,復制到輸出目錄 =》 始終復制,生成操作 =》 BundleResource
(2)不要雙擊,右擊 Info.plist ,查看代碼,添加如下內容
<dict> <key>UIAppFonts</key> <array> <string>segmdl2.ttf</string> </array> </dict>
如果要添加其他的資源,可以自己新建一個 .plist 文件,新建的文件是正常顯示資源列表的,添加完成后,復制代碼到 Info.plist 即可。
2. Padding
和安卓一樣,需要給 MasterPage.xaml 中的根 Grid 賦值一個 Padding="0,20,0,-6",我會在文末給出代碼。
六、Padding 代碼
在 MasterPage.xaml 添加如下代碼
<!--安卓空出狀態欄的寬度--> <ContentPage.Resources> <ResourceDictionary> <OnPlatform x:Key="padding" x:TypeArguments="Thickness" iOS="0,20,0,-6" Android="0,25,0,-6" WinPhone="0" /> </ResourceDictionary> </ContentPage.Resources>
別忘了在 Grid 中引用資源
Padding="{StaticResource padding}"
MasterPage.xaml 最終代碼
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="HamburgerMenuDemo.MasterPage" Icon="hamburger.png" Title=" "> <!--安卓空出狀態欄的寬度--> <ContentPage.Resources> <ResourceDictionary> <OnPlatform x:Key="padding" x:TypeArguments="Thickness" iOS="0,20,0,0" Android="0,20,0,0" WinPhone="0" /> </ResourceDictionary> </ContentPage.Resources> <Grid Padding="{StaticResource padding}"> <Grid.RowDefinitions> <RowDefinition Height="*" /> <RowDefinition Height="1" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <!--一級菜單--> <ListView x:Name="PrimaryListView" VerticalOptions="StartAndExpand" SeparatorVisibility="None"> <ListView.ItemTemplate> <DataTemplate> <ViewCell> <Grid HeightRequest="48"> <Grid.ColumnDefinitions> <ColumnDefinition Width="48"/> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions> <BoxView BackgroundColor="{Binding Color}" WidthRequest="5" HeightRequest="26" HorizontalOptions="Start" VerticalOptions="Center" IsVisible="{Binding Selected}" /> <Label Text="{Binding Icon}" FontFamily="{Binding FontFamily}" TextColor="{Binding Color}" FontSize="16" HorizontalOptions="Center" VerticalOptions="Center" /> <Label Grid.Column="1" Text="{Binding Title}" TextColor="{Binding Color}" FontSize="14" VerticalOptions="Center" /> </Grid> </ViewCell> </DataTemplate> </ListView.ItemTemplate> </ListView> <!--BoxView 充當 Border--> <BoxView BackgroundColor="Gray" Grid.Row="1" HorizontalOptions="FillAndExpand" /> <!--二級菜單--> <ListView x:Name="SecondaryListView" Grid.Row="2" VerticalOptions="End" SeparatorVisibility="None" Margin="0,-6,0,0"> <ListView.ItemTemplate> <DataTemplate> <ViewCell> <Grid HeightRequest="48"> <Grid.ColumnDefinitions> <ColumnDefinition Width="48"/> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions> <BoxView BackgroundColor="{Binding Color}" WidthRequest="5" HeightRequest="26" HorizontalOptions="Start" VerticalOptions="Center" IsVisible="{Binding Selected}" /> <Label x:Name="IconLabel" Text="{Binding Icon}" FontFamily="{Binding FontFamily}" TextColor="{Binding Color}" FontSize="16" HorizontalOptions="Center" VerticalOptions="Center" /> <Label Grid.Column="1" Text="{Binding Title}" TextColor="{Binding Color}" FontSize="14" VerticalOptions="Center" /> </Grid> </ViewCell> </DataTemplate> </ListView.ItemTemplate> </ListView> </Grid> </ContentPage>
文章列表