zoukankan      html  css  js  c++  java
  • 【Win10】实现 ListViewBase 平滑滚动

    首先解释下标题的 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 的控件)。

    首先我们先复习一下怎么滚动到 ListViewBase 的某一个 item。

    在 ListViewBase 类中,有一个方法叫做 ScrollIntoView。这个方法有两个重载,我们看复杂一点,有两个参数的这个:

    //
    // 摘要:
    //     滚动列表,以将指定数据项移入具有指定对齐方式的视图中。
    //
    // 参数:
    //   item:
    //     要在视图中显示的数据项。
    //
    //   alignment:
    //     指定项是使用 Default 还是 Leading 对齐方式的枚举值。
    [Overload("ScrollIntoViewWithAlignment")]
     public void ScrollIntoView(System.Object item, ScrollIntoViewAlignment alignment);

    第一个参数就是我们需要滚动到当前可视区域的 item,而第二个参数,Default 是指让其滚动到当前可视区域即可,Leading 则是指让其滚动到当前可视区域的顶部。

    但是比较遗憾的是,这个方法一执行(?)立马滚动到目标 item 了,完全不带一丁点动画效果(后文你会了解到内部执行仍需很少一段时间,尽管我们肉眼察觉不到)。在这个时代,没有一个好的 UI,怎么能吸引用户呢?因此我们就来研究并实现怎样能让 ListViewBase 平滑滚动到某个 item。

    说起滚动的话,我们一定会想到 ScrollBar、ScrollViewer 这类的控件的。而幸运的是,ScrollViewer 有一个方法,叫 ChangeView 是带动画效果的(也可以选择不使用动画效果)。并且 ListView、GridView 内部都是有一个 ScrollViewer 的。那么我们自然而然就想到,是不是可以操作 ListViewBase 内部的这个 ScrollViewer 来实现平滑滚动。

    先开始编写代码吧:

    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 所在的位置。

    尽管我们不知道,但是,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);
        }
    }

    这次我们终于成功了!!!

    效果:

    ListViewBaseScrollIntoViewSmoothly

    最后我们像 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


    最后的最后,冰天雪地裸体 360 度跪求一份 UWP/WP8.1 相关的工作QQ图片20150925222632。(长期有效)

  • 相关阅读:
    SqLite 框架 GreenDAO
    HttpClient的使用
    android事件分发介绍
    Android常见开发思路
    AndroidStudio学习记录
    java.lang.RuntimeException: Unable to instantiate activity ComponentInfo异常总结
    开源Pull_To_Refresh控件使用
    自定义控件ViewPagae<
    白龙软件商店面试问题整理
    基于anyrtc的sdk实现直播连麦互动
  • 原文地址:https://www.cnblogs.com/h82258652/p/4862104.html
Copyright © 2011-2022 走看看