Windows phone應用開發[22]-再談下拉刷新

幾周以前在博客更新一篇Windows phone應用開發[18]-下拉刷新 博文,有不少人在微博和博客評論中提到了不少問題.其實在實際項目中我基於這篇博文提出解決問題思路優化了這個解決方案.爲了可以詳細系統解決和說明補充這個問題.以爲單獨開一篇博文來解答.在評論中提到的一些問題.css

在原來的源碼中有人提到:html

#11樓 灬番茄2013-10-06 14:53

@chenkai
p.Y值一直是你設置的默認值,因此if (p.Y < -VerticalPullToRefreshDistance)這個判斷一直是進不去的。
我閱讀了另一篇下拉刷新的文章
http://www.cnblogs.com/wuzhsh/archive/2012/09/04/2670307.html,裏面提到ScrollViewer的ManipulationMode屬性設爲Conrtrol(必需),默認是System。而後我也在你的源碼裏添加了這句ElementScrollViewer.ManipulationMode = ManipulationMode.Control; 才實現了下拉刷新。至於原理卻沒搞清楚,MSDN文檔裏也只是說System比Control的滑動更流暢.git

有人提到下拉時沒有自動刷新效果效果.爲了詳細說明這個問題.首先來看看上篇博客中提到關於下拉刷新源碼的實現.找到源碼中繼承ListBox的類RefreshBox.在該類實現中重寫了OnApplyTemplate方法.在該方法中能夠看到:github

   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:      ElementScrollViewer = GetTemplateChild("ScrollViewer") as ScrollViewer;
  10:   
  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:   }

首先在OnApplyTemplate()方法中能夠看到作了以下幾件事:windows

A: 添加ScrollViewer 關於MouseMove 和ManipulationComplated 兩個事件訂閱 【ScrollViewer非空時取消】app

B:獲取ListBox中ScrollViewer對象ide

C:獲取頂部刷新提示Element 的引用對象測試

D:初始化控制頂部刷新提示VisualState 狀態優化

其實到這裏 須要額外說明一下實現下拉刷新的原理.從源碼中能夠看出. 在下拉時會首先觸發MouseMove 事件. MouseMove事件主要做用是用來經過下拉的距離來控制下拉刷新狀態[下拉、鬆手刷新]兩種狀態切換提示. 下拉刷新並非下拉後會當即刷新.而是用戶鬆手後列表回到頂部纔開始刷新數據.等用戶手勢操做離開了屏幕就會自動觸發ManipulationComplated 事件.你能夠看到在Complated事件中:this

   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:                  if (PullRefresh != null)
   7:                      PullRefresh(this, EventArgs.Empty);
   8:                  isPulling = false;
   9:                  ChangeVisualState(true);
  10:              }
  11:          }

經過判斷ElementRealse也就是下拉刷新頂部提示部分下拉的距離來觸發事件PullRefresh來刷新新的數據. 其中VerticalPullToRefreshDistance屬性是用來判斷當下啦到多少距離時才觸發刷新事件.能夠定義控件時預設.在回到上文.來回答爲什麼在下拉時沒有觸發刷新事件?

p.y對象的值爲什麼一直爲90? 那是由於在剛開始定義ElementRealse對象時對頂部Manger Top值就是90, 那爲什麼在下拉結束時 這個對應的X值沒有跟隨滑動操做變化? 其實這個問題和SCrollView的ManipulationMode屬性有關係. 首先咱們能夠在OnApplyTemplate方法能夠看到沒有設置MainpulationMode屬性的值. 而MainpilationMode屬性在默認狀況下是設置爲System的.也就是指定系統來處理ListBox的平滑滾動的.ScrollViewer並無拖到頂部或底部的事件,並且當ScrollViewer的ManipulationMode爲System的時候,是不能獲取到ScrollViewer滾動條的當前位置.也就是沒法動態在ManipulationComplated 事件來獲取ElementRealse距離頂部的距離.這也就是爲什麼p.y的值一直是初始化90 而不隨着滑動操做發生改變的緣由.

那在具體點? 爲什麼設置MainpulationMode屬性爲System 後就沒法獲取ScrollViewer滾動條的位置? System和Control不一樣在於.二者的變換(Transform)方式不同,當ManipulationMode爲System的時候,ScrollViewer的變換方式是MatrixTransform[系統矩陣變換處理滑動],因此沒法獲取ScaleY或者TranslateY等屬性。經過這個MatrixTransform也沒有辦法直接拿到當前ScrollViewer的上下滾動、壓縮狀態。而置爲Control時,變換方式就成了CompositeTransform,經過CompositeTransform就能夠獲得ScrollViewer的TranslateY值(當到達頂部的時候,TranslateY變爲正值,其他時候爲負值,超過底部時,絕對值大於ScrollViewer內容長度),而後在ScrollViewer的操做事件ManipulationStarted、ManipulationDetla或ManipulationCompleted中,獲取ScrollViewer的變換方式,獲得TranslateY值,最後判斷是否到達頂部或底部,決定是否要進行處理.

能夠看到二者之間的本質原理上不一樣.這也就可以解釋爲什麼. 當ScrollViewer 的ManipulationMode屬性 默認爲System時沒法即時獲取下拉ElementRealse 的X的值了.也就是說用目前下拉刷新必須設置ManipulationMode屬性爲Control. 但你測試後發現. 下拉刷新邏輯可以正常觸發刷新事件.可是整個滑動過程會明顯感受卡了不少[須要聲明的是ListBox不存在虛擬化的問題].沒有設置爲System系統處理方式平滑流暢. 那如何來解決設置設置ManipulationMode屬性爲Control 滑動會卡頓的問題? 或是有沒有一個可以得到System處理滑動同樣平滑體驗同時又可以判斷ScrollViewer當前的位置狀態的解決方案.

通過一番周折在MSDN Blog上找到了一個可以實現如上兩點解決方案:

Windows Phone Mango change, Listbox: How to detect compression(end of scroll) states ?

首先來講說這個解決方案的實現.固然咱們實現ListBox上平滑處理髮現系統ManipulationMode屬性爲system 矩陣處理方式滑動體驗很流暢.那如何來判斷在設置爲System時獲取ScrollViewer的狀態呢? 答案是採用VisualState.

要實現採用Visual State來獲取SCrollViewer當前位置.只須要如今Xaml文件添加以下代碼[只截取其中Visual State 所有代碼見源碼]:    

   1:                               <VisualStateManager.VisualStateGroups>
   2:                                  <VisualStateGroup x:Name="ScrollStates">
   3:                                      <VisualStateGroup.Transitions>
   4:                                          <VisualTransition GeneratedDuration="00:00:00.5"/>
   5:                                      </VisualStateGroup.Transitions>
   6:                                      <VisualState x:Name="Scrolling">
   7:                                          <Storyboard>
   8:                                              <DoubleAnimation Storyboard.TargetName="VerticalScrollBar"
   9:                                                                            Storyboard.TargetProperty="Opacity" To="1" Duration="0"/>
  10:                                              <DoubleAnimation Storyboard.TargetName="HorizontalScrollBar" 
  11:                                                                            Storyboard.TargetProperty="Opacity" To="1" Duration="0"/>
  12:                                          </Storyboard>
  13:                                      </VisualState>
  14:                                      <VisualState x:Name="NotScrolling">
  15:                                      </VisualState>
  16:                                  </VisualStateGroup>
  17:                                  <VisualStateGroup x:Name="VerticalCompression">
  18:                                      <VisualState x:Name="NoVerticalCompression"/>
  19:                                      <VisualState x:Name="CompressionTop"/>
  20:                                      <VisualState x:Name="CompressionBottom"/>
  21:                                  </VisualStateGroup>
  22:                                  <VisualStateGroup x:Name="HorizontalCompression">
  23:                                      <VisualState x:Name="NoHorizontalCompression"/>
  24:                                      <VisualState x:Name="CompressionLeft"/>
  25:                                      <VisualState x:Name="CompressionRight"/>
  26:                                  </VisualStateGroup>
  27:                              </VisualStateManager.VisualStateGroups>

在後臺代碼中添加對ScrollViewer狀態的變化事件訂閱.

   1:              sv = (ScrollViewer)FindElementRecursive(MainListBox, typeof(ScrollViewer));
   2:              if (sv != null)
   3:              {
   5:                  FrameworkElement element = VisualTreeHelper.GetChild(sv, 0) as FrameworkElement;
   6:                  if (element != null)
   7:                  {
   8:                      VisualStateGroup group = FindVisualState(element, "ScrollStates");
   9:                      if (group != null)                    
  10:                          group.CurrentStateChanging += new EventHandler<VisualStateChangedEventArgs>(group_CurrentStateChanging);
  11:                      
  12:                      VisualStateGroup vgroup = FindVisualState(element, "VerticalCompression");
  13:                      VisualStateGroup hgroup = FindVisualState(element, "HorizontalCompression");
  14:                      if (vgroup != null)                    
  15:                          vgroup.CurrentStateChanging += new EventHandler<VisualStateChangedEventArgs>(vgroup_CurrentStateChanging);
  16:                      
  17:                      if (hgroup != null)                    
  18:                          hgroup.CurrentStateChanging += new EventHandler<VisualStateChangedEventArgs>(hgroup_CurrentStateChanging);                    
  19:                  }
  20:              }         

 

從代碼邏輯可見.Xaml文件重寫了整個ScrollViewer的樣式並添加兩組Vistaul State Group狀態的標識. 後代代碼經過訂閱ScrollViewer垂直和水平滑動的狀態開始事件CurrentStateChanging.在事件對應的經過以下方式進行判斷當前ScrollViewer的狀態:

   1:       private void vgroup_CurrentStateChanging(object sender, VisualStateChangedEventArgs e)
   2:          {
   3:              if (e.NewState.Name == "CompressionTop")
   4:              {
   5:                  #region Goto Top
   6:                  #endregion
   7:              }
   8:              else if (e.NewState.Name == "CompressionBottom")
   9:              {
  10:                  #region Goto Bottom
  11:                  #endregion
  12:              }
  13:              else if (e.NewState.Name == "NoVerticalCompression")
  14:              {
  15:                  #region No Vertical Compression
  16:                  #endregion
  17:              }
  18:          }

其實以拿到VerticalCompression和HorizontalCompression兩種VisualStateGroup,能夠用來檢測ListBox的上下左右方向的壓縮狀態。這種解決方案的作法是運用VisualState檢測ScrollViewer滾動狀態,來判斷SCrollViewer是到了頂部仍是底部 以及是否滾動中狀態.只有在滾動中止時,即NotScrolling狀態,檢測滾動偏移(Offset),若是偏移加上滾動前位置超過了控件內容總長度(非可視長度),就進行刷新或者其餘相應的處理。

其實基於這個方案.結合第一個方法稍微改造一下ScrollViewer 的Vistual State 便可達到平滑處理滑動下拉刷新提示操做.這裏就不作過多贅述了.

源碼下載[https://github.com/chenkai/ListBoxVisualStatesDemo/tree/master/ListBoxVisualStatesDemo]

Contact ME [@chenkaihome]

參考資料:

Windows Phone Mango change, Listbox: How to detect compression(end of scroll) states ? 

Listbox – ScrollViewer performance improvement for Mango and how it impacts your existing application?

ScrollViewer.ManipulationMode 屬性

MatrixTransform 類

相關文章
相關標籤/搜索