Win10 UWP 開發系列:使用SplitView實現漢堡菜單及頁面內導航


在Win10之前,WP平台的App主要有樞軸和全景兩種導航模式,我個人更喜歡Pivot即樞軸模式,可以左右切換,非常方便。全景視圖因為對設計要求比較高,自己總是做不出好的效果。對於一般的新聞閱讀類App來說,Pivot更適合多個頻道的展示,因為內容基本都是一樣的。

到了Win10,微軟模仿其他平台也推出了漢堡菜單,但並沒有提供現成的控件,而是需要開發者通過一個名為SplitView的控件來實現。我個人並不覺得左上角的菜單有多么方便,漢堡菜單的使用必然會改變以前的導航模式,比如以前底部的AppBar使用很頻繁,現在可以通過漢堡菜單的按鈕來切換不同的頁面。因此之前的App的導航模式需要重新設計。

假設有A、B、C三個平行的頁面,可以在每個頁面的左側都放個漢堡菜單,也可以像web的框架頁一樣,做一個殼,漢堡菜單只放在外面的框架里,點擊不同的按鈕,在content里實現不同頁面的導航。我比較傾向第二種,之前在做澎湃新聞uwp的時候就使用了這種方式,后來看了下Template10的模板,也是用的這種方式,在主頁面外層套了一個Frame,而且還實現 了一個漢堡菜單控件。有興趣的同學可以參考Template10來快速生成一個帶漢堡菜單的基礎App,Github地址:https://github.com/Windows-XAML/Template10 ,這個項目還帶了很多好東西,比如一些常用的幫助類和一些behavior等,值得uwp開發者好好學習。

我沒有直接使用T10的模板,以下介紹的還是當時使用MVVM-Sidekick框架實現的頁面內導航。

首先通過MVVM-Sidekick提供的項目模板來新建一個UWP項目,命名為NavDemo。

考慮我們要實現的目的:在主頁面放置一個漢堡菜單,在右側的content中實現不同頁面的導航。

先來看一下效果:

PC版:

手機版:

一、創建菜單項類

漢堡菜單每個選項一般是由一個圖標和一個文字組成,我還是使用FontAwesomeFont這個字體來顯示圖標,如何使用這個字體來做圖標,可參考我之前的blog。首先建立一個菜單的類NavMenuItem,放在Models目錄下,使用provm代碼段生成兩個屬性:

public class NavMenuItem : BindableBase<NavMenuItem>

{

/// <summary>

/// FontAwesomeFontFamily

/// </summary>

public string Glyph

{

get { return _GlyphLocator(this).Value; }

set { _GlyphLocator(this).SetValueAndTryNotify(value); }

}

#region Property string Glyph Setup

protected Property<string> _Glyph = new Property<string> { LocatorFunc = _GlyphLocator };

static Func<BindableBase, ValueContainer<string>> _GlyphLocator = RegisterContainerLocator<string>("Glyph", model => model.Initialize("Glyph", ref model._Glyph, ref _GlyphLocator, _GlyphDefaultValueFactory));

static Func<string> _GlyphDefaultValueFactory = () => { return default(string); };

#endregion

 

/// <summary>

///文字

/// </summary>

public string Label

{

get { return _LabelLocator(this).Value; }

set { _LabelLocator(this).SetValueAndTryNotify(value); }

}

#region Property string Label Setup

protected Property<string> _Label = new Property<string> { LocatorFunc = _LabelLocator };

static Func<BindableBase, ValueContainer<string>> _LabelLocator = RegisterContainerLocator<string>("Label", model => model.Initialize("Label", ref model._Label, ref _LabelLocator, _LabelDefaultValueFactory));

static Func<string> _LabelDefaultValueFactory = () => { return default(string); };

#endregion

 

}

 

打開NavDemo\ViewModels\MainPage_Model.cs,使用propvm代碼段生成一個列表:

public ObservableCollection<NavMenuItem> NavMenuItemList

{

get { return _NavMenuItemListLocator(this).Value; }

set { _NavMenuItemListLocator(this).SetValueAndTryNotify(value); }

}

#region Property ObservableCollection<HamburgerMenuItem> NavMenuItemList Setup

protected Property<ObservableCollection<NavMenuItem>> _NavMenuItemList = new Property<ObservableCollection<NavMenuItem>> { LocatorFunc = _NavMenuItemListLocator };

static Func<BindableBase, ValueContainer<ObservableCollection<NavMenuItem>>> _NavMenuItemListLocator = RegisterContainerLocator<ObservableCollection<NavMenuItem>>("NavMenuItemList", model => model.Initialize("NavMenuItemList", ref model._NavMenuItemList, ref _NavMenuItemListLocator, _NavMenuItemListDefaultValueFactory));

static Func<ObservableCollection<NavMenuItem>> _NavMenuItemListDefaultValueFactory = () => default(ObservableCollection<NavMenuItem>);

#endregion

 

在vm的構造函數里,添加幾個項:

public MainPage_Model()

{

if (IsInDesignMode )

{

Title = "Title is a little different in Design mode";

}

NavMenuItemList = new ObservableCollection<NavMenuItem>();

NavMenuItemList.Add(new NavMenuItem { Glyph = "\uf015", Label = "首頁" });

NavMenuItemList.Add(new NavMenuItem { Glyph = "\uf002", Label = "搜索" });

NavMenuItemList.Add(new NavMenuItem { Glyph = "\uf05a", Label = "關於" });

}

 

注意Glyph的賦值方式。

 

 二、顯示漢堡菜單

在項目中新建Resources目錄,把FontAwesome.otf字體文件放在里面。在項目中新建CustomTheme目錄,然后建立自定義的樣式資源文件CustomStyles.xaml,代碼如下:

<ResourceDictionary

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

xmlns:local="using:NavDemo">

<FontFamily x:Key="FontAwesomeFontFamily">/Resources/FontAwesome.otf#FontAwesome</FontFamily>

 

<Style x:Key="SplitViewTogglePaneButtonStyle" TargetType="ToggleButton">

<Setter Property="FontSize" Value="20" />

<Setter Property="FontFamily" Value="{ThemeResource SymbolThemeFontFamily}" />

<Setter Property="MinHeight" Value="48" />

<Setter Property="MinWidth" Value="48" />

<Setter Property="Margin" Value="0" />

<Setter Property="Padding" Value="0" />

<Setter Property="HorizontalAlignment" Value="Left" />

<Setter Property="VerticalAlignment" Value="Top" />

<Setter Property="HorizontalContentAlignment" Value="Center" />

<Setter Property="VerticalContentAlignment" Value="Center" />

<Setter Property="Background" Value="Transparent" />

<Setter Property="Foreground" Value="{ThemeResource SystemControlForegroundBaseHighBrush}" />

<Setter Property="Content" Value="&#xE700;" />

<Setter Property="AutomationProperties.Name" Value="Menu" />

<Setter Property="UseSystemFocusVisuals" Value="True"/>

<Setter Property="Template">

<Setter.Value>

<ControlTemplate TargetType="ToggleButton">

<Grid Background="{TemplateBinding Background}" x:Name="LayoutRoot">

<VisualStateManager.VisualStateGroups>

<VisualStateGroup x:Name="CommonStates">

<VisualState x:Name="Normal" />

<VisualState x:Name="PointerOver">

<Storyboard>

<ObjectAnimationUsingKeyFrames Storyboard.TargetName="LayoutRoot" Storyboard.TargetProperty="(Grid.Background)">

<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlHighlightListLowBrush}"/>

</ObjectAnimationUsingKeyFrames>

<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">

<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlHighlightAltBaseHighBrush}"/>

</ObjectAnimationUsingKeyFrames>

</Storyboard>

</VisualState>

<VisualState x:Name="Pressed">

<Storyboard>

<ObjectAnimationUsingKeyFrames Storyboard.TargetName="LayoutRoot" Storyboard.TargetProperty="(Grid.Background)">

<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlHighlightListMediumBrush}"/>

</ObjectAnimationUsingKeyFrames>

<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">

<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlHighlightAltBaseHighBrush}"/>

</ObjectAnimationUsingKeyFrames>

</Storyboard>

</VisualState>

<VisualState x:Name="Disabled">

<Storyboard>

<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="(TextBlock.Foreground)">

<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlDisabledBaseLowBrush}"/>

</ObjectAnimationUsingKeyFrames>

</Storyboard>

</VisualState>

<VisualState x:Name="Checked"/>

<VisualState x:Name="CheckedPointerOver">

<Storyboard>

<ObjectAnimationUsingKeyFrames Storyboard.TargetName="LayoutRoot" Storyboard.TargetProperty="(Grid.Background)">

<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlHighlightListLowBrush}"/>

</ObjectAnimationUsingKeyFrames>

<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">

<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlHighlightAltBaseHighBrush}"/>

</ObjectAnimationUsingKeyFrames>

</Storyboard>

</VisualState>

<VisualState x:Name="CheckedPressed">

<Storyboard>

<ObjectAnimationUsingKeyFrames Storyboard.TargetName="LayoutRoot" Storyboard.TargetProperty="(Grid.Background)">

<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlHighlightListMediumBrush}"/>

</ObjectAnimationUsingKeyFrames>

<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">

<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlHighlightAltBaseHighBrush}"/>

</ObjectAnimationUsingKeyFrames>

</Storyboard>

</VisualState>

<VisualState x:Name="CheckedDisabled">

<Storyboard>

<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="(TextBlock.Foreground)">

<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlDisabledBaseLowBrush}"/>

</ObjectAnimationUsingKeyFrames>

</Storyboard>

</VisualState>

</VisualStateGroup>

</VisualStateManager.VisualStateGroups>

<ContentPresenter x:Name="ContentPresenter"

Content="{TemplateBinding Content}"

Margin="{TemplateBinding Padding}"

HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"

VerticalAlignment="{TemplateBinding VerticalContentAlignment}"

AutomationProperties.AccessibilityView="Raw" />

</Grid>

</ControlTemplate>

</Setter.Value>

</Setter>

</Style>

</ResourceDictionary>

 

然后打開App.xaml文件,把這個資源引用進來:

<Application.Resources>

<ResourceDictionary>

<ResourceDictionary.MergedDictionaries>

<ResourceDictionary Source="CustomTheme/CustomStyles.xaml"/>

</ResourceDictionary.MergedDictionaries>

</ResourceDictionary>

</Application.Resources>

 

樣式資源文件里主要定義了兩個樣式,一是定義了FontAwesomeFontFamily字體,二是定義了一個針對ToggleButton的按鈕樣式SplitViewTogglePaneButtonStyle,作為漢堡菜單的開關。這個開關鍵為什么要設置高度為48呢?參考https://msdn.microsoft.com/zh-cn/library/windows/apps/dn997787.aspx

拆分視圖控件具有一個可展開/可折疊的窗格和一個內容區域。內容區域始終可見。窗格可以展開和折疊或停留在打開狀態,而且可以從應用窗口的左側或右側顯示其自身。窗格中有三種模式:

  • 覆蓋

    在打開之前隱藏窗格。在打開時,窗格覆蓋內容區域。

  • 內聯

    窗格始終可見,並且不會覆蓋內容區域。窗格和內容區域划分可用的屏幕實際使用面積。

  • 精簡

    在此模式下窗格始終可見,它僅足夠寬以顯示圖標(通常 48 epx 寬)。窗格和內容區域划分可用的屏幕實際使用面積。盡管標准精簡模式不覆蓋內容區域,但它可以轉化為更寬的窗格來顯示更多內容,這將覆蓋該內容區域。

 

所以我就根據官方文檔設置為48了。

修改MainPage.xaml,把根Grid改為以下代碼:

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" DataContext="{StaticResource DesignVM}">

<!-- Top-level navigation menu + app content -->

<SplitView x:Name="RootSplitView" IsPaneOpen="True"

DisplayMode="Inline"

OpenPaneLength="256"

IsTabStop="False">

<SplitView.Pane>

<!-- A custom ListView to display the items in the pane. The automation Name is set in the ContainerContentChanging event. -->

 

<ListView ItemsSource="{Binding NavMenuItemList}">

</ListView>

</SplitView.Pane>

 

 

<SplitView.Content>

<Frame x:Name="mainFrame">

</Frame>

</SplitView.Content>

</SplitView>

 

<!-- Declared last to have it rendered above everything else, but it needs to be the first item in the tab sequence. -->

<ToggleButton x:Name="TogglePaneButton"

TabIndex="1"

Style="{StaticResource SplitViewTogglePaneButtonStyle}"

IsChecked="{Binding IsPaneOpen, ElementName=RootSplitView, Mode=TwoWay}"

 

AutomationProperties.Name="Menu"

ToolTipService.ToolTip="Menu" />

</Grid>

 

為了方便查看菜單展開的效果,暫時先把IsPaneOpen屬性設置為true,OpenPaneLength設置的是菜單展開后的寬度。在Pane里放一個ListView,ItemSource綁定到之前做好的NavMenuItemList上。SplitView的Content設置為一個Frame,用來展示右側的頁面。

 

注意,如果當SplitView的Content直接設置為Frame的時候,也就是把外層的<SplitView.Content>去掉后,會報一個錯:

這個錯誤可以不用理會,程序是可以正常運行的。

 

此外 還要有一個按鈕來控制菜單的展開關閉狀態,用一個ToggleButton來實現,這個按鈕的圖標一般是三個橫杠,設置其Style為SplitViewTogglePaneButtonStyle即可。

然后,還要設置ListView的項模板,可以使用Blend來設計項模板,但因為這個比較簡單,我就直接手寫了,在Resources目錄下添加一個資源文件CustomDataTemplates.xaml,項目所有的自定義模板都可以寫在這里,代碼如下:

<ResourceDictionary

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

xmlns:Interactivity="using:Microsoft.Xaml.Interactivity"

xmlns:Core="using:Microsoft.Xaml.Interactions.Core"

xmlns:Behaviors="using:MVVMSidekick.Behaviors">

 

<DataTemplate x:Key="NavMenuItemTemplate" >

<Grid>

<Grid.ColumnDefinitions>

<ColumnDefinition MinWidth="48" />

<ColumnDefinition />

</Grid.ColumnDefinitions>

<FontIcon x:Name="Glyph" FontFamily="{StaticResource FontAwesomeFontFamily}" FontSize="16" Margin="0" Glyph="{Binding Glyph}" VerticalAlignment="Center" HorizontalAlignment="Center" ToolTipService.ToolTip="{Binding Label}"/>

<TextBlock x:Name="Text" Grid.Column="1" Text="{Binding Label}" VerticalAlignment="Center"/>

</Grid>

</DataTemplate>

</ResourceDictionary>

 

在這里定義一個項模板NavMenuItemTemplate,在里面放一個FontIcon,把Glyph屬性綁定到NavMenuItem的Glyph屬性,當然不要忘了把FontFamily設置為我們在自定義樣式里定義好的FontAwesomeFontFamily,不然是不會生效的。

再把這個項模板應用到頁面的ListView控件上:

ItemTemplate="{StaticResource NavMenuItemTemplate}"

 

現在跑一下試試,報錯了:

原來忘了把剛才的模板文件引入進來,修改App.xaml,修改為以下的樣子:

<Application.Resources>

<ResourceDictionary>

<ResourceDictionary.MergedDictionaries>

<ResourceDictionary Source="CustomTheme/CustomStyles.xaml"/>

<ResourceDictionary Source="Resources/CustomDataTemplates.xaml" />

</ResourceDictionary.MergedDictionaries>

</ResourceDictionary>

</Application.Resources>

 

現在可以運行了:

貌似左上角的按鈕跟ListView重疊了,這樣可不好看。

 

三、調整顯示效果

左上角的按鈕應用了SplitViewTogglePaneButtonStyle樣式,最小高度為48,把ListView往下移動一點,添加一個Margin屬性,頂部把開關按鈕的空間空出來:

<ListView Margin="0,48,0,0" ItemsSource="{Binding NavMenuItemList}"

ItemTemplate="{StaticResource NavMenuItemTemplate}">

現在列表位置正常了,但圖標的位置貌似還是偏右了,那就再給ListView設置ItemContainerStyle樣式,在CustomStyles.xaml文件里添加以下代碼:

<Style x:Key="NavMenuItemContainerStyle" TargetType="ListViewItem">

<Setter Property="MinWidth" Value="{StaticResource SplitViewCompactPaneThemeLength}"/>

<Setter Property="Height" Value="48"/>

<Setter Property="Padding" Value="0"/>

</Style>

ListView應用此樣式:

<ListView Margin="0,48,0,0" ItemsSource="{Binding NavMenuItemList}"

ItemTemplate="{StaticResource NavMenuItemTemplate}"

ItemContainerStyle="{StaticResource NavMenuItemContainerStyle}">

</ListView>

 

再跑一下:

現在樣式正常了。

 

四、增加新頁面

現在MainPage.xaml只是一個殼,右側內容是空的,下面來添加幾個頁面。在項目里添加幾個頁面,比如可以命名為HomePage、SearchPage、AboutPage等:

因為每個頁面里已經默認添加了一個TextBlock,並且綁定到了vm的Title屬性,這個屬性默認取值就是當前頁面的Name,所以我們就不用改了,知道當前頁面是哪個就行了。

現在的問題是,如何在MainPage載入時,自動在SplitView的Content里顯示HomePage呢?

這就需要用到MVVM-Sidekick的一個Behavior了,用Blend打開項目,找到行為:

有一個叫做BaeconBehavior的行為,把它拖到……咦,怎么找不到Content呢?

 

那就直接手寫吧,把Frame部分的代碼改成這樣:

<SplitView.Content>

<Frame x:Name="mainFrame" mvvm:StageManager.Beacon="frameMain" x:FieldModifier="public">

 

</Frame>

</SplitView.Content>

 

StageManager.Beacon屬性是用來標識StageManager,MVVM-Sidekick已經把導航的功能封裝到了StageManager里,以前我們一般使用this.StageManager.DefaultStage.Show(xxx)的方式來使用,即可實現整個頁面的導航,如果要實現頁面內某個區域的導航,就需要手動指定是哪個StageManager了,這就需要使用以下屬性來標識某個區域:

mvvm:StageManager.Beacon="frameMain"

 

找到OnBindedViewLoad方法,取消默認的注釋,將該方法改為以下的樣子:

protected override async Task OnBindedViewLoad(MVVMSidekick.Views.IView view)

{

await base.OnBindedViewLoad(view);

await StageManager["frameMain"].Show(new HomePage_Model());

}

 

這里要注意,一定要等Bind完成后再Show,不然會顯示不出來哦,因為要將整個頁面Bind完后,才可以進行后續的動作。

跑一下看看:

很好,默認轉到HomePage頁了。

 

五、實現其他頁面導航

現在可以處理菜單部分的導航了,點擊不同的項導航到不同的頁面。看到這里應該也有個大概了,處理不同項的點擊事件,將名為frameMain的StageManager使用Show方法展示不同的ViewModel即可。

使用ItemClick事件嗎?No,還記得我之前提過的SendToEventRouterAction嗎?如果不熟悉的話就翻翻我之前的blog吧,這里我還是用這個Action來實現。

修改項模板為:

<DataTemplate x:Key="NavMenuItemTemplate" >

<Grid>

<Interactivity:Interaction.Behaviors>

<Core:EventTriggerBehavior EventName="Tapped">

<Behaviors:SendToEventRouterAction IsEventFiringToAllBaseClassesChannels="True" EventRoutingName="NavToPage" EventData="{Binding}" />

</Core:EventTriggerBehavior>

</Interactivity:Interaction.Behaviors>

<Grid.ColumnDefinitions>

<ColumnDefinition MinWidth="48" />

<ColumnDefinition />

</Grid.ColumnDefinitions>

<FontIcon x:Name="Glyph" FontFamily="{StaticResource FontAwesomeFontFamily}" FontSize="16" Margin="0" Glyph="{Binding Glyph}" VerticalAlignment="Center" HorizontalAlignment="Center" ToolTipService.ToolTip="{Binding Label}"/>

<TextBlock x:Name="Text" Grid.Column="1" Text="{Binding Label}" VerticalAlignment="Center"/>

</Grid>

</DataTemplate>

然后在MainPage_Model.cs文件中,添加一個方法:

private void RegisterCommand()

{

//一般列表項點擊事件

MVVMSidekick.EventRouting.EventRouter.Instance.GetEventChannel<Object>()

.Where(x => x.EventName == "NavToPage")

.Subscribe(

async e =>

{

NavMenuItem item = e.EventData as NavMenuItem;

if (item != null)

{

switch (item.Label)

{

case "首頁":

await StageManager["frameMain"].Show(new HomePage_Model());

break;

case "搜索":

await StageManager["frameMain"].Show(new SearchPage_Model());

break;

 

case "關於":

await StageManager["frameMain"].Show(new AboutPage_Model());

break;

default:

break;

}

}

}

).DisposeWith(this);

 

 

}

別忘了在OnBindedViewLoad方法里調用一下:

private bool isLoaded;

/// <summary>

/// This will be invoked by view when the view fires Load event and this viewmodel instance is already in view's ViewModel property

/// </summary>

/// <param name="view">View that firing Load event</param>

/// <returns>Task awaiter</returns>

protected override async Task OnBindedViewLoad(MVVMSidekick.Views.IView view)

{

if (!isLoaded)

{

this.RegisterCommand();

this.isLoaded = true;

}

await base.OnBindedViewLoad(view);

await StageManager["frameMain"].Show(new HomePage_Model());

}

 

添加一個isLoaded屬性是避免重復調用。

跑一下看看,咦,有時候好用,有時候不好用,點擊圖標和文字的時候好用,點擊不到圖標和文字就不好用,這是什么原因?

熟悉ListView的同學可能會想到,ListViewItem默認是沒有橫向撐滿的,所以雖然點擊了項,但因為項模板里的Grid沒有橫向撐滿,所以並沒有觸發Grid的Tapped事件,那我們可以設置ListItemStyle,讓ListViewItem都橫向撐滿。在NavMenuItemContainerStyle里添加以下代碼:

<Setter Property="HorizontalContentAlignment" Value="Stretch"/>

<Setter Property="VerticalContentAlignment" Value="Stretch"/>

 

這樣就可以橫向縱向撐滿了,再跑下:

又亂套了,再改哪里呢,修改項模板NavMenuItemTemplate,設置左側列寬為Auto:

<DataTemplate x:Key="NavMenuItemTemplate" >

<Grid >

<Interactivity:Interaction.Behaviors>

<Core:EventTriggerBehavior EventName="Tapped">

<Behaviors:SendToEventRouterAction IsEventFiringToAllBaseClassesChannels="True" EventRoutingName="NavToPage" EventData="{Binding}" />

</Core:EventTriggerBehavior>

</Interactivity:Interaction.Behaviors>

<Grid.ColumnDefinitions>

<ColumnDefinition MinWidth="48" Width="Auto" />

<ColumnDefinition />

</Grid.ColumnDefinitions>

<FontIcon x:Name="Glyph" FontFamily="{StaticResource FontAwesomeFontFamily}" FontSize="16" Margin="0" Glyph="{Binding Glyph}" VerticalAlignment="Center" HorizontalAlignment="Center" ToolTipService.ToolTip="{Binding Label}"/>

<TextBlock x:Name="Text" Grid.Column="1" Text="{Binding Label}" VerticalAlignment="Center" />

</Grid>

</DataTemplate>

 

再運行一下:

現在正常了。

看一下手機上的樣子:

 

六、其他細節調整

使用了一下感覺還是有點細節需要改進,比如菜單彈出后,點擊項后應該讓菜單自動縮回去,現在改一下吧。

在MainPage的vm里添加一個屬性:

/// <summary>

///是否展開菜單

/// </summary>

public bool IsPaneOpen

{

get { return _IsPaneOpenLocator(this).Value; }

set { _IsPaneOpenLocator(this).SetValueAndTryNotify(value); }

}

#region Property bool IsPaneOpen Setup

protected Property<bool> _IsPaneOpen = new Property<bool> { LocatorFunc = _IsPaneOpenLocator };

static Func<BindableBase, ValueContainer<bool>> _IsPaneOpenLocator = RegisterContainerLocator<bool>("IsPaneOpen", model => model.Initialize("IsPaneOpen", ref model._IsPaneOpen, ref _IsPaneOpenLocator, _IsPaneOpenDefaultValueFactory));

static Func<bool> _IsPaneOpenDefaultValueFactory = () => default(bool);

#endregion

 

在vm的構造函數里將此值設置為false,默認為關閉。

然后將SplitView的IsPaneOpen屬性綁定到上面:

<SplitView x:Name="RootSplitView" IsPaneOpen="{Binding IsPaneOpen,Mode=TwoWay}"

DisplayMode="Inline"

OpenPaneLength="256"

IsTabStop="False">

 

修改RegisterCommand方法,在點擊每個項的部分,添加以下代碼,關閉菜單:

this.IsPaneOpen = false;

 

現在點擊菜單項后可以自動關閉菜單面板了。

還可以繼續針對PC版和手機版調整一下細節,PC版屏幕大,可以讓菜單收起時留下圖標的部分,這就需要調整PC版的DisplayMode屬性為CompactInline,需要請StateTriggers出馬了。

在根Grid里添加以下代碼:

<!-- Adaptive triggers -->

<VisualStateManager.VisualStateGroups>

<VisualStateGroup>

<VisualState>

<VisualState.StateTriggers>

<AdaptiveTrigger MinWindowWidth="720" />

</VisualState.StateTriggers>

<VisualState.Setters>

<Setter Target="RootSplitView.DisplayMode" Value="CompactInline"/>

<Setter Target="RootSplitView.IsPaneOpen" Value="True"/>

<Setter Target="RootSplitView.CompactPaneLength" Value="48" />

</VisualState.Setters>

</VisualState>

<VisualState>

<VisualState.StateTriggers>

<AdaptiveTrigger MinWindowWidth="0" />

</VisualState.StateTriggers>

<VisualState.Setters>

<Setter Target="RootSplitView.DisplayMode" Value="Overlay"/>

</VisualState.Setters>

</VisualState>

</VisualStateGroup>

</VisualStateManager.VisualStateGroups>

 

這段代碼的意思是,如果寬度大於720,就將SplitView的DisplayMode設置為CompactInline,菜單收起的時候可以保留圖標部分,這部分圖標的寬度通過CompactPaneLength這個值來設定。

 

還有一點,手機是有硬件返回鍵的,在菜單彈出的時候,如果用戶點擊了返回鍵,應該讓菜單縮回去,所以還要額外處理一下手機的返回鍵。

給項目添加Mobile Extensions引用:

注意我安裝了兩個版本的SDK,這里需要根據項目的實際版本來選擇對應的擴展。

打開MainPage.xaml.cs,添加以下代碼:

protected override void OnNavigatedTo(NavigationEventArgs e)

{

if (Windows.Foundation.Metadata.ApiInformation.IsTypePresent("Windows.Phone.UI.Input.HardwareButtons"))

{

HardwareButtons.BackPressed += HardwareButtons_BackPressed;

}

base.OnNavigatedTo(e);

}

 

protected override void OnNavigatedFrom(NavigationEventArgs e)

{

if (Windows.Foundation.Metadata.ApiInformation.IsTypePresent("Windows.Phone.UI.Input.HardwareButtons"))

{

HardwareButtons.BackPressed -= HardwareButtons_BackPressed;

}

base.OnNavigatedFrom(e);

}

 

private void HardwareButtons_BackPressed(object sender, BackPressedEventArgs e)

{

//throw new NotImplementedException();

var vm = this.LayoutRoot.DataContext as MainPage_Model;

if (vm != null)

{

if (vm.IsPaneOpen)

{

e.Handled = true;

vm.IsPaneOpen = false;

}

}

}

 

至此,一個具有基本功能的漢堡菜單就完成了,可以通過修改背景色、前景色等方式再來改善展示效果。再來總結一下主要的知識點:

  1. 使用SplitView來區分菜單面板和內容部分;
  2. 使用FontAwesomeFont字體顯示圖標;
  3. 為區域使用mvvm:StageManager.Beacon屬性來設置StageManager的標識,並通過StageManager["xxx"]形式來調用;
  4. 通過StateTriggers來為PC和手機端設置不同的菜單效果;
  5. 通過添加Mobile Extensions引用來支持手機硬件返回鍵;

附demo下載地址:

鏈接:http://pan.baidu.com/s/1pJRJcRh 密碼:jofi


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM