在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 数据列表加载策略 总结如下:
明确了我们需求既加载策略. 来尝试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本身上下滑动刷新基本实现我们如上三个需求.