幾周之前在博客更新一篇Windows phone應用開發[18]-下拉刷新 博文,有很多人在微博和博客評論中提到了很多問題.其實在實際項目中我基於這篇博文提出解決問題思路優化了這個解決方案.為了能夠詳細系統解決和說明補充這個問題.覺得單獨開一篇博文來解答.在評論中提到的一些問題.
在原來的源碼中有人提到:
#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的滑動更流暢.
有人提到下拉時沒有自動刷新效果效果.為了詳細說明這個問題.首先來看看上篇博客中提到關於下拉刷新源碼的實現.找到源碼中繼承ListBox的類RefreshBox.在該類實現中重寫了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: 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()方法中可以看到做了如下幾件事:
A: 添加ScrollViewer 關於MouseMove 和ManipulationComplated 兩個事件訂閱 【ScrollViewer非空時取消】
B:獲取ListBox中ScrollViewer對象
C:獲取頂部刷新提示Element 的引用對象
D:初始化控制頂部刷新提示VisualState 狀態
其實到這里 需要額外說明一下實現下拉刷新的原理.從源碼中可以看出. 在下拉時會首先觸發MouseMove 事件. MouseMove事件主要作用是用來通過下拉的距離來控制下拉刷新狀態[下拉、松手刷新]兩種狀態切換提示. 下拉刷新並不是下拉后會立即刷新.而是用戶松手后列表回到頂部才開始刷新數據.等用戶手勢操作離開了屏幕就會自動觸發ManipulationComplated 事件.你可以看到在Complated事件中:
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 ?