課程內容
Ø Panarama控制項
Groceries是一個簡易的購物清單應用程式,我們可以用它來一步一步建立自訂的購物清單。根據個人的喜好,我們可以命名並添加儘可能多的購物頁面。能夠方便地添加記錄,這是本應用程式的特點,比如,大量新增、選擇最喜歡的商品以及選擇最近購買的商品等等。
Groceries應用展示了Panorama控制項,這是Windows Phone平台上具有標誌意義的使用者控制項,它被廣泛地應用於手機上的“hub”介面(例如人脈、圖片等等)。粗略地說,Panorama控制項的行為與Pivot很類似,它允許在一個頁面的不同部分之間進行水平切換。Panorama的與眾不同之處就在於它的外觀和動態切換。
Panorama的核心理念就是讓使用者感覺是在瀏覽一幅很長的水平放置的油畫。該控制項向使用者給出了一些視覺上的元素,指引使用者進行水平切換。比如,應用程式的顯示的標題要比螢幕的尺寸大(除非標題實在太短),每個Section的大小要比螢幕的尺寸略窄,所以下一個Section的左邊界部分就可以在這個介面中顯示出來。Panorama支援水平復原,在最後一個Section繼續向右切換,就會回到第一個Section。同樣,在第一個Section向左切換,就會導航到最後一個Section頁面。
圖27.1展示了Groceries應用程式中Panorama控制項的用法。第一個Section包含了整個購物清單,最後一個Section包含了購物車(使用者已經購買的商品)。在這兩個部分之間,可以動態添加多個Section,存放使用者希望購買的商品清單,並且展示這些商品是否已經放入購物車。
圖27.1 Groceries應用程式中的Panorama控制項,展示了購物清單。
雖然長的水平畫卷的方式是Panorama控制項一貫的介面風格,但這五個Section中的背景圖片並不是完全連續的。事實上,這個機制要更加複雜一點。Panorama控制項包括三個不同的層,每個層的移動速度不一樣,以達到一個視差的效果。背景平移的速度最慢,其次是標題,平移速度最快的是內容,它以普通的滾動/切換的速度進行平移。圖27.2展示了訪問圖27.1中的每個Section時,螢幕所展示的頁面內容。
圖27.2 訪問圖27.1中Panorama的每個Section時,展示的頁面內容。
在應用程式中,我們應該如何選擇使用Panorama或者是Pivot控制項?
主要的考慮因素是應用程式想要呈現給使用者的視覺外觀。與Pivot相比,具有背景畫面的Panorama控制項可以提供更具吸引力、更加有趣的使用者介面。即使在Pivot中使用背景圖片,它也不能達到Panorama的效果,主要原因是Panorama提供的視差平移效果。另外,Panorama在同一個Section中支援更加好的水平滾動,這使得寬度可變的Section更容易實現。
在其他方面,Pivot也有勝過Panorama的優勢。Pivot展示每個Section更加真實的狀態,在內容或者記錄數目較多時,Pivot的效能更加出色,原因有三點:外觀和過渡更加簡潔,採用內容的延時載入機制,為延時載入與卸載提供API。在Pivot中,仍然可以使用應用程式欄(或者程式狀態列),而在Panorama中一般不適用它們。因此,如果我們想要展示一些基於頁面的行為,那麼,具有應用程式欄的Pivot應該是一個更好的選擇。
Groceries應用程式其實應用更適合使用Pivot,而不是Panorama,因為每個頁面只是同一個資料集的不同過濾頁面而已。一個典型的Panorama中的Section要比Groceries應用中的更加豐富與生動,具有更加多的縮圖(就像我們在Marketplace應用程式中所看到的那樣)。但是,通過使用Panorama,Groceries給使用者留下了深刻的印象,使用起來也更加有趣。
The Panorama Control
在閱讀前一章“Pivot”控制項之後,Panorama控制項看上去應該是比較熟悉了。Panorama控制項位於Microsoft.Phone.Controls二進位集的Microsoft.Phone.Controls命名空間中,它是一個item控制項,與PanoramaItem這個content控制項配合使用。
雖然Panorama控制項的行為要比Pivot複雜得多,但是它提供的API更加少。與Pivot一樣,Panorama具有Title 和 TitleTemplate屬性,其中,HeaderTemplate屬性用來自訂控制項中的header。一般情況下,沒有必要使用這個屬性,因為控制項已經提供了很好的外觀和感受。因為Panorama的Title是一個類型對象,所以把它設定為logo而非文本,是可以接受的。著名的Facebook應用程式就是這樣做的。
PanoramaItem具有Header屬性,但是與PivotItem不同,它也為自訂不同外觀的Header提供了HeaderTemplate屬性(當然,我們可以直接把Heade設定為使用者自訂的UI元素,而不需要HeaderTemplate屬性)。同時,PanoramaItem具有一個螢幕方向的屬性,它可以在內容不合適時,指示合理的滾動方向。該屬性的預設值是Vertical,將它設定為Horizontal時,可以使得單個Panorama Item的橫向展開寬度大於整個螢幕的寬度。注意,如果我們想要Panorama Item進行垂直的滾動,就必須加入scroll viewer控制項。在水平的Panorama Item中,我們不會想著使用scroll viewer控制項,Panorama自動會處理。每個水平的Panorama Item具有一個最大的寬度,那就是兩個螢幕的大小(960像素)。
Horizontal Panorama Items and Their Headers
系統內建應用程式中的Panorama控制項, Panorama Item在水平狀態並且比螢幕要寬時,它的標題的平移速度要比內容的平移速度慢(這就確保了在查看Panorama Item頁面時,我們只能看到標題的部分內容)。但是,Panorama控制項並不提供這個行為。無論它的寬度有多大,每個Panorama Item的標題移動速度和內容的移動速度相同。
對於Panorama Item中記錄的布局來說,我們可以自行設定。雖然Panorama中會使用一些方形圖片和文字,但並沒有特殊的控制項會自動完成這些布局的設定。我們應該使用通用的Panel控制項,例如Grid或者Wrap。
The Main Page
Groceries應用程式的首頁面27.2所示,只有它使用了Panorama。首頁面提供了導航到其他四個頁面的連結:添加記錄頁面、編輯記錄頁面、設定頁面和說明頁面。這些頁面的代碼說明在這裡省略。
如果應用程式使用了Panorama,一般只使用一個,而且一般也只是用在應用程式的第一頁。在一個應用程式中使用多個Panorama,會給熟悉Windows Phone體驗的使用者帶來使用上的困惑。
The User Interface
➔ 控制項的XML命名空間再次添加了對panorama的引用。
➔ 本頁面只使用豎屏模式,這也正是我們對每個具有panorama控制項的頁面所期望的行為。
➔ 本頁面填充了白色的前景色彩,這正是考慮到了在light主題和dark主題下,應用程式的外觀保持一致。因為背景圖片沒有改變,所以我們不想讓文字的顏色變為黑色。
➔ Panorama的背景和其他元素中的Background屬性類似。雖然設計指導中建議我們使用純色的畫刷或者圖片畫刷,但我們可以把它設定為任何的畫刷。該列表利用圖片畫刷將背景設定為background.jpg。
確保Panorama應用程式在dark 和 light兩種主題模式下測試通過!
這對於任何Panorama應用程式都是必須做的一項測試,因為我們經常在設計Panorama時犯錯,那就是設定一個固定的背景圖片。如果背景從不改變,那麼我們就需要確保內容的顏色也從不發生改變。
為了防止圖片展開,確保我們使用的Panorama背景圖片的高度為800像素。另外,為了保證我們應用程式的效能,圖片的寬度不應該大於1024像素,並且圖片的類型應該是JPEG格式的。Groceries使用了一張解析度為1024x800的 JPEG圖片。在我決定寫這個應用程式時,帶著我妻子的具備拍攝Panorama圖片功能的新相機去一個附近的雜貨店拍攝了圖片。而這之後,我意識到最好的背景圖片其實並不是Panorama類型的。圖27.3顯示了應用程式的背景圖片檔案。
圖27.3 應用程式的背景圖片檔案
使用超大解析度的背景圖片會導致背景切換遲滯的問題。事實上,背景切換的速度取決於Panorama Item數量,因為Panorama保證在你切換到最後一頁時,才會看到背景圖片的結尾。在Groceries應用中,標題“groceries”和背景圖片的寬度導致標題與背景圖片基本上以相同的速度切換,為了獲得更加豐富的視差效果,我們可以改變其中任何一個元素的寬度。
為了獲得最好的效果,Panorama應用中的背景圖片的Build Action屬性應該設定為Resource,而並不是Content。實際應用中很少使用資源檔,本應用是其中之一,原因在於同步和非同步載入/解碼之間的差異。如果圖片比較大,並且作為content檔案,Panorama就有可能在背景圖片顯示之間出現。如果作為resource檔案,Panorama就會在圖片準備好以後載入。resource檔案的同步載入機制,通常被認人們詬病,但在這裡卻保證了應用程式的友好體驗。雖然增加了Panorama顯示需要的時間,但人們還是不希望圖片背景在Panorama之後載入。其實,我們可以使用活動的UI元素作為Panorama的背景!Panorama控制項的建立者,本書的技術編輯,微軟員工Dave Relyea共用了這個成果,你可以在這裡看到:http://bit.ly/panoramaxaml。
➔ 由於Panorama是水平切換的,因此在背景右邊沿與左邊沿的串連處,會出現一條“縫隙”,除非我們使用指定的美工設計(如遊戲Hub)或者是純色的背景(如人脈Hub)。縫隙存在也沒有問題,只要使用者習慣就好,這也有助於提示使用者控制項將要復原(我們可以在圖片和Marketplace Hub中看到這條縫隙)。但是,Groceries使用的背景圖片邊沿有一些陰影,使得切換過程更加的平滑。27.4所示。
圖27.4 背景圖片的陰影使得Panorama控制項從最後切換到開始頁面的過渡更加平滑。
即使選擇使用美工設計的圖片,1個像素寬度的背景色縫隙在頁面復原過程中也偶爾會被使用者看到。我們仍然可以通過設定一個新Panorama控制模板來解決這個問題。它可以是預設範本的一份拷貝,其background邊界具有負邊距,如下:
<Border x:Name=”background” Background=”{TemplateBinding Background}”CacheMode=”BitmapCache” Margin=”-1,0”/>
➔ Panorama包含了兩個始終顯示的Item:等待購買的所有物品的清單和購物車清單。中間的一些頁面通過代碼來動態添加。
➔ “list” 這個Panorama Item的Header是使用者自訂的,在通常的標題文本邊上,它有三個按鈕:一個用來添加新購物清單,一個用來進行參數設定,還有一個是協助,詳見圖27.2。一般來說,這些應該設定為應用程式欄的按鈕,但因為在Panorama的設計指導中,指明了最好不要使用應用程式欄,所以就把它們放在這個地區中去了。
➔ “購物車”清單也具有一個自訂的Header,我們在它的文本旁邊加入了一個“刪除”按鈕。其他的Panorama Item(主要通過代碼添加)只包含了一個list box,但“購物車”清單包含了一個Grid控制項,用來在list box背後加入一個明顯的“購物車”表徵圖。
➔ 按鈕的使用貫穿了整個應用,它們具有自訂的“SimpleButtonStyle”風格。在這種風格中,每個按鈕具有新的控制項範本,移除了按鈕的border、padding和其他行為,所以我們看到的只是按鈕的文字內容(它同時還加入了本書中使用的標題效果)。
➔ 如果每個按鈕採用預設的樣式(調整了按鈕的布局,使得它們都能夠顯示在介面上),那麼它們的效果27.5所示。在這裡使用按鈕控制項的原因是:按鈕的單擊事件只有在使用者的單擊動作下觸發,而非平移動作。這就使得使用者可以在無意中點擊按鈕時,也可以對Panorama進行平移。如果使用MouseLeftButtonUp事件來檢測使用者對UI元素的點擊,那麼在UI元素上的平移操作將會觸發原來點擊行為的事件。
圖27.5 填充了按鈕的Groceries應用介面,它可以很容易地檢測使用者非平移的點擊。
在Panorama和Pivot控制項中,避免使用原始的滑鼠事件,如MouseLeftButtonDown、MouseMove和MouseLeftButtonUp!因為整個控制項的平移受使用者手勢的控制,對於這些事件中任何附加的使用者邏輯來說,它就必須處理使用者的平移手勢。我們可以尋找其他不會被平移手勢觸發的事件來替代,比如按鍵的單擊事件或者list box的SelectionChanged事件等等。
The Code-Behind
➔本應用程式維護著一個購物清單列表(Settings.AvailableItems),該列表記錄的是使用者加入的每個清單。根據每個商品的屬性,Panorama中的每個list box正是這個列表進行條件過濾後的視圖。這些列表具有靜態屬性,比如FilteredLists.Need(整個“List”列表中的商品)和FilteredLists.InCart(購物車列表中的商品)。與前一章中依靠填寫表格的頁面進行過濾的方式不同,這些列表由內部邏輯實現過濾。這就使得首頁面可以對每個list box使用資料繫結。這些列表以及代表每件商品的Item類型會在接下來的“Supporting Data Types”一節中介紹。
➔RefreshAisles負責動態地往第一個Panorama item頁和最後一個Panorama item頁之間的頁面填寫資訊。每個動態頁面由自訂的AislePanoramaItem控制項(繼承自PanoramaItem)來封裝。該控制項會在下一節中介紹。Panorama item只添加使用者自訂的頁面,該頁面中的商品最終有可能會被添加到購物車。
➔對每一個動態Panorama item頁使用各自的過濾集,這種方法的效率並不高,因為每個FilteredObservableCollection(它的實現會在稍後講解)必須通過傳入的可用Item列表來迭代。如果Item列表非常大的話,有可能需要選擇一個新的策略。
➔本應用程式證明了如何來實現Panorama item的動態卸載,在動態網頁面中的所有商品均放入購物車以後,就會觸發該行為。但是,與Pivot類似,Panorama並不對它的Item移除進行優雅的處理。其存在的問題有兩點:尋找移除Panorama item的合適時間點,以及它對視差效果的影響。
➔為了消除疑惑,在使用者切換視圖以後,才會將一個空的Panorama item移除。所以,這就需要由代碼在Panorama的SelectionChanged事件處理中進行檢查。在該事件處理過程中,前一個顯示頁以唯一的頁面存放在RemovedItems集合中。因為立即移除的效果會與平移過渡的效果類似,而平移動作會觸發SelectionChanged事件,所以處理常式使用DispatcherTimer在之後的半秒鐘內進行移除操作。實際上,這種處理效果非常好。唯一存在的問題就是:由於背景的移動,而且標題基於整個Panorama的寬度,如果移除一個Item的話,會減少整個Panorama的寬度,這就會導致背景和標題的突然抖動。除非這種情況發生時,我們停留在Panorama的第一頁。很遺憾,我們沒有辦法避免這種情況的出現,除非不移除Panorama item。
➔ Storyboards 被用來類比商品加入/移出購物車的效果。對購物車列表做的改變發生在Completed事件處理當中,這是通過設定Item的Status屬性為InCart或者Need來完成的。這會在列表發生改變後,觸發一個屬性更改的通知,由於採用了資料繫結,這兩個列表會自動完成更新。與Pivot Item不一樣,將Panorama Item的Visibility設定為Collapsed以後,可以成功隱藏真箇Item。即通過更改Panorama Item的可見度,而不是添加或者刪除Item。但是,隱藏Panorama Item同刪除操作一樣,都存在抖動的情況。
Panorama無法通過編程來設定當前的Panorama Item! 本應用程式明顯遺漏了一個設定,該設定用於記錄當前的Panorama Item,使得程式下一次啟動或者啟用時,當前的Panorama Item能夠恢複。但是,Panorama的SelectedIndex和SelectedItem屬性是唯讀,所以儘管我們可以儲存它們兩個中任何一個的值,但卻在程式啟動或者啟用時,無法恢複Panorama Item。
Panorama具有一個可讀寫的DefaultItem屬性,它可以立即改變螢幕顯示的Panorama item,但是可能和你所期望的不大一樣。它會移動Item,使得DefaultItem變成虛擬Canvas上的第一個Section,27.6所示。這意味著標題與當前的預設Item對齊,背景圖片的縫隙立刻移到了Item的左邊。在Groceries應用程式中,背景圖片的縫隙出現在除購物車和所有商品頁面中的話,會給使用者帶來疑惑。因此,DefaultItem屬性並不適合讓使用者迴歸到他們登出的頁面。
The AislePanoramaItem Control
AislePanoramaItem可以作為一個使用者控制項的形式加入到Visual Studio工程中,但是它的基類從UserControl變成了PanoramaItem。這樣做是為了使得AislePanoramaItem這個子類可以像使用者控制項一樣,獲得方便的XAML支援。 這個Panorama Item和首頁面上的第一個Panorama Item很類似,但是在Item模板中沒有編輯按鈕。就像應用程式中RefreshAisles方法所做的那樣,這種方便的打包方式使得它可以很容易被首頁面重用。
Supporting Data Types
➔Status枚舉定義如下:
public enum Status
{
Need, // In the current shopping list (but not in the cart yet)
InCart, // In the cart
Unused // Added at some point in the past, but not currently used
}
➔IsFavorite屬性在“添加”頁面和“編輯”頁面中使用,協助使用者管理商品輸入。
➔屬性更改的通知使得過濾集合可以保證商品出現在正確的列表分類中。它們使得單個商品資訊保持最近的更新。比如,在Item的IsFavorite狀態發生改變以後,“添加”頁面使用了一些值轉換器來顯示或者隱藏按鈕。
➔AvailableItems設定用來儲存列表中的所有商品資訊。
➔本應用中使用的過濾列表並沒有被程式儲存,而是由單個列表在程式運行時進行初始化。
➔每個FilteredObservableCollection在ReadOnlyObservableCollection中被封裝,這樣做是為了避免使用者直接嘗試修改資料集合。
➔該類的建構函式中有兩個參數:一個來源資料集和一個返回單條記錄是否屬於過濾列表的回呼函數。這使得每個執行個體都可以使用不同的過濾器,就和FilteredLists靜態類中一樣。該類中使用的記錄類型必須實現INotifyPropertyChanged介面,因為該類在監視來源資料集的添加和刪除操作的同時,也要跟蹤每條記錄的屬性更改(這是Groceries應用程式的需求,因為類似Status或者IsFavorite這些屬性的改變必須立即反應在顯示的列表當中)。