Win10 UWP 开发系列:使用SplitView实现汉堡菜单及页面内导航

Cle****-he UID.1073626
2016-01-14 发表

本帖最后由 Clever-he 于 2016-1-14 15:01 编辑

在Win10之前,WP平台的App主要有枢轴和全景两种导航模式,我个人更喜欢Pivot即枢轴模式,可以左右切换,非常方便。全景视图因为对设计要求比较高,自己总是做不出好的效果。对于一般的新闻阅读类App来说,Pivot更适合多个频道的展示,因为内容基本都是一样的。

到了Win10,微软模仿其他平台也推出了汉堡菜单,但并没有提供现成的控件,而是需要开发者通过一个名为SplitView的控件来实现。我个人并不觉得左上角的菜单有多么方便,汉堡菜单的使用必然会改变以前的导航模式,比如以前底部的AppBar使用很频繁,现在可以通过汉堡菜单的按钮来切换不同的页面。因此之前的App的导航模式需要重新设计。

假设有A、B、C三个平行的页面,可以在每个页面的左侧都放个汉堡菜单,也可以像web的框架页一样,做一个壳,汉堡菜单只放在外面的框架里,点击不同的按钮,在content里实现不同页面的导航。我比较倾向第二种,之前在做澎湃新闻uwp的时候就使用了这种方式,后来看了下Template10的模板,也是用的这种方式,在主页面外层套了一个Frame,而且还实现 了一个汉堡菜单控件。有兴趣的同学可以参考Template10来快速生成一个带汉堡菜单的基础App,Github地址:***链接停止解析*** ,这个项目还带了很多好东西,比如一些常用的帮助类和一些behavior等,值得uwp开发者好好学习。

我没有直接使用T10的模板,以下介绍的还是当时使用MVVM-Sidekick框架实现的页面内导航。

首先通过MVVM-Sidekick提供的项目模板来新建一个UWP项目,命名为NavDemo。

考虑我们要实现的目的:在主页面放置一个汉堡菜单,在右侧的content中实现不同页面的导航。

先来看一下效果:

PC版:

***附件停止解析***


手机版:

***附件停止解析***

一、创建菜单项类

汉堡菜单每个选项一般是由一个图标和一个文字组成,我还是使用FontAwesomeFont这个字体来显示图标,如何使用这个字体来做图标,可参考我之前的blog。首先建立一个菜单的类NavMenuItem,放在Models目录下,使用provm代码段生成两个属性:

[mw_shl_code=csharp,true]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
} [/mw_shl_code]

打开NavDemo\ViewModels\MainPage_Model.cs,使用propvm代码段生成一个列表:

[mw_shl_code=csharp,true]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[/mw_shl_code]

在vm的构造函数里,添加几个项:

[mw_shl_code=csharp,true]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 = "关于" });
}[/mw_shl_code]

注意Glyph的赋值方式。

二、显示汉堡菜单

在项目中新建Resources目录,把FontAwesome.otf字体文件放在里面。在项目中新建CustomTheme目录,然后建立自定义的样式资源文件CustomStyles.xaml

然后打开App.xaml文件,把这个资源引用进来:

[mw_shl_code=csharp,true]<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="CustomTheme/CustomStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
[/mw_shl_code]

样式资源文件里主要定义了两个样式,一是定义了FontAwesomeFontFamily字体,二是定义了一个针对ToggleButton的按钮样式SplitViewTogglePaneButtonStyle,作为汉堡菜单的开关。这个开关键为什么要设置高度为48呢?参考***链接停止解析***

拆分视图控件具有一个可展开/可折叠的窗格和一个内容区域。内容区域始终可见。窗格可以展开和折叠或停留在打开状态,而且可以从应用窗口的左侧或右侧显示其自身。窗格中有三种模式:
•覆盖
在打开之前隐藏窗格。在打开时,窗格覆盖内容区域。
•内联
窗格始终可见,并且不会覆盖内容区域。窗格和内容区域划分可用的屏幕实际使用面积。
•精简
在此模式下窗格始终可见,它仅足够宽以显示图标(通常 48 epx 宽)。窗格和内容区域划分可用的屏幕实际使用面积。尽管标准精简模式不覆盖内容区域,但它可以转化为更宽的窗格来显示更多内容,这将覆盖该内容区域。
所以我就根据官方文档设置为48了。
修改MainPage.xaml,把根Grid改为以下代码:

[mw_shl_code=csharp,true]<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>[/mw_shl_code]

为了方便查看菜单展开的效果,暂时先把IsPaneOpen属性设置为true,OpenPaneLength设置的是菜单展开后的宽度。在Pane里放一个ListView,ItemSource绑定到之前做好的NavMenuItemList上。SplitView的Content设置为一个Frame,用来展示右侧的页面。

注意,如果当SplitView的Content直接设置为Frame的时候,也就是把外层的<SplitView.Content>去掉后,会报一个错:

***附件停止解析***
这个错误可以不用理会,程序是可以正常运行的。

此外 还要有一个按钮来控制菜单的展开关闭状态,用一个ToggleButton来实现,这个按钮的图标一般是三个横杠,设置其Style为SplitViewTogglePaneButtonStyle即可。
然后,还要设置ListView的项模板,可以使用Blend来设计项模板,但因为这个比较简单,我就直接手写了,在Resources目录下添加一个资源文件CustomDataTemplates.xaml,项目所有的自定义模板都可以写在这里,代码如下:

[mw_shl_code=csharp,true]<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>[/mw_shl_code]

在这里定义一个项模板NavMenuItemTemplate,在里面放一个FontIcon,把Glyph属性绑定到NavMenuItem的Glyph属性,当然不要忘了把FontFamily设置为我们在自定义样式里定义好的FontAwesomeFontFamily,不然是不会生效的。

再把这个项模板应用到页面的ListView控件上:

[mw_shl_code=csharp,true]
ItemTemplate="{StaticResource NavMenuItemTemplate}"
[/mw_shl_code]


现在跑一下试试,报错了:

***附件停止解析***

原来忘了把刚才的模板文件引入进来,修改App.xaml,修改为以下的样子:

[mw_shl_code=csharp,true]
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="CustomTheme/CustomStyles.xaml"/>
<ResourceDictionary Source="Resources/CustomDataTemplates.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>[/mw_shl_code]

现在可以运行了:

***附件停止解析***

貌似左上角的按钮跟ListView重叠了,这样可不好看。


三、调整显示效果

左上角的按钮应用了SplitViewTogglePaneButtonStyle样式,最小高度为48,把ListView往下移动一点,添加一个Margin属性,顶部把开关按钮的空间空出来:

[mw_shl_code=csharp,true]<ListView Margin="0,48,0,0" ItemsSource="{Binding NavMenuItemList}"

ItemTemplate="{StaticResource NavMenuItemTemplate}">[/mw_shl_code]

现在列表位置正常了,但图标的位置貌似还是偏右了,那就再给ListView设置ItemContainerStyle样式,在CustomStyles.xaml文件里添加以下代码:

[mw_shl_code=csharp,true]
<Style x:Key="NavMenuItemContainerStyle" TargetType="ListViewItem">
<Setter Property="MinWidth" Value="{StaticResource SplitViewCompactPaneThemeLength}"/>
<Setter Property="Height" Value="48"/>
<Setter Property="Padding" Value="0"/>
</Style>[/mw_shl_code]


ListView应用此样式:

***附件停止解析***
[mw_shl_code=csharp,true]<ListView Margin="0,48,0,0" ItemsSource="{Binding NavMenuItemList}"
ItemTemplate="{StaticResource NavMenuItemTemplate}"
ItemContainerStyle="{StaticResource NavMenuItemContainerStyle}">
</ListView>
[/mw_shl_code]

再跑一下:
***附件停止解析***
现在样式正常了。


四、增加新页面
现在MainPage.xaml只是一个壳,右侧内容是空的,下面来添加几个页面。在项目里添加几个页面,比如可以命名为HomePage、SearchPage、AboutPage等:

***附件停止解析***

因为每个页面里已经默认添加了一个TextBlock,并且绑定到了vm的Title属性,这个属性默认取值就是当前页面的Name,所以我们就不用改了,知道当前页面是哪个就行了。
现在的问题是,如何在MainPage载入时,自动在SplitView的Content里显示HomePage呢?

这就需要用到MVVM-Sidekick的一个Behavior了,用Blend打开项目,找到行为:

***附件停止解析***

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

***附件停止解析***

那就直接手写吧,把Frame部分的代码改成这样:

[mw_shl_code=csharp,true]
<SplitView.Content>
<Frame x:Name="mainFrame" mvvm:StageManager.Beacon="frameMain" x:FieldModifier="public">
</Frame>
</SplitView.Content>
[/mw_shl_code]


StageManager.Beacon属性是用来标识StageManager,MVVM-Sidekick已经把导航的功能封装到了StageManager里,以前我们一般使用this.StageManager.DefaultStage.Show(xxx)的方式来使用,即可实现整个页面的导航,如果要实现页面内某个区域的导航,就需要手动指定是哪个StageManager了,这就需要使用以下属性来标识某个区域:

[mw_shl_code=csharp,true]mvvm:StageManager.Beacon="frameMain"[/mw_shl_code]


找到OnBindedViewLoad方法,取消默认的注释,将该方法改为以下的样子:


[mw_shl_code=csharp,true]protected override async Task OnBindedViewLoad(MVVMSidekick.Views.IView view)

{
await base.OnBindedViewLoad(view);
await StageManager["frameMain"].Show(new HomePage_Model());
}[/mw_shl_code]

这里要注意,一定要等Bind完成后再Show,不然会显示不出来哦,因为要将整个页面Bind完后,才可以进行后续的动作。

跑一下看看:

***附件停止解析***

很好,默认转到HomePage页了。

五、实现其他页面导航

现在可以处理菜单部分的导航了,点击不同的项导航到不同的页面。看到这里应该也有个大概了,处理不同项的点击事件,将名为frameMain的StageManager使用Show方法展示不同的ViewModel即可。

使用ItemClick事件吗?No,还记得我之前提过的SendToEventRouterAction吗?如果不熟悉的话就翻翻我之前的blog吧,这里我还是用这个Action来实现。

修改项模板为:

[mw_shl_code=csharp,true]<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>[/mw_shl_code]


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


[mw_shl_code=csharp,true]
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);
} [/mw_shl_code]


别忘了在OnBindedViewLoad方法里调用一下:

[mw_shl_code=csharp,true]
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());
}
[/mw_shl_code]


添加一个isLoaded属性是避免重复调用。

跑一下看看,咦,有时候好用,有时候不好用,点击图标和文字的时候好用,点击不到图标和文字就不好用,这是什么原因?

熟悉ListView的同学可能会想到,ListViewItem默认是没有横向撑满的,所以虽然点击了项,但因为项模板里的Grid没有横向撑满,所以并没有触发Grid的Tapped事件,那我们可以设置ListItemStyle,让ListViewItem都横向撑满。在NavMenuItemContainerStyle里添加以下代码:
[mw_shl_code=csharp,true]<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="VerticalContentAlignment" Value="Stretch"/>
[/mw_shl_code]

这样就可以横向纵向撑满了,再跑下:

***附件停止解析***

又乱套了,再改哪里呢,修改项模板NavMenuItemTemplate,设置左侧列宽为Auto:

[mw_shl_code=csharp,true]
<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>[/mw_shl_code]


再运行一下:

***附件停止解析***


现在正常了。

看一下手机上的样子:

***附件停止解析***
***附件停止解析***


至此,一个具有基本功能的汉堡菜单就完成了,可以通过修改背景色、前景色等方式再来改善展示效果。再来总结一下主要的知识点:
1.使用SplitView来区分菜单面板和内容部分;
2.使用FontAwesomeFont字体显示图标;
3.为区域使用mvvm:StageManager.Beacon属性来设置StageManager的标识,并通过StageManager["xxx"]的形式来调用;
4.通过StateTriggers来为PC和手机端设置不同的菜单效果;
5.通过添加Mobile Extensions引用来支持手机硬件返回键;

附demo下载地址:

链接:***链接停止解析*** 密码:jofi

开发者交流群:53078485,期待您的加入!

敬告:
为防止不可控的内容风险,本站已关闭新用户注册,新贴的发表及评论;
你现在看到的内容只是互联网用户曾经发表的言论快照,仅用于老用户留存纪念,且仅与科技行业相关,全部内容不代表本站观点及立场;
本站重新开放前已针对包括用户隐私、版权保护、信息安全、国家政策在内的各种互联网法律法规要求,执行了隐患内容的自查、屏蔽和删除;
本站目前所属个人主体,未有任何盈利安排与计划,且与原WFUN.COM所属公司不存在任何关联关系;
如果本帖内容或者相关资源侵犯到您的合法权益,或者您认为存在问题,那么请您务必点此举报或投诉!
全部回复:
xyangtian UID.832923
2016-01-14 回复

大神的文章果然没人看

she****520 UID.1053994
2016-01-14 使用 Lumia 950 XL 回复

大神的文章果然没人看

synelee UID.516043
2016-01-14 回复

不喜欢汉堡菜单

wind UID.62
2016-01-14 回复

大神的文章,我得亲自顶。

←****才 UID.360018
2016-01-14 回复

大神的文章,我得亲自顶。

龍影 UID.708954
2016-01-14 回复

大神的文章,我得亲自顶。

shy536 UID.310065
2016-01-14 回复

大神的文章,我得亲自顶。

君莫停 UID.1148062
2016-01-14 回复

大神的文章,我得亲自顶。

本站使用Golang构建,点击此处申请开源鄂ICP备18029942号-4联系站长投诉/举报