1. 程式人生 > >張高興的 Xamarin.Forms 開發筆記:為 Android 與 iOS 引入 UWP 風格的漢堡菜單 ( MasterDetailPage )

張高興的 Xamarin.Forms 開發筆記:為 Android 與 iOS 引入 UWP 風格的漢堡菜單 ( MasterDetailPage )

操作 using eat stp 取消 height 新建 屬性 turn

  所謂 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>

張高興的 Xamarin.Forms 開發筆記:為 Android 與 iOS 引入 UWP 風格的漢堡菜單 ( MasterDetailPage )