几周之前在博客更新一篇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方法.在该方法中可以看到:
class="alt"> 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 ?
Listbox – ScrollViewer performance improvement for Mango and how it impacts your existing application?
ScrollViewer.ManipulationMode 属性
MatrixTransform 类