前面有一篇博客说到了淘宝UWP的"四核驱动的三维导航—淘宝新UI(需求分析篇)",花了两周的时间实现了这个框架,然后又陆陆续续用了三周的时间完善它。
多窗口导航,与传统的导航方式的最大的不同点是:
- 需要指定目标窗口(target Frame)
- 维护上下文关系(context)
- Back Stack的维护
- 页面间的异步通信
- 适配Desktop和Mobile界面
- 支持Continuum(optional)
本篇博客先说一下前三个问题如何解决。
在从页面A跳到页面B时,单窗口模式下,很简单,只要在页面A中调用系统方法就可以了:
this.Frame.NavigateTo(typeof (PageB));
因为页面A使用的Frame和页面B使用的Frame相同。但是在多窗口(多Frame)时,你需要指定页面B使用那个Frame作为目标窗口。
比如,淘宝首页:
在点击了"淘抢购"频道的入口后,你想在什么地方显示淘抢购的主页面呢?在目前的Taobao UWP中,我们选择在主页的右侧显示(但是考虑到淘抢购的使用频率,我们可能会考虑在下一版中把它作为一个独立的Scenario来对待,也就是说可能要在目前的主页的位置显示)。于是,界面就会变成这样:
这就要求在导航方法中多一个参数来指定具体的位置。按照我们的设计,在Taobao UWP中有以下4个Frame承担具体的导航工作:
Home Frame:作为一个Scenario的首页,必须存在。
List Frame:显示商品列表,可以存在。有些Scenario,它的首页就是一个商品列表,就在Home Frame里显示了,List Frame可以不存在。
Detail Frame:详情页,必须存在。基本上所有的商品,都是要通过详情页来展示具体的商品信息的。当然,在别的Scenario中,如"我的淘宝"场景,可以用此Frame显示别的非商品内容。
Chat Frame:附加页,一般用于显示内置旺信聊天窗口,必须存在。
淘宝首页是显示在Home Frame中的,点击淘抢购入口后,code里都做了什么坏事儿呢?
Nav.To(NavToUrls.RushBuy_Home, mode: NavMode.List_Replace);
其中,Nav是我们自定义的一个导航帮助类,To()是它的静态方法,定义如下:
public static bool To(string url, object param = null, NavMode mode);
参数解释:
- string url:这个是必须的。我们把所有的页面都指定了一个URL,淘抢购的主页的URL定义如下:
public const string RushBuy_Home = "taobao://go/RushBuy/index";
- object param:指定页面间传递的参数。
- NavMode mode:指定导航模式,是一个枚举值,定义如下:
public enum NavMode
{
Default, // show page in scenario frame
Home_Add, // show page in scenario frame
List_Add, // show page in list frame, keep old page (if exists) in back history
List_Replace, // show page in list frame to replace the old page (clear old page in back history)
Detail_Replace, // show page in detail frame to replace the old page (clear old page in back history)
Detail_Replace_Clear, // same as above, also delete page in list frame
Chat_Add, // show page in chat frame, keep old page (if exists) in back history
Chat_Replace, // show page in chat frame to replace the old page (clear old page in back history)
Chat_Replace_Clear, // same as above, also delete pages in list/detail frame
}
每个枚举值都包含有两个信息:位置,方式。如List_Add,下划线前面的"List"表示位置,在List Frame中显示目标页面;下划线后面的"Add"是方式,一共有三种方式:
- Add:如果在目标Frame中已经有其它页面存在,则在已有页面基础上再做一次Navigation,也就是back stack历史中会多出一个记录。
- Replace:如果目标Frame中已有其它页面存在,则"删除"它,也就是在back stack中清空历史记录,然后做一次Navigation,最后back stack中只有最新的页面记录。
- Replace_Clear:同Replace,另外,如果前面的Frame中(除了Home Frame以外),如果有页面存在,则清除它们。
我们逐个解释一下这些参数的用法。
为什么需要Add/Replace方式?举个例子,在淘宝首页,假设用户先点击了天猫:
然后用户又在左侧首页上点击了淘抢购:
由于天猫和淘抢购都是在List Frame中显示,于是淘抢购把天猫覆盖了。此时用户按Back键,希望看到什么?显然不是要回天猫!于是,我们是这样指定淘抢购的导航方式的:
Nav.To(NavToUrls.RushBuy_Home, mode: NavMode.List_Replace);
它指定了淘抢购在List Frame中显示,用Replace方式,也就是清掉前面的天猫的痕迹。
何时用Add方式呢?比如用户在首页点了搜索:
右侧进入了搜索起始页面,用户输入搜索词后按回车,再进入搜索结果页面:
此时用户想改一下搜索的关键字为lumia 950 xl,点击一下搜索结果页上方的搜索框,就会回到搜索起始页。对于这个Scenario,我们需要这样使用导航方法:
1)在首次进入搜索起始页时,使用Replace方式来清空历史:
Nav.To(NavToUrls.Go_Search, mode: NavMode.List_Replace);
2)在进入搜索结果页时,使用Add方式附加搜索结果页:
Nav.To(NavToUrls.Search_Result, mode: NavMode.List_Add);
如此一来,当用户在搜索结果页按返回键,或者点击搜索框(等同于按返回键)时,就会在List Frame中调用Navigation Back方法,回到搜索起始页,因为此时搜索起始页一直在List Frame的历史stack中等候。
何时使用Replace_Clear这种方式呢?对于有些Scenario,有一些特殊的导航需求,主要是为了保证左右两侧窗口的上下文关系。比如店铺:
假设我们先看了店铺简介,现在左右两侧的窗口内容是有上下文关系的。此时,店铺首页在Home Frame中,店铺简介在List Frame中。我们用这个方法来显示店铺简介页:
Nav.To(NavToUrls.Shop_Brief, param: ViewModelData.ShopInfo?.ShopId, mode: NavMode.List_Replace);
现在,左右两侧的情况是:Home Frame <-> List Frame。然后用户在左侧的店铺首页上点击了一个商品,想看该商品的详情,页面就会变成这个样子:
此时左右两侧的情况是:Home Frame <-> Detail Frame。那么刚才的用于显示店铺简介的List Frame哪里去了呢?被清掉了!因为我们使用了这个方式来显示详情页:
Nav.To(data?.H5Url, mode: NavMode.Detail_Replace_Clear);
请大家注意第二个参数,使用了Detail_Replace_Clear方式,表示在Detail Frame中显示详情页,Replace该Frame中以前的内容,并且Clear掉List Frame中的所有页面。于是,店铺简介页面神奇地消失了,因为它不符合现在的上下文关系。当然,此时还需要其他一些手段来隐藏空白的List Frame,比如判断List Frame的Content是否为空。
写累了,喘口气。刚才在写博客做截图时,还发现了个Navigation的bug,顺手给fix了,是调用参数写错了,应该写成Detail_Replace_Clear,但是写成了Detail_Replace,导致现在线上的版本,在店铺页,先点击店铺简介,再在左侧点击全部宝贝,再点击任意商品,此时,随着窗口左滑,左侧是店铺简介,右侧是一个商品详情,上下文关系不对。
本篇博客只是简述了多窗口导航的原理和一些code片段,具体的实现可以根据个人应用场景的需求自己定制,尤其要注意多窗口之间的上下文关系。下次再说说页面间通信和屏幕适配吧,Continuum其实也是一种屏幕适配。