前言 孔子說:"軟體是對客觀世界的抽象"。 首先聲明,這裡的"三維導航"和地圖沒一毛錢關係,"四核驅動"和硬體也沒關係,而是為了複雜的應用而發明創造的導航邏輯。說這是發明創造,也不是危言聳聽,因為它完全突破了傳統意義的頁面導航概念,看完了本博客以後,相信會讓你腦洞大開。當然這也是一種嘗試,只有UWP ...
前言
孔子說:"軟體是對客觀世界的抽象"。
首先聲明,這裡的"三維導航"和地圖沒一毛錢關係,"四核驅動"和硬體也沒關係,而是為了複雜的應用而發明創造的導航邏輯。說這是發明創造,也不是危言聳聽,因為它完全突破了傳統意義的頁面導航概念,看完了本博客以後,相信會讓你腦洞大開。當然這也是一種嘗試,只有UWP的出現才會帶來這種機遇,希望廣大開發者給予指正。
上周發佈了淘寶UWP的更新,地址在這裡:https://www.microsoft.com/zh-cn/store/apps/%e6%b7%98%e5%ae%9d/9nblggh6c2cd
以前淘寶UWP的mobile版叫“手機淘寶”,desktop版叫“淘寶HD”。上周把silverlight版的半殘“淘寶”下線了,把“淘寶”這個應用名稱拿回來了,現在終於把二者統一為“淘寶”了。
隨著UWP的誕生,一個App應該既可以在狹小的手機屏幕上運行,也可以在手持式平板電腦上運行,甚至可以在寬大的桌面電腦屏幕上運行。而對我們程式員來說,這才是真正的挑戰。Windows 10,一統天下的巨集偉計劃確實給操作系統本身的設計帶來了巨大的挑戰,同時也給UWP應用的開髮帶來同樣大的困難,因為它要求你的App可以運行在所有運行Windows 10的硬體上,而這些硬體有著截然不同的屏幕尺寸,從Band,到手機,到平板,到桌面電腦,到Hub。但從另外一個方面看,統一的操作系統和UWP卻給用戶帶來了統一的體驗,給整個產業帶來了同樣巨大的利益。
扯遠了……
一維Z軸導航模式
首先看看手機上運行的App採用的導航模式:
這是最簡單的導航模式了,從數據結構上看,是一個簡單的堆棧。A是起始頁面;在A上點擊個按鈕,進入B頁面;在B上點擊個圖片,又進入C頁面。按back鍵從C回到B,再按就回到A。
以下是淘寶UWP中,在手機屏幕上運行時的情況。
首先進入主頁A,然後點擊了"淘搶購"頻道,進入淘搶購的列表頁B,然後在B中點擊了一個列表項,進入該商品的詳情頁面C。
手機屏幕有限,一次只能看一個頁面,當用戶想看其它商品信息時,必須先返回到B頁,然後再選擇另外一個條目。
在UWP中,導航必須在Frame類上實現,下麵代碼中所示的BackStack,是Frame類中的一個屬性,專門用於保存導航歷史記錄:
// // Summary: // Gets a collection of PageStackEntry instances representing the backward navigation // history of the Frame. // // Returns: // The backward navigation stack. public IList<PageStackEntry> BackStack { get; }
當用戶按了back鍵時,你就可以簡單地用Frame.GoBack()方法來返回到上一頁了。
在Z軸導航模式中,我們使用App.xaml.cs里創建的Frame來做所有頁面的導航容器。
Frame rootFrame = new Frame(); Window.Current.Content = rootFrame; rootFrame.Navigate(typeof(PageA));
調用Frame.Navigate(typeof(PageA))時,會把Page A作為該Frame的Content,同時,這個Frame的值會傳送到Page A的Frame屬性中,兩者是一回事兒,是為了方便再次Navigation時使用。比如this.Frame.Navigate(typeof(PageB)),這裡的this實際上是Page A的實例,而this.Frame就是App.xaml.cs里創建的rootFrame。
Z軸導航的實際UWP的例子很多,如網易雲音樂。Windows Store應用商店App是另外一個例子,它的內容也是可擴展的,就是說在窄屏上,可以一行列出4個項目,在寬屏上可以根據屏幕寬度列出5~8個項目。還有一個例子就是著名的必應詞典Win10版啦,也是用了一個Frame搞定所有事情。
用這種模式最忌諱的事情是: 當應用場景稍微複雜一些時,也就是頁面較多(8個以上),有可能產生交叉跳轉,可能會造成用戶迷失;而在code方面,造成後退棧越來越大,不過這還是小事情。只要單個Frame的導航能滿足你的應用場景就可以。
舉例來說,如果我在必應詞典里的背單詞模塊中,想查一個詞,但不想跳轉到search頁,而是想在屏幕右側滑出一個區域來顯示該單詞的詞典解釋,那就需要兩個Frame來完成這個要求了。
不同的導航模式,對界面設計的要求是不同的,這一點請designer註意。在Z軸單頁模式中,只需要專註每個頁面的設計即可。但在後面的複雜導航模式中,只這樣做就不夠了。
一維Master/Detail導航模式
在寬大的桌面電腦屏幕上,上面那種在手機的窄屏界面的展示方式是不可接受的。如下圖的Windows 10中的設置頁面,它的視窗是可縮放的,可寬可窄,隨心所欲。在窄視窗中,顯示一頁的內容;在寬視窗中,side by side地顯示兩頁的內容,合理利用屏幕空間,同時簡化了用戶的操作: 用戶可以在右圖中的左側列表控制項中任意點擊感興趣的條目,在右側可以立刻得到響應;但是在左圖中,用戶點擊了Display,進入詳情頁,必須點back鍵回到列表框,才能再進入另外一個條目。
我們再來看看ITHome - IT之家UWP的界面:
整個屏幕分成三列,最左側的窄條Vertical Menu Bar,中間偏左部分的資訊列表,右半部份的資訊詳情。可以說這是一個標準的寬屏設計模式,易於理解,易於操作,適合推廣使用。
它對應的XAML應該是這個樣子:
1 <Grid Background="{ThemeResource WhiteColorBrush}"> 2 <Grid.ColumnDefinitions> 3 <!--vertical menu bar--> 4 <ColumnDefinition Width="48" x:Name="menuCol"/> 5 <!--list page container--> 6 <ColumnDefinition Width="2*" x:Name="leftCol" MinWidth="360"/> 7 <!--detail page container--> 8 <ColumnDefinition Width="3*" x:Name="middleCol"/> 9 </Grid.ColumnDefinitions> 10 11 <common:VerticalMenuBarControl x:Name="menuBar" Grid.Column="0"/> 12 13 <!--list content in this frame--> 14 <Frame x:Name="leftFrame" Grid.Column="1"/> 15 16 <!--detail content in this frame--> 17 <Frame x:Name="rightFrame" Grid.Column="2"/> 18 </Grid>
Column-0的寬度為48 epx (effective pixel),在點擊了上方的漢堡控制項展開菜單後,這個寬度可以調整為120左右。
Column-1和Column-2的常規比例為2:3,但是當視窗太窄時,Column-1的寬度有個限制MinWidth="360",避免裡面的內容無法以合理方式展示。當然也可以設置一個MaxWidth="720",避免太寬時橫向拉伸太難看。
關於epx的概念,請看這裡:https://msdn.microsoft.com/windows/uwp/layout/design-and-ui-intro 。改天把這篇東西翻成中文給大家洗洗腦吧。
這種設計適合於簡單的二級內容應用場景,想看三級片是不行的了。現在市面上大多數的UWP,都可以用這種方式來實現,如微博UWP就是另外一個典型。
如果這樣做的話,是否能讓mobile和desktop兩個device使用同一套UI代碼呢?當然可以,否則就不能叫做UWP了,只是需要我們多做一點點事情而已。具體需要做什麼事情,我們在另外一篇博客中(關於Win10 Mobile Continuum的使用方式)會詳細說明。
二維(Z+X)導航模式
用上述的master/detail模式,雖然可以充分利用屏幕,但是只適合於場景較為簡單的應用,如新聞類,閱讀類。下麵我們看一下稍微複雜一些的應用,比如旺信UWP。先看看截圖獲得感性認識:
聊天主頁,左側展示好友列表:
右側視窗為具體聊天頁:
最右側的浮出視窗為輔助的設置頁面,基本是popup類型的信息:
它的主控頁的XAML是這樣寫的:
<Grid Background="{ThemeResource WXWhiteColorBrush}"> <Grid.ColumnDefinitions> <!--vertical menu bar--> <ColumnDefinition Width="48" x:Name="menuCol"/> <!--list page container--> <ColumnDefinition Width="2*" x:Name="leftCol" MinWidth="360"/> <!--detail page container--> <ColumnDefinition Width="3*" x:Name="middleCol"/> </Grid.ColumnDefinitions> <!--left side vertical menu bar--> <common:VerticalMenuBarControl x:Name="menuBar" Grid.Column="0"/> <!--list content in this frame--> <Frame x:Name="leftFrame" Grid.Column="1"/> <!--detail content in this frame--> <Frame x:Name="middleFrame" Grid.Column="2"/> <!--popup content in this frame--> <Grid x:Name="rightFrameGrid" Grid.Column="0" Grid.ColumnSpan="3"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition Width="500" x:Name="rightCol"/> </Grid.ColumnDefinitions> <Rectangle Grid.Column="0" Fill="Black" Opacity="0.5"/> <Frame x:Name="rightFrame" Grid.Column="1"/> </Grid> </Grid>
- 最左側的是vertical menu bar
- 第二列是leftFrame,用於顯示主要Scenario的主頁面
- 第三列是middleFrame,用於顯示每個Scenario的詳細頁
- 最右側是rightFrame,用一個grid承載。這個grid用於遮蓋後面的menu bar, left Frame, middle Frame,形成性感的半透明遮罩效果;right Frame用於配置頁面的展示。
在旺信UWP中,我們使用了三個Frame做navigation。在子模塊開發前,就實現了一個導航framework,使得每個業務模塊的開發者只需要完成自己的頁面開發,然後直接調用該framework提供的導航方法即可。
當然,這裡的導航不再是this.Frame.Navigate()那麼簡單,但是也沒複雜到哪裡去,也就是多加了一個參數而已,叫做that.Nav.To(page, left|middle|right),由後面那個參數指定你將要展示的頁面在哪個frame里出現。這樣做的基本條件是,做好需求分析,仔細梳理頁面之間的邏輯關係,使得每個頁面都在一個預訂的frame里展示。
旺信的導航邏輯可以抽象成這個樣子:
具體是這樣運行的:
- 用戶登錄旺信後,首先展示的是聯繫人頁面,命名為A1;
- 用戶分別和三個聯繫人聊天,會使用X軸導航方式,在右側依次展示三個聊天頁面,A2/A2'/A2";在Middle Frame中,用了Z軸導航,承載三個頁面。
- 用戶在A2時做了兩次配置修改,使用X軸導航方式,依次出現了A3/A3'兩個頁面;在Right Frame中,用Z軸導航承載兩個頁面。
所以,此應用的導航是X+Z的二維模式。這種模式要求interactive designer對該應用的業務非常熟悉,上下文處理得很流暢,否則會有跳躍感。而對於user experience designer,更多地要考慮兩個頁面之間的差別和聯繫,和單頁設計的感覺完全不同,兩個頁面既不能長得完全不同(因為有上下文關係),又不能看著差不多,以為是讓用戶玩“找出不同處”的游戲,因為有上下級關係。
淘寶UWP的應用場景分析
迄今為止,我們遇到的最複雜的UWP就是淘寶了。打橋牌的人知道,橋牌是叫牌難,打牌難,計分難,發牌好像最簡單;淘寶是設計難,實現難,測試難,使用起來可是隨心所欲。
下麵我們先來看一個常見的用戶購物行為:
- 打開淘寶首頁,點擊淘搶購
- 進入淘搶購,點擊一個水果商品
- 進入詳情頁,點擊店鋪按鈕
- 進入該水果的店鋪,點擊了另一個水果
- 再次進入詳情,加入購物車,然後從超級按鈕跳轉按鈕中進入購物車
- 在購物車中忽然看到了一件以前收入的衣服,還沒下單,於是點擊立即購買按鈕
- 進入訂單頁,發現送貨地址應該修改一下,點擊了地址控制項
- 進入地址選擇頁,發現沒有自己想要的地址,於是點擊地址管理按鈕
- 進入地址管理頁,添加好了新地址,想看看以前的訂單的送貨地址有沒有搞對
- 進入我的淘寶,點擊我的訂單按鈕
- 進入訂單管理,增加了一個新的發貨地址
- 後面一系列折騰,整個購物過程經過了50多次頁面跳轉,歷時1小時……
琳琅滿目的商品,讓人流連忘返,迷失於虛擬的貨架之間。但是作為UWP開發者,我們不能迷失,要理智,理智,理智……老闆,那個快遞費能不能便宜些,再送一個贈品……回歸理智!
之所以有超級跳轉按鈕的設計,是因為你不可能讓用戶back無數次後才能找到購物車頁面,就好像在超市裡你必須推著一個購物車,可以隨時放置商品,並且可以有無數通道通向收銀台一樣。
下圖演示了這次購物行為的頁面跳轉過程(篇幅有限,圖片較模糊,見諒):
讓我們來仔細分析一下這個常見的購物行為。
第一眼看上去,我丂!頁面是可以亂跳的!根本不是普通的App那樣的進棧出棧的規矩,看上去像是一種網狀關係。再仔細看看!怎麼好像"詳情"頁出現了很多次呢?
在網購中,詳情是購物的關鍵環節,圖片,價格,評價,快遞,等等,它可以很大程度決定是否立刻剁手。
懷著剁手黨的激動心情,我們把詳情用藍色標記一下,變成如下圖:
都可以從哪些地方進入詳情頁呢?順藤摸瓜找上游業務鏈,那就是淘搶購,購物車,店鋪。還有很多其他頁面可以進入詳情,篇幅有限,不一一列出,比如天貓,聚划算等等。
於是乎,我們把詳情的上游業務標記成橙色的,如下圖所示:
這裡要註意,店鋪雖然在下圖中,處於詳情的下游,但是從真實場景看,店鋪的概念顯然是大於詳情的概念的,所以它實際是詳情的上游。那個鏈接只是系統提供的一種方便的跳轉而已。
我們還可以繼續剛纔的思路,看看更遠端的上游和下游是什麼?
先看上游,主頁是一個肯定的入口;進入淘寶後,也可以不幹別的,直接進入購物車,因此我們把購物車從橙色變為更高級的紅色;我的淘寶是和主頁、購物車併列的入口,也標為紅色;地址管理的scenario非常特殊,可以作為一個獨立的業務,因此也標為紅色。
再看下游,詳情後面緊跟著的業務應該是確認訂單和支付,當然也可以先和商家討價還價,進入旺信聊天頁面,這些個下游頁面我們標記為綠色的。如下圖所示:
圖中出現了4種顏色,紅色,橙色,藍色,綠色,也就是說,淘寶的絕大部分業務流程,都可以以詳情為核心,串聯起前後一共3~5步,成為業務鏈。而每條業務鏈,就是一個Scenario!
4色定理,怎麼讓我想起了微軟大田村的logo呢?
OK,讓我們照著這個思路,回過頭瀏覽一下淘寶中的所有頁面,大概能總結出以下幾種scenario:
- Home->H5(如天貓,聚划算)->詳情->旺
- Home(猜你喜歡)->詳情->旺
- Home->RushBuy->詳情->旺
- 購物車->詳情->旺
- 店鋪->詳情->旺
- 我的淘寶->H5->詳情->旺
- 我的淘寶->設置[list]->關於[detail]->版權信息[chat]
- 我的淘寶->訂單管理->詳情
- Home->搜索[list]->詳情[detail]->旺[chat]
- 消息(物流)->物流列表[list]->查看跟蹤[detail]
- 消息(商戶)->旺[chat]
- 收貨地址管理->ADRU
- ……
還有一些分支,處於第3層和第4層,我沒有列出來,不太重要。以上這些足夠我們整理出一個合理的需求分析了,如下示意圖:
也就是說,主頁、店鋪、購物車、我的淘寶、地址管理等,都可以作為後續一系列業務的入口看待,我們把它們叫做Scenario.Home;
淘搶購、搜索、訂單管理、天貓、聚划算等,都是一種業務頻道,引導用戶更方便快捷地購物,我們稱之為Scenario.List;
詳情是業務核心,成為Scenario.Detail;
後面的業務環節可以叫做Scenario.Chat,裡面可以放旺信聊天頁,也可以放下單頁等等。
淘寶,必須做成四核的,需要4個Frame參與導航,才能符合我們經過縝密需求分析得出的完美業務模型。
於是,我們可以把業務抽象成這個樣子:
- A是一個Scenario的名字;
- 1/2/3/4分別是四核導航的4個Frame;
- A1/A2/A2'/A2"/A3/A4/A4'是這個scenario所涉及的頁面,後面的序號代表它們應該在那個Frame上顯示,如果需要的話,在2/3/4三個frame上都可以做傳統的Z軸導航。比如在淘搶購中,在Frame 2上點擊一個商品,會在Frame 3上顯示一個詳情;在Frame 2上點擊另一個商品,在Frame 3上就會顯示另一個詳情。
三維(Z+X+Y)導航模式
如果你沒看懂前面說的二維(Z+X)導航模式,那就goto前一章節重新看。如果看懂了,那下麵的東西就很容易理解了,就是在那個基礎上多加了一維Y軸導航。
咱們還是看圖說話吧,請看下圖:
從上到下,一共三層(可以是N層)淡藍色的面板,每一層表示一個scenario的內部navigation,這一點與前面說的二維導航模式相同,只不過是多了一個frame 4。具體用幾個frame,是由這個App的Scenario決定的,原則上講,用的Frame的數量越少,對系統的performance越有利,code寫著也會簡單些。
淡藍色的面板的數量是隨著用戶navigation的深入而增加的,隨著按下back鍵而減少的,可以把藍色的面板定義為Scenario類。在下麵這張圖中,用戶首先在最下麵的Scenario A中暢游了6個頁面(A1,A2,A2',A3,A4,A4'),其中A1->A2做了X軸導航,也就是切換了Frame;在Frame 2中做了Z軸導航,然後從A2->A3做了X軸導航,然後再次X軸導航到A4,在Frame 4中做了最後一次Z軸導航之後,發現了一個神奇的按鈕,切換到了Scenario B,也就是到了第二層面板。以此類推,最後到了Scenario C中,又搞了一些事情。
此時,在Frame 1的導航歷史中,有3個頁面,A1/B1/C1;在Frame 2的導航歷史中,有7個頁面,A2/A2'/B2/B2'/C2/C2'/C2";在Frame 3的歷史中有個3個頁面,A3/B3/C3;在Frame 4中有7個頁面,A4/A4'/B4/B4'/B4"/C4/C4'。
用下麵兩個類可以表示以上數據模型:
public class Scenario { public FrameSlot HomeSlot; public FrameSlot ListSlot; public FrameSlot DetailSlot; public FrameSlot ChatSlot;
public Scenario(Frame scenarioFrame, Frame listFrame, Frame detailFrame, Frame chatFrame, string url) { this.HomeSlot = new FrameSlot(scenarioFrame); this.ListSlot = new FrameSlot(listFrame); this.DetailSlot = new FrameSlot(detailFrame); this.ChatSlot = new FrameSlot(chatFrame); this.Url = url;
} } public class FrameSlot { public Frame frame; // current frame int depth = 0; // navigation times }
在Scenario類的初始化函數中,從外面指定了Frame的實例,也就可以保證不同的Scenario實例可以共用同一組Frame。
而這些Frame,可以這樣在XAML中定義:
<Grid x:Name="gridRoot" SizeChanged="gridRoot_SizeChanged"> <Grid.ColumnDefinitions> <!--vertical menu bar--> <ColumnDefinition Width="64" x:Name="menuColumn"/> <ColumnDefinition Width="*" x:Name="contentColumn"/> </Grid.ColumnDefinitions>
<localControls:VerticalMenuBarControl2 x:Name="menuBar" Grid.Column="0"/> <!--for content page--> <Canvas x:Name="panel" Grid.Column="1"> <Frame x:Name="homeFrame" Canvas.ZIndex="10"/> <localControls:VerticalSeperatorControl x:Name="listSep"/> <Frame x:Name="listFrame" Canvas.ZIndex="20"/> <localControls:VerticalSeperatorControl x:Name="detailSep"/> <Frame x:Name="detailFrame" Canvas.ZIndex="30"/> <localControls:VerticalSeperatorControl x:Name="chatSep"/> <Frame x:Name="chatFrame" Canvas.ZIndex="40"/> </Canvas> </Grid>
從XAML中可以看到,最左側是個Vertical Menu Bar;右側是個Canvas面板。之所以使用Canvas面板,是因為這個panel類型最簡單,可以任意調整裡面Frame的位置。其它的panel,如Grid, StackPanel, RelativePanel等等都有自己的一套Arrange Children的演算法,我們並不需要。當然,你也可以自己從panel類派生出一個MyTaobaoPanel類,來組織這4個Frame。
好了,到目前為止,我們經過需求分析,找出了合理的模型,制定了基本類和界面邏輯,下麵可以著手開發了……此處略去10000字的開發過程……最後的樣子如下麵的截圖所示:
主頁:
在主頁上點擊了淘搶購:
在淘搶購中點擊了第一個商品:
在詳情中點擊了旺旺:
在詳情中點擊了店鋪:
在左側的超級菜單中點擊了我的淘寶:
在我的淘寶中點擊了設置:
頁面太多了,上百種navigation的組合路徑,一網打盡!
現在各位看官可能明白了,“四核”指的是4個Frame參與頁面展示與導航,”三維“指的是在三個維度上的導航方向,一維是在一個frame上的Z軸棧式導航,二維是在兩個frame以上的橫嚮導航,三維是在兩個Frame以上的橫嚮導航,再加上虛擬層之間的Y軸導航,切換場景相當於OS的進程切換,只不過是棧式的。
孔子還說:"完美的抽象不能決定軟體的成功與否,但能判別開發者的素質。"
老子也說:"好的開發者是一個軟體成功與否的基石。"
莊子說:……哎,讓莊子喝口茶水吧,實在編不下去了,意思你們都知道的,大家努力提高自己的職業素質吧,多學習,多觀察,多思考,多實踐,更好地抽象這個世界。