在windows phone 中采用數據列表時為了保證用戶體驗常遇到加載數據的問題.這個問題普遍到只要你用到數據列表就要早晚面對這個問題. 很多人會說這個問題已經有解決方案. 其實真正問題並不在於如何實現列表數據動態加載? 而我們真正目標是如何使這種加載方式達到用戶在操作時良好的用戶體驗. 基於用戶體驗合理性要高於功能本身的實現.
而這種合理性主要體現在什么時候需要加載數據? 什么時候需要數據本地緩存加速本地UI響應? 也是說我們出發點是基於產品用戶體驗.需要我們在列表動態加載上加以一定加載策略進行操作行為上的約束. 用來達到這個目的. 在WP平台上如果你留意.會發現每當遇到這樣的涉及用戶體驗的問題時.我們也會通常會看看其他平台是做法.不妨也是一種開拓思路. 從Android 和IOS 平台角度來看. 幾種常見加載數據的方式.
[方式1]: 自動下拉加載

這種方式比較常見.通常一個獨立的數據列表中. 在我們第一次進來時列表加載最新數據.當用戶需要獲取更多或是更舊的數據時.用戶向上滑動.當滾動到UI底部時自動加載更多的數據.特點是 自動加載 避免更多手動的操作. 在網絡通暢情況 列表操作流暢. 確定是用戶無法控制整個數據過程.
[方式2]:手動下拉加載

方式1采用的用戶下拉到UI底部時自動加載.整個加載過程是用戶是可不控.即 無法實現用戶只在需要時才手動啟用加載更多或更舊的數據方式.二方式2當用戶滾動UI時可以選擇是否加載更多數據.用戶能夠對整個數據加載過程進行控制.
[方式3]:UI提示加載

UI提示加載的方式和方式1 、2完全不同.當用戶下拉時加載更多數據時. 會提示彈出一個UI提示層. 對加載進度進行提示. 在數據加載過程中整個LiveView時無法進行任何UI操作的.用戶只能等待數據加載完成才能重新操作UI. 這點在很多Pc平台項目見到很多.
[方式4]:下拉刷新

當用戶第一次進來時.列表中獲取到最新數據時. 如果這個列表時隨着時間點會發生數據動態變化時. 用戶就希望在當前頁面就能獲取到最新的數據. 這個時候下拉刷新價值就體現出來了. 而不需要重新進入這個頁面來獲取最新數據.下拉刷新整個操作流程是. 用戶在UI頂部區域下拉整個列表.當用戶手勢離開UI頂部區域時. 列表自動回到頂部.並開始加載最新的數據.更新到ListView中來. 在加載過程中用戶依然可以隨意操作當前UI數據.
如上四種方式時Android和Ios中比較常見的數據加載方式. 當然在Ios中還看到類似Pc端數據分頁. 還包含采用一些自定義動畫方式獲取更好的加載體驗. 拋開這些不談.我們就從這些最基本的加載方式入手.來談談如何在Windows Phone 中數據列表中獲得最好的加載體驗.
我們目前需求時在一個豎屏中有一個ListBox. 希望用戶通過手勢操作方式能夠實現操作獲取到最新和更舊的數據.那我們從如上四種獨立加載方式來看.結合四種方式優缺點.設計一下windows phone 數據列表加載策略 總結如下:
WP ListBox數據加載策略:
A:列表頂部區域支持下拉數據刷新.
B:當用戶滑動操作時滾動列表到最底部時 可以加載更多舊的數據
C:當用戶滑動操作時從列表底部滾動頂部時 依然支持可以加載最新的數據.
明確了我們需求既加載策略. 來嘗試Windows Phone 單個獨立類表嘗試實現如上三個特點.
列表上下滑動加載
從上面三點加載策略來看. 我們首先來實現. 列表中上下滑動加載數據. 也就是當用戶滾動UI底部時自動加載更舊的數據. 當用戶滾動頂部自動加載最新的數據. 頁面采用加載數據集合就采用常用ListBox來演示這個實例.
首先我們構建一個Project 命名為DynamicLoadData 在MainPage添加一個默認的ListBox控件:
1: <!--ContentPanel - place additional content here-->
2: <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
3: <ListBox x:Name="DynamicLoadData_LB"></ListBox>
4: </Grid>
眾所周知.實現Listbox滑動加載數據.很多人都會采用網上一種比較通用的方式.即采用監聽ListBox的MouseMove事件. 當手勢操作列表上下滑動會觸發該事件. 事件觸發后. 通過檢測ListBox.VerticalOffSet當前滾動條位置.再同ListBox.ScrollableHeight滾動條能達到最大位移兩者之間的間距差. 來判斷是否到達底部. 加載新的數據.
但你會發現會存在一個問題. 在某些手勢操作時 會突然發現Listbox已經滾動底部卻沒有執行加載數據的操作. 邏輯雖然正確但操作時卻時靈時而不靈 其實這個問題根本原因是因為. ListBox.MouseMove事件是只有的你的手指觸摸到屏幕上並且滑動屏幕才會觸發.但只要你的手指離開屏幕. 類似在離開前用力下滑. 你會發現listbox已經到了底部卻沒有觸發這個加載事件. 主要因為當前手勢已經離開了屏幕 MouseMove事件就不會被觸發.哪怕ListBox已經滾動到底部了.
同樣我們也知道ListBox控件本身就內置了ScrollViewer. 同樣的思路我們通過判斷當前ListBox 的VerticalOffSet 和內置ScrollViewer實際滾動位置進行比較. 來判斷當前滾動是到達頂部或底部.
首先獲取ListBox中ScrollViewer控件:
1: public static List<T> GetVisualChildCollection<T>(object parent) where T : UIElement
2: {
3: List<T> visualCollection = new List<T>();
4: GetVisualChildCollection(parent as DependencyObject, visualCollection);
5: return visualCollection;
6: }
7:
8: public static void GetVisualChildCollection<T>(DependencyObject parent, List<T> visualCollection) where T : UIElement
9: {
10: int count = VisualTreeHelper.GetChildrenCount(parent);
11: for (int i = 0; i < count; i++)
12: {
13: DependencyObject child = VisualTreeHelper.GetChild(parent, i);
14: if (child is T)
15: visualCollection.Add(child as T);
16: else if (child != null)
17: GetVisualChildCollection(child, visualCollection);
18: }
19: }
獲取ScrollViewer控件並訂閱其垂直水平ValueChanged事件 實現如下:
1: private void RegisterScrollListBoxEvent()
2: {
3: List<ScrollBar> controlScrollBarList =GetVisualChildCollection<ScrollBar>(this.WholeCityPictureFllow_LB);
4: if (controlScrollBarList == null)
5: return;
6:
7: foreach (ScrollBar queryBar in controlScrollBarList)
8: {
9: if (queryBar.Orientation == System.Windows.Controls.Orientation.Vertical)
10: queryBar.ValueChanged += queryBar_ValueChanged;
11: }
12: }
在ValueChange事件中判斷其到達最頂部還是最底部:
1: void queryBar_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
2: {
3: ScrollBar scrollBar = (ScrollBar)sender;
4: object valueObj = scrollBar.GetValue(ScrollBar.ValueProperty);
5: object maxObj = scrollBar.GetValue(ScrollBar.MaximumProperty);
6: object minObj = scrollBar.GetValue(ScrollBar.MinimumProperty);
7:
8: if (valueObj != null && maxObj != null)
9: {
10: double value = (double)valueObj;
11: double max = (double)maxObj;
12: double min = (double)minObj;
13:
14: if (value >= max)
15: {
16: #region Load Old
17: #endregion
18: }
19:
20: if (value <= min)
21: {
22: #region Load New
23: #endregion
24: }
25: }
26: }
如上通過判斷判斷listbox當前位置和最大滾動區域Max和Min進行對比來判斷當前滾動是否到頂或底部. 方法及其簡單. 值得提到一點是. 我們到達頂部判斷不需要額外處理. 有時我們UI元素比較豐富時. 我們希望保證下滑操作時不希望因為數據加載操作導致UI出現卡頓. 這里需要有兩個需要額外控制一下. 如果你每次加載數據類似30條排版內容最好多出整個屏幕. 另外我們需要在下滑時觸發加載時. 要把Max-100或是適當的值. 這樣的做目的是用戶向下滾動不用滾動底部才開始加載. 而是快到達到底部時就已經開始預加載數據. 在網絡穩定情況下回操作UI列表更為流暢.
如上實際加載效果還需要微調才能達到最佳. 已經上下滑動加載.
so 在來重點說說 下拉刷新.
下拉刷新
說道下拉刷新.恐怕在Windows Phone上應用每天用的最頻繁應該就是Sina微博了.和IOS上效果基本一致 效果如下:

當用戶下拉時 數據列表頂部會顯示 一個向下箭頭和下拉刷新的文字提示. 緊接着提示松開自動刷新. 松開手勢操作 列表回到頂部.自動開始加載最新數據.並更新數據到ListBox中來, 整個流程如上.首先來分析一下如何實現思路?
因Listbox基本所有我們需要操作事件和屬性. 基於ListBox我們重寫一個控件RefreshListBox.首先來看看頂部提示區域如何實現.
其實ListBox的Template實現基於ScrollViewer控件中放置ItemsPresenter. ItemsPresenter是用來在項目控件模板中指定在 ItemsControl 定義的 ItemsPanel 要添加的控件的可視化樹.那么我們只需要在一個Grid把提示區域放在ItemsPresenter上面就可以在下拉是看到整個提示區域. 類似這樣自定義ListBox的模板:
1: <ControlTemplate TargetType="local:RefreshBox">
2: <ScrollViewer x:Name="ScrollViewer" ...>
3: <Grid>
4: <Grid Margin="0,-90,0,30" Height="60" VerticalAlignment="Top" x:Name="ReleaseElement">
5: <!-- Tip Area Here -->
6: </Grid>
7: </Grid>
8: <ItemsPresenter/>
9: </ScrollViewer>
10: </ControlTemplate>
在加載控件時. 我們需要獲取到自定義控件RefreshListBox內置滑動ScrollViewer並訂閱其MouseMove和ManipulationCompleted事件. 並拿到提示區域ReleaseElement對象的引用. 重寫OnApplyTemplate方法:
1: public override void OnApplyTemplate()
2: {
3: base.OnApplyTemplate();
4: if (ElementScrollViewer != null)
5: {
6: ElementScrollViewer.MouseMove -= viewer_MouseMove;
7: ElementScrollViewer.ManipulationCompleted -= viewer_ManipulationCompleted;
8: }
9:
10: ElementScrollViewer = GetTemplateChild("ScrollViewer") as ScrollViewer;
11: if (ElementScrollViewer != null)
12: {
13: ElementScrollViewer.MouseMove += viewer_MouseMove;
14: ElementScrollViewer.ManipulationCompleted += viewer_ManipulationCompleted;
15: }
16:
17: ElementRelease = GetTemplateChild("ReleaseElement") as UIElement;
18: ChangeVisualState(false);
19: }
當SrollViewer為Null訂閱事件操作時.如果在不同SDK版本[WP7 Or WP8]執行過程發現訂閱的ManipulationCompleted沒有被觸發. 可以采用如下方式來強制添加處理事件[在WP7 And WP8 均測試有效] :
1: ElementScrollViewer.AddHandler(ScrollViewer.ManipulationCompletedEvent,
2: new EventHandler<ManipulationCompletedEventArgs>(viewer_ManipulationCompleted), true);
在MouseMove事件中.通過判斷ListBox的VerticalOffset 當它等於0;既在頂部.當下拉超過一定距離是開始提示下拉刷新更新RealseElement元素中提示信息:
1: private void viewer_MouseMove(object sender, MouseEventArgs e)
2: {
3: if (VerticalOffset == 0)
4: {
5: var p = this.TransformToVisual(ElementRelease).Transform(new Point());
6: if (p.Y < -VerticalPullToRefreshDistance) //Passed thresdhold : In pulling state area
7: {
8: //TODO: Update layout//visual states
9: }
10: else //Is not pulling
11: {
12: //TODO: Update layout/visual states
13: }
14: }
15: }
同樣的邏輯.在ManipulationCompleted事件中當用戶完成手勢操作時觸發.如果當前ListBox VerticalOffset 等於0 也就是位於頂部時. 松開時手勢時 listBox回到頂部並開始加載最新列表數據並更新列表:
1: private void viewer_ManipulationCompleted(object sender, ManipulationCompletedEventArgs e)
2: {
3: var p = this.TransformToVisual(ElementRelease).Transform(new Point());
4: if (p.Y < -VerticalPullToRefreshDistance)
5: {
6: //TODO: Raise Polled to refresh event
7: }
8: }
這樣整個下拉刷新的基本邏輯實現思路已經很明朗.可以完整重寫整個ListBox實現.
當第一次進來加載數據:

下拉是效果:

剛松開效果:

這樣下拉刷新結合ListBox本身上下滑動刷新基本實現我們如上三個需求.
源碼下載[https://github.com/chenkai/LoadData]
Contact: @chenkaihome
