Win10 UWP开发系列:实现Master/Detail布局
2017-09-21 10:17
603 查看
原文:Win10 UWP开发系列:实现Master/Detail布局在开发XX新闻的过程中,UI部分使用了Master/Detail(大纲/细节)布局样式。Win10系统中的邮件App就是这种样式,左侧一个列表,右侧是详情页面。关于这种 样式的说明可参看MSDN文档:https://msdn.microsoft.com/zh-cn/library/windows/apps/xaml/dn997765.aspx
样式如下:
View Code
我在代码里输出了一些信息,调试的时候可以观察各种状态是在什么时候切换的。
然后在MainPage.xaml里 应用这个StateTrigger,首先,要在MainPage的ViewModel里添加一个object,用于绑定DetailFrame的内容:
MainPage.xaml里的第二个Frame的Content绑定到这个DetailContent上:
注意Mode要设置为TwoWay,这样才可以让Trigger知道DetaiFrame的内容。在MainPage.xaml的根Grid里添加以下Trigger:
还要把默认的gridMain的两列的宽度默认值分别改为*和0:
Trigger的意义很清楚了,Setter会根据不同的状态去设置gridMain两列的宽度来控制MasterPage和DetailPage的显示和隐藏:
当刚开始进入程序,左侧显示列表,右侧显示BlankPage,这时候如果宽度大于720,两个页面正常展示,如果页面宽度小于720,则只显示列表页;
如果页面宽度大于720的时候,点击列表,右侧正常显示详情;
如果页面宽度小于720,点击列表,列表会隐藏,只显示详情;
基本达到了文章开头提出的目的。
基本思路是,点击返回后,应该先判断DetailPage是否可GoBack,如果可以就GoBack,直到返回最开始的BlankPage为止,这样StateTrigger会自动触发NarrowAndBlankDetail状态,显示MasterPage。
返回是处理SystemNavigationManager.GetForCurrentView().BackRequested这个事件,打开MainPage.xaml.cs文件,在OnNavigatedTo里订阅这个事件:
用户点击返回键的时候,首先看DetailPage能否GoBack,再看MasterPage能否GoBack,当没有可GoBack的时候就把返回键隐藏。
在PC上的返回键默认是隐藏的,还需要在导航到详情页的时候将其展示出来,修改MainPage_Model.cs文件里的RegisterCommand方法:
现在运行一下,PC上也可以返回了。当第一次打开的时候,是这样 的:
如果拖动窗口缩小,则只会显示MasterPage:
当点击列表项时,会只显示DetailPage:
点击左上角返回键,又只显示MasterPage了。
具体切换动画我不会截图,大家可以下载demo自己试试。
在NarrowAndBlankDetail的VisualState里,添加一段StoryBoard:
设置透明度从0到1,同时有一个移动的效果。注意这里的StoryBoard.TargetProperty的写法,详细说明可以参考MSDN文档:
https://msdn.microsoft.com/zh-cn/library/windows/apps/windows.ui.xaml.media.animation.storyboard.targetproperty.aspx
https://msdn.microsoft.com/zh-cn/library/windows/apps/jj569302.aspx
再次吐槽一下MSDN文档真是太难找了。版本太多。
在<VisualStateManager.VisualStateGroups>里添加Transitions:
同时要在gridMain里添加以下代码:
不然动画无法起作用。
现在运行一下看看,返回的时候MasterPage也是从左侧渐变滑入的,效果好了不少。
这种方式基本可以把WP8.1的代码直接拿过来用,页面改动不大。如果您有更好的实现方式,欢迎留言讨论。
这篇基本就写到这里了。最近WP圈一片哀嚎,很多无奈的事情。但作为普通个人开发者来说,抱怨也没用,能做多少就做多少吧,总好过只吐槽。行动的意义永远大于口头空讲。
本文得到了礼物说开发者郑大神的鼎力支持。希望大家下载他的礼物说,做的非常漂亮。
预祝大家新春快乐!
最后给出demo下载:链接:http://pan.baidu.com/s/1hqQTbEW 密码:ilar
样式如下:
public class MasterDetailStateTrigger : StateTriggerBase, ITriggerValue { public MasterDetailStateTrigger() { if (!Windows.ApplicationModel.DesignMode.DesignModeEnabled) { var weakEvent = new WeakEventListener<MasterDetailStateTrigger, ApplicationView, object>(this) { OnEventAction = (instance, source, eventArgs) => MasterDetailStatetateTrigger_MasterDetailStateChanged(source, eventArgs), OnDetachAction = (instance, weakEventListener) => ApplicationView.GetForCurrentView().VisibleBoundsChanged -= weakEventListener.OnEvent }; ApplicationView.GetForCurrentView().VisibleBoundsChanged += weakEvent.OnEvent; } } private void MasterDetailStatetateTrigger_MasterDetailStateChanged(ApplicationView sender, object args) { UpdateTrigger(); } private void UpdateTrigger() { IsActive = GetMasterDetailState() == MasterDetailState; } public MasterDetailState MasterDetailState { get { return (MasterDetailState)GetValue(MasterDetailStateProperty); } set { SetValue(MasterDetailStateProperty, value); } } // Using a DependencyProperty as the backing store for MasterDetailState. This enables animation, styling, binding, etc... public static readonly DependencyProperty MasterDetailStateProperty = DependencyProperty.Register("MasterDetailState", typeof(MasterDetailState), typeof(MasterDetailStateTrigger), new PropertyMetadata(MasterDetailState.Wide, OnMasterDetailStatePropertyChanged)); private static void OnMasterDetailStatePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var obj = (MasterDetailStateTrigger)d; if (!Windows.ApplicationModel.DesignMode.DesignModeEnabled) { obj.UpdateTrigger(); } } public object DetailContent { get { return (object)GetValue(DetailContentProperty); } set { SetValue(DetailContentProperty, value); } } // Using a DependencyProperty as the backing store for DetailContent. This enables animation, styling, binding, etc... public static readonly DependencyProperty DetailContentProperty = DependencyProperty.Register("DetailContent", typeof(object), typeof(MasterDetailStateTrigger), new PropertyMetadata(null, new PropertyChangedCallback(OnValuePropertyChanged))); private static void OnValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var obj = (MasterDetailStateTrigger)d; obj.UpdateTrigger(); } internal MasterDetailState GetMasterDetailState() { System.Diagnostics.Debug.WriteLine("DetailContent为空:" + (DetailContent == null).ToString()); //第一种 窄屏模式 DetailFrame为空 if (Window.Current.Bounds.Width < 720) { System.Diagnostics.Debug.WriteLine("VisibleBounds.Width:" + ApplicationView.GetForCurrentView().VisibleBounds.Width.ToString()); System.Diagnostics.Debug.WriteLine("Window.Current.Bounds:" + Window.Current.Bounds.Width.ToString()); MVVMPage detailPage = (MVVMSidekick.Views.MVVMPage)DetailContent; if (detailPage != null) { if (detailPage.BaseUri.ToString() == "ms-appx:///BlankPage.xaml") { System.Diagnostics.Debug.WriteLine("触发NarrowAndBlankDetail模式"); return MasterDetailState.NarrowAndBlankDetail; } else { System.Diagnostics.Debug.WriteLine("触发NarrowAndNoBlankDetail模式"); return MasterDetailState.NarrowAndNoBlankDetail; } } else { return MasterDetailState.NarrowAndBlankDetail; } } else { System.Diagnostics.Debug.WriteLine("触发Wide模式"); return MasterDetailState.Wide; } } #region ITriggerValue private bool m_IsActive; /// <summary> /// Gets a value indicating whether this trigger is active. /// </summary> /// <value><c>true</c> if this trigger is active; otherwise, <c>false</c>.</value> public bool IsActive { get { return m_IsActive; } private set { if (m_IsActive != value) { m_IsActive = value; base.SetActive(value); if (IsActiveChanged != null) IsActiveChanged(this, EventArgs.Empty); } } } /// <summary> /// Occurs when the <see cref="IsActive" /> property has changed. /// </summary> public event EventHandler IsActiveChanged; #endregion ITriggerValue } public enum MasterDetailState { /// <summary> /// narrow and a blank detail page /// </summary> NarrowAndBlankDetail, /// <summary> /// narrow and detail page is not blank /// </summary> NarrowAndNoBlankDetail, /// <summary> /// wide /// </summary> Wide }
View Code
我在代码里输出了一些信息,调试的时候可以观察各种状态是在什么时候切换的。
然后在MainPage.xaml里 应用这个StateTrigger,首先,要在MainPage的ViewModel里添加一个object,用于绑定DetailFrame的内容:
/// <summary> /// detailFrame的内容 /// </summary> public object DetailContent { get { return _DetailContentLocator(this).Value; } set { _DetailContentLocator(this).SetValueAndTryNotify(value); } } #region Property object DetailContent Setup protected Property<object> _DetailContent = new Property<object> { LocatorFunc = _DetailContentLocator }; static Func<BindableBase, ValueContainer<object>> _DetailContentLocator = RegisterContainerLocator<object>("DetailContent", model => model.Initialize("DetailContent", ref model._DetailContent, ref _DetailContentLocator, _DetailContentDefaultValueFactory)); static Func<object> _DetailContentDefaultValueFactory = () => default(object); #endregion
MainPage.xaml里的第二个Frame的Content绑定到这个DetailContent上:
<Frame x:Name="detailFrame" Content="{Binding DetailContent,Mode=TwoWay}" HorizontalAlignment="Stretch" Grid.Column="1" mvvm:StageManager.Beacon="detailFrame" x:FieldModifier="public">
注意Mode要设置为TwoWay,这样才可以让Trigger知道DetaiFrame的内容。在MainPage.xaml的根Grid里添加以下Trigger:
<VisualStateManager.VisualStateGroups> <VisualStateGroup> <VisualState x:Name="NarrowAndBlankDetail"> <VisualState.StateTriggers> <triggers:MasterDetailStateTrigger MasterDetailState="NarrowAndBlankDetail" DetailContent="{Binding DetailContent}" /> </VisualState.StateTriggers> <VisualState.Setters> <Setter Target="gridMain.ColumnDefinitions[0].Width" Value="*" /> <Setter Target="gridMain.ColumnDefinitions[1].Width" Value="0" /> </VisualState.Setters> </VisualState> <VisualState x:Name="NarrowAndNoBlankDetail"> <VisualState.StateTriggers> <triggers:MasterDetailStateTrigger MasterDetailState="NarrowAndNoBlankDetail" DetailContent="{Binding DetailContent}" /> </VisualState.StateTriggers> <VisualState.Setters> <Setter Target="gridMain.ColumnDefinitions[0].Width" Value="0" /> <Setter Target="gridMain.ColumnDefinitions[1].Width" Value="*" /> </VisualState.Setters> </VisualState> <VisualState x:Name="Wide"> <VisualState.StateTriggers> <triggers:MasterDetailStateTrigger MasterDetailState="Wide" DetailContent="{Binding DetailContent}" /> </VisualState.StateTriggers> <VisualState.Setters> <Setter Target="gridMain.ColumnDefinitions[0].Width" Value="2*" /> <Setter Target="gridMain.ColumnDefinitions[1].Width" Value="3*" /> </VisualState.Setters> </VisualState> </VisualStateGroup> </VisualStateManager.VisualStateGroups>
还要把默认的gridMain的两列的宽度默认值分别改为*和0:
<Grid x:Name="gridMain" > <Grid.RenderTransform> <CompositeTransform /> </Grid.RenderTransform> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="0" /> </Grid.ColumnDefinitions> <Frame x:Name="masterFrame" Grid.Column="0" mvvm:StageManager.Beacon="masterFrame" x:FieldModifier="public"/> <Frame x:Name="detailFrame" Content="{Binding DetailContent,Mode=TwoWay}" HorizontalAlignment="Stretch" Grid.Column="1" mvvm:StageManager.Beacon="detailFrame" x:FieldModifier="public"> </Frame> </Grid>
Trigger的意义很清楚了,Setter会根据不同的状态去设置gridMain两列的宽度来控制MasterPage和DetailPage的显示和隐藏:
当刚开始进入程序,左侧显示列表,右侧显示BlankPage,这时候如果宽度大于720,两个页面正常展示,如果页面宽度小于720,则只显示列表页;
如果页面宽度大于720的时候,点击列表,右侧正常显示详情;
如果页面宽度小于720,点击列表,列表会隐藏,只显示详情;
基本达到了文章开头提出的目的。
四、处理返回键
当在手机上运行的时候,就会发现当点击列表显示DetailPage后,再按返回键直接退出程序了。因为还没有处理返回键事件。PC上也一样,程序左上角应该有个返回按钮。下面来处理返回事件。基本思路是,点击返回后,应该先判断DetailPage是否可GoBack,如果可以就GoBack,直到返回最开始的BlankPage为止,这样StateTrigger会自动触发NarrowAndBlankDetail状态,显示MasterPage。
返回是处理SystemNavigationManager.GetForCurrentView().BackRequested这个事件,打开MainPage.xaml.cs文件,在OnNavigatedTo里订阅这个事件:
protected override void OnNavigatedTo(NavigationEventArgs e) { SystemNavigationManager.GetForCurrentView().BackRequested += CurrentView_BackRequested; //SystemNavigationManager.GetForCurrentView().AppViewBackButtonVisibility = AppViewBackButtonVisibility.Visible; base.OnNavigatedTo(e); } protected override void OnNavigatedFrom(NavigationEventArgs e) { SystemNavigationManager.GetForCurrentView().BackRequested -= CurrentView_BackRequested; base.OnNavigatedFrom(e); } private void CurrentView_BackRequested(object sender, BackRequestedEventArgs e) { //判断DetailPage能否GoBack,如果可以GoBack则GoBack 显示BlankPage //其次判断MasterPage能否GoBack,如果可以GoBack则GoBack //如果不能GoBack,则提示是否退出 if (StrongTypeViewModel.StageManager["detailFrame"].CanGoBack) { e.Handled = true; StrongTypeViewModel.StageManager["detailFrame"].Frame.GoBack(); } else if (StrongTypeViewModel.StageManager["masterFrame"].CanGoBack) { e.Handled = true; StrongTypeViewModel.StageManager["masterFrame"].Frame.GoBack(); } else { //TODO 隐藏回退键 提示退出 SystemNavigationManager.GetForCurrentView().AppViewBackButtonVisibility = AppViewBackButtonVisibility.Collapsed; } }
用户点击返回键的时候,首先看DetailPage能否GoBack,再看MasterPage能否GoBack,当没有可GoBack的时候就把返回键隐藏。
在PC上的返回键默认是隐藏的,还需要在导航到详情页的时候将其展示出来,修改MainPage_Model.cs文件里的RegisterCommand方法:
private void RegisterCommand() { MVVMSidekick.EventRouting.EventRouter.Instance.GetEventChannel<Object>() .Where(x => x.EventName == "NewsItemTapped") .Subscribe( async e => { NewsItem item = e.EventData as NewsItem; SystemNavigationManager.GetForCurrentView().AppViewBackButtonVisibility = AppViewBackButtonVisibility.Visible; await StageManager["detailFrame"].Show(new DetailPage_Model(item)); } ).DisposeWith(this); }
现在运行一下,PC上也可以返回了。当第一次打开的时候,是这样 的:
如果拖动窗口缩小,则只会显示MasterPage:
当点击列表项时,会只显示DetailPage:
点击左上角返回键,又只显示MasterPage了。
具体切换动画我不会截图,大家可以下载demo自己试试。
五、添加切换动画效果
我们还可以做的更美观一点。UWP默认的Page切换是有动画效果的,但这里因为只使用StateTrigger设置了Grid的列宽,当从DetailPage返回MasterPage的时候MasterPage一下子就显示出来了,感觉有点生硬。现在给切换加一个动画。在NarrowAndBlankDetail的VisualState里,添加一段StoryBoard:
<Storyboard > <DoubleAnimation Storyboard.TargetName="gridMain" Storyboard.TargetProperty="Opacity" From="0" To="1" Duration="0:0:0.6"> <DoubleAnimation.EasingFunction> <CircleEase EasingMode="EaseOut" /> </DoubleAnimation.EasingFunction> </DoubleAnimation> <DoubleAnimation Storyboard.TargetName="gridMain" Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.TranslateX)" From="-100" To="0" Duration="0:0:0.3"> <DoubleAnimation.EasingFunction> <CircleEase EasingMode="EaseOut" /> </DoubleAnimation.EasingFunction> </DoubleAnimation> </Storyboard>
设置透明度从0到1,同时有一个移动的效果。注意这里的StoryBoard.TargetProperty的写法,详细说明可以参考MSDN文档:
https://msdn.microsoft.com/zh-cn/library/windows/apps/windows.ui.xaml.media.animation.storyboard.targetproperty.aspx
https://msdn.microsoft.com/zh-cn/library/windows/apps/jj569302.aspx
再次吐槽一下MSDN文档真是太难找了。版本太多。
在<VisualStateManager.VisualStateGroups>里添加Transitions:
<VisualStateGroup.Transitions> <VisualTransition From="NarrowAndNoBlankDetail" To="NarrowAndBlankDetail" ></VisualTransition> </VisualStateGroup.Transitions>
同时要在gridMain里添加以下代码:
<Grid.RenderTransform> <CompositeTransform /> </Grid.RenderTransform>
不然动画无法起作用。
现在运行一下看看,返回的时候MasterPage也是从左侧渐变滑入的,效果好了不少。
这种方式基本可以把WP8.1的代码直接拿过来用,页面改动不大。如果您有更好的实现方式,欢迎留言讨论。
这篇基本就写到这里了。最近WP圈一片哀嚎,很多无奈的事情。但作为普通个人开发者来说,抱怨也没用,能做多少就做多少吧,总好过只吐槽。行动的意义永远大于口头空讲。
本文得到了礼物说开发者郑大神的鼎力支持。希望大家下载他的礼物说,做的非常漂亮。
预祝大家新春快乐!
最后给出demo下载:链接:http://pan.baidu.com/s/1hqQTbEW 密码:ilar
相关文章推荐
- Win10 UWP开发系列:实现Master/Detail布局
- Win10 UWP 开发系列:使用SplitView实现汉堡菜单及页面内导航
- Win10 UWP开发系列:实现Master/Detail布局
- Win10 UWP开发系列——开源控件库:UWPCommunityToolkit
- Win10 UWP开发系列:开发一个自定义控件——带数字徽章的AppBarButton
- Win10 UWP 开发系列:使用SQLite
- Win10 UWP 开发系列:支持异步的SQLite
- Win10 UWP 开发系列:使用SQLite
- Win10 UWP开发系列——开源控件库:UWPCommunityToolkit
- Win10 UWP开发系列:解决Win10不同版本的Style差异导致的兼容性问题
- Win10 UWP 开发系列:使用SQLite
- Win10 UWP 开发系列:支持异步的SQLite
- Win10 UWP 开发系列:支持异步的SQLite
- Win10 UWP 开发系列:使用多语言工具包让应用支持多语言
- Win10 UWP 开发系列:使用多语言工具包让应用支持多语言
- Win10 UWP开发系列:使用VS2015 Update2+ionic开发第一个Cordova App
- Win10 UWP开发系列:解决Win10不同版本的Style差异导致的兼容性问题
- Win10 UWP开发系列:开发一个自定义控件——带数字徽章的AppBarButton
- Win10 UWP开发系列:使用VS2015 Update2+ionic开发第一个Cordova App
- 火云开发课堂 - 《Shader从入门到精通》系列 第十节:在Shader中实现模糊滤镜