首先解释下标题的 ListViewBase 是什么鬼。ListViewBase 咱们能够查阅 MSDN 文档:https://msdn.microsoft.com/zh-cn/library/windows.ui.xaml.controls.listviewbase.aspx 得知,ListViewBase 是 ListView 和 GridView 的基类(ListView 和 GridView 则为经常使用的数据展现控件之一)。而本文的主要目的就是实现 ListView 和 GridView 的平滑滚动,所以我将标题写成“实现 ListViewBase 平滑滚动”而不是“实现 ListView 和 GridView 平滑滚动”(实际上本文适用于任何继承自 ListViewBase 的控件)。windows
首先咱们先复习一下怎么滚动到 ListViewBase 的某一个 item。异步
在 ListViewBase 类中,有一个方法叫作 ScrollIntoView。这个方法有两个重载,咱们看复杂一点,有两个参数的这个:动画
// // 摘要: // 滚动列表,以将指定数据项移入具备指定对齐方式的视图中。 // // 参数: // item: // 要在视图中显示的数据项。 // // alignment: // 指定项是使用 Default 仍是 Leading 对齐方式的枚举值。 [Overload("ScrollIntoViewWithAlignment")] public void ScrollIntoView(System.Object item, ScrollIntoViewAlignment alignment);
第一个参数就是咱们须要滚动到当前可视区域的 item,而第二个参数,Default 是指让其滚动到当前可视区域便可,Leading 则是指让其滚动到当前可视区域的顶部。ui
可是比较遗憾的是,这个方法一执行就(?)立马滚动到目标 item 了,彻底不带一丁点动画效果(后文你会了解到内部执行仍需不多一段时间,尽管咱们肉眼察觉不到)。在这个时代,没有一个好的 UI,怎么能吸引用户呢?所以咱们就来研究并实现怎样能让 ListViewBase 平滑滚动到某个 item。this
提及滚动的话,咱们必定会想到 ScrollBar、ScrollViewer 这类的控件的。而幸运的是,ScrollViewer 有一个方法,叫 ChangeView 是带动画效果的(也能够选择不使用动画效果)。而且 ListView、GridView 内部都是有一个 ScrollViewer 的。那么咱们天然而然就想到,是否是能够操做 ListViewBase 内部的这个 ScrollViewer 来实现平滑滚动。spa
先开始编写代码吧:code
public static class ListViewBaseExtensions { public static void ScrollIntoViewSmoothly(this ListViewBase listViewBase, object item, ScrollIntoViewAlignment alignment) { if (listViewBase == null) { throw new ArgumentNullException(nameof(listViewBase)); } // GetFirstDescendantOfType 是 WinRTXamlToolkit 中的扩展方法, // 寻找该控件在可视树上第一个符合类型的子元素。 ScrollViewer scrollViewer = listViewBase.GetFirstDescendantOfType<ScrollViewer>(); // 因为 ScrollViewer 确定有,所以不作 null 检查判断了。 scrollViewer.ChangeView(targetHorizontalOffset, targetVerticalOffset, null); } }
然而问题来了,targetHorizontalOffset 和 targetVerticalOffset 咱们是不知道的,也就是说,咱们不知道目标 item 所在的位置。blog
尽管咱们不知道,可是,ListViewBase 自身的 ScrollIntoView 方法它是知道的,那咱们干脆就让它当个跑腿,先执行一次,而后就能够获取目标位置了。继承
public static class ListViewBaseExtensions { public static void ScrollIntoViewSmoothly(this ListViewBase listViewBase, object item, ScrollIntoViewAlignment alignment) { if (listViewBase == null) { throw new ArgumentNullException(nameof(listViewBase)); } // GetFirstDescendantOfType 是 WinRTXamlToolkit 中的扩展方法, // 寻找该控件在可视树上第一个符合类型的子元素。 ScrollViewer scrollViewer = listViewBase.GetFirstDescendantOfType<ScrollViewer>(); // 因为 ScrollViewer 确定有,所以不作 null 检查判断了。 // 记录初始位置,用于 ScrollIntoView 检测目标位置后复原。 double originHorizontalOffset = scrollViewer.HorizontalOffset; double originVerticalOffset = scrollViewer.VerticalOffset; // 跑腿。 listViewBase.ScrollIntoView(item, alignment); // 获取目标位置。 double targetHorizontalOffset = scrollViewer.HorizontalOffset; double targetVerticalOffset = scrollViewer.VerticalOffset; // scrollViewer.ChangeView(targetHorizontalOffset, targetVerticalOffset, null); } }
然而经过断点检查后,发现 targetHorizontalOffset 和 targetVerticalOffset 并无发生变化。可是执行事后,ListViewBase 确实发生了滚动,所以咱们质疑,是否是 ScrollIntoView 方法在控件内部是以一个异步的形式执行。事件
这个时候,咱们仍是想起近乎万能的 LayoutUpdated 事件吧。改写下代码。
public static class ListViewBaseExtensions { public static void ScrollIntoViewSmoothly(this ListViewBase listViewBase, object item, ScrollIntoViewAlignment alignment) { if (listViewBase == null) { throw new ArgumentNullException(nameof(listViewBase)); } // GetFirstDescendantOfType 是 WinRTXamlToolkit 中的扩展方法, // 寻找该控件在可视树上第一个符合类型的子元素。 ScrollViewer scrollViewer = listViewBase.GetFirstDescendantOfType<ScrollViewer>(); // 因为 ScrollViewer 确定有,所以不作 null 检查判断了。 // 记录初始位置,用于 ScrollIntoView 检测目标位置后复原。 double originHorizontalOffset = scrollViewer.HorizontalOffset; double originVerticalOffset = scrollViewer.VerticalOffset; EventHandler<object> layoutUpdatedHandler = null; layoutUpdatedHandler = delegate { listViewBase.LayoutUpdated -= layoutUpdatedHandler; // 获取目标位置。 double targetHorizontalOffset = scrollViewer.HorizontalOffset; double targetVerticalOffset = scrollViewer.VerticalOffset; // scrollViewer.ChangeView(targetHorizontalOffset, targetVerticalOffset, null); }; listViewBase.LayoutUpdated += layoutUpdatedHandler; // 跑腿。 listViewBase.ScrollIntoView(item, alignment); } }
此次咱们再断点后,发现可以获取目标位置了!!(因此我上面说“内部执行仍需不多一段时间,尽管咱们肉眼察觉不到”)
接下来,因为跑腿是已经滚动目标位置了,所以咱们须要复原到原来的位置,再滚动到目标位置以实现平滑滚动的动画效果。
public static void ScrollIntoViewSmoothly(this ListViewBase listViewBase, object item, ScrollIntoViewAlignment alignment) { if (listViewBase == null) { throw new ArgumentNullException(nameof(listViewBase)); } // GetFirstDescendantOfType 是 WinRTXamlToolkit 中的扩展方法, // 寻找该控件在可视树上第一个符合类型的子元素。 ScrollViewer scrollViewer = listViewBase.GetFirstDescendantOfType<ScrollViewer>(); // 因为 ScrollViewer 确定有,所以不作 null 检查判断了。 // 记录初始位置,用于 ScrollIntoView 检测目标位置后复原。 double originHorizontalOffset = scrollViewer.HorizontalOffset; double originVerticalOffset = scrollViewer.VerticalOffset; EventHandler<object> layoutUpdatedHandler = null; layoutUpdatedHandler = delegate { listViewBase.LayoutUpdated -= layoutUpdatedHandler; // 获取目标位置。 double targetHorizontalOffset = scrollViewer.HorizontalOffset; double targetVerticalOffset = scrollViewer.VerticalOffset; // 复原位置,且不须要使用动画效果。 scrollViewer.ChangeView(originHorizontalOffset, originVerticalOffset, null, true); // 最终目的,带平滑滚动效果滚动到 item。 scrollViewer.ChangeView(targetHorizontalOffset, targetVerticalOffset, null); }; listViewBase.LayoutUpdated += layoutUpdatedHandler; // 跑腿。 listViewBase.ScrollIntoView(item, alignment); } }
执行以后,然而咱们发现仍是直接滚动到目标,不带一丁点动画效果。可是,有了上面 ScrollIntoView 的经验后,咱们天然而然也能够质疑 ChangeView 方法是否是像 ScrollIntoView 同样,内部也是异步执行的。再改写下:
public static class ListViewBaseExtensions { public static void ScrollIntoViewSmoothly(this ListViewBase listViewBase, object item, ScrollIntoViewAlignment alignment) { if (listViewBase == null) { throw new ArgumentNullException(nameof(listViewBase)); } // GetFirstDescendantOfType 是 WinRTXamlToolkit 中的扩展方法, // 寻找该控件在可视树上第一个符合类型的子元素。 ScrollViewer scrollViewer = listViewBase.GetFirstDescendantOfType<ScrollViewer>(); // 因为 ScrollViewer 确定有,所以不作 null 检查判断了。 // 记录初始位置,用于 ScrollIntoView 检测目标位置后复原。 double originHorizontalOffset = scrollViewer.HorizontalOffset; double originVerticalOffset = scrollViewer.VerticalOffset; EventHandler<object> layoutUpdatedHandler = null; layoutUpdatedHandler = delegate { listViewBase.LayoutUpdated -= layoutUpdatedHandler; // 获取目标位置。 double targetHorizontalOffset = scrollViewer.HorizontalOffset; double targetVerticalOffset = scrollViewer.VerticalOffset; EventHandler<ScrollViewerViewChangedEventArgs> scrollHandler = null; scrollHandler = delegate { scrollViewer.ViewChanged -= scrollHandler; // 最终目的,带平滑滚动效果滚动到 item。 scrollViewer.ChangeView(targetHorizontalOffset, targetVerticalOffset, null); }; scrollViewer.ViewChanged += scrollHandler; // 复原位置,且不须要使用动画效果。 scrollViewer.ChangeView(originHorizontalOffset, originVerticalOffset, null, true); }; listViewBase.LayoutUpdated += layoutUpdatedHandler; // 跑腿。 listViewBase.ScrollIntoView(item, alignment); } }
此次咱们终于成功了!!!
效果:
最后咱们像 ListViewBase 的 ScrollIntoView 方法,加多个只有一个参数的重载吧。
最终代码:
public static class ListViewBaseExtensions { public static void ScrollIntoViewSmoothly(this ListViewBase listViewBase, object item) { ScrollIntoViewSmoothly(listViewBase, item, ScrollIntoViewAlignment.Default); } public static void ScrollIntoViewSmoothly(this ListViewBase listViewBase, object item, ScrollIntoViewAlignment alignment) { if (listViewBase == null) { throw new ArgumentNullException(nameof(listViewBase)); } // GetFirstDescendantOfType 是 WinRTXamlToolkit 中的扩展方法, // 寻找该控件在可视树上第一个符合类型的子元素。 ScrollViewer scrollViewer = listViewBase.GetFirstDescendantOfType<ScrollViewer>(); // 因为 ScrollViewer 确定有,所以不作 null 检查判断了。 // 记录初始位置,用于 ScrollIntoView 检测目标位置后复原。 double originHorizontalOffset = scrollViewer.HorizontalOffset; double originVerticalOffset = scrollViewer.VerticalOffset; EventHandler<object> layoutUpdatedHandler = null; layoutUpdatedHandler = delegate { listViewBase.LayoutUpdated -= layoutUpdatedHandler; // 获取目标位置。 double targetHorizontalOffset = scrollViewer.HorizontalOffset; double targetVerticalOffset = scrollViewer.VerticalOffset; EventHandler<ScrollViewerViewChangedEventArgs> scrollHandler = null; scrollHandler = delegate { scrollViewer.ViewChanged -= scrollHandler; // 最终目的,带平滑滚动效果滚动到 item。 scrollViewer.ChangeView(targetHorizontalOffset, targetVerticalOffset, null); }; scrollViewer.ViewChanged += scrollHandler; // 复原位置,且不须要使用动画效果。 scrollViewer.ChangeView(originHorizontalOffset, originVerticalOffset, null, true); }; listViewBase.LayoutUpdated += layoutUpdatedHandler; // 跑腿。 listViewBase.ScrollIntoView(item, alignment); } }
最后再附送上 Demo:ListViewBaseScrollSmoothlyDemo.zip