Windows phone應用開發[18]-下拉重新整理

chenkai發表於2013-09-30

在windows phone 中採用資料列表時為了保證使用者體驗常遇到載入資料的問題.這個問題普遍到只要你用到資料列表就要早晚面對這個問題. 很多人會說這個問題已經有解決方案. 其實真正問題並不在於如何實現列表資料動態載入? 而我們真正目標是如何使這種載入方式達到使用者在操作時良好的使用者體驗. 基於使用者體驗合理性要高於功能本身的實現.

而這種合理性主要體現在什麼時候需要載入資料? 什麼時候需要資料本地快取加速本地UI響應? 也是說我們出發點是基於產品使用者體驗.需要我們在列表動態載入上加以一定載入策略進行操作行為上的約束. 用來達到這個目的. 在WP平臺上如果你留意.會發現每當遇到這樣的涉及使用者體驗的問題時.我們也會通常會看看其他平臺是做法.不妨也是一種開拓思路. 從Android 和IOS 平臺角度來看. 幾種常見載入資料的方式.

[方式1]: 自動下拉載入

04132328_gDBd

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

[方式2]:手動下拉載入

26221224_A5wJ

方式1採用的使用者下拉到UI底部時自動載入.整個載入過程是使用者是可不控.即 無法實現使用者只在需要時才手動啟用載入更多或更舊的資料方式.二方式2當使用者滾動UI時可以選擇是否載入更多資料.使用者能夠對整個資料載入過程進行控制.

[方式3]:UI提示載入

201106091817486874

UI提示載入的方式和方式1 、2完全不同.當使用者下拉時載入更多資料時. 會提示彈出一個UI提示層. 對載入進度進行提示. 在資料載入過程中整個LiveView時無法進行任何UI操作的.使用者只能等待資料載入完成才能重新操作UI. 這點在很多Pc平臺專案見到很多.

[方式4]:下拉重新整理

2013-09-30_141801

當使用者第一次進來時.列表中獲取到最新資料時. 如果這個列表時隨著時間點會發生資料動態變化時. 使用者就希望在當前頁面就能獲取到最新的資料. 這個時候下拉重新整理價值就體現出來了. 而不需要重新進入這個頁面來獲取最新資料.下拉重新整理整個操作流程是. 使用者在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上效果基本一致 效果如下:

pulldown1-550x290

當使用者下拉時 資料列表頂部會顯示 一個向下箭頭和下拉重新整理的文字提示. 緊接著提示鬆開自動重新整理. 鬆開手勢操作 列表回到頂部.自動開始載入最新資料.並更新資料到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實現.

當第一次進來載入資料:

image

下拉是效果:

image (1)

剛鬆開效果:

image (2)

這樣下拉重新整理結合ListBox本身上下滑動重新整理基本實現我們如上三個需求.

原始碼下載[https://github.com/chenkai/LoadData]

Contact: @chenkaihome

相關文章