首先解释下标题的 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);
}
}

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

效果:

最后我们像 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 相关的工作。(长期有效)

【Win10】实现 ListViewBase 平滑滚动的更多相关文章

  1. js平滑滚动到顶部,底部,指定地方

    [原文链接] 采用锚点进行页面中的跳转的确很方便,但是要想增加网页的效果,可以使用jquery中的animate,实现滚动的一个动作,慢慢的滚动到你想跳转到的位置,从而看起来会非常高大上. [示例演示 ...

  2. 【转】使用jquery animate创建平滑滚动效果

    这篇文章主要介绍了使用jquery animate创建平滑滚动效果,效果可以滚动到顶部.到底部或页面中指定地方,生要的是非常平滑,很舒服,需要的朋友可以参考下 滚动到顶部: $('.scroll_to ...

  3. Windows 10 Edge浏览器、照片查看程序关闭“平滑滚动”

    升级到10后,这两个常用软件的“平滑滚动”功能,个人感觉体验有点不好,特别是图片这个自带程序,看了几十张图后就有点头晕了,所以把它关闭为好: 控制面板\系统和安全\系统\高级系统设置\高级\性能\设置 ...

  4. Android游戏开发之主角的移动与地图的平滑滚动

    人物移动地图的平滑滚动处理 玩过rpg游戏的朋友应该都知道RPG的游戏地图一般都比较大 今天我和大家分享一下在RPG游戏中如何来处理超出手机屏幕大小的游戏地图. 如图所示为程序效果动画图 地图滚动的原 ...

  5. JQuery简单实现锚点链接的平滑滚动

    在平时的项目中,我们经常需要一些特效链接,如果使效果进一步加强,我们可以使点击锚点链接平滑滚动到锚点,下面就来给大家讲解下如何使用jQuery来实现.   一般使用锚点来跳转到页面指定位置的时候,会生 ...

  6. javaScript滚动新闻之上下左右平滑滚动

    <!DOCTYPE HTML> <html> <head> <meta http-equiv="Content-Type" content ...

  7. 页面中的平滑滚动——smooth-scroll.js的使用

    正常的本页面锚链接跳转的时候跟PPT似的,特别生硬,用户体验非常差. 这时候我们就可以借助smooth-scroll.js这个插件,来实现本页面的平滑的跳转. 1首先,导入必须的JS文件 <sc ...

  8. 仿微信未读RecyclerView平滑滚动定位效果

    效果图有红点的地方表示有未读消息,依次双击首页图标定位,然后定位到某个未读在手动下滑一点距离在次点击定位效果 用过 RecyclerView 的人都知道,自带有几个滚动到item下标的方法,但是不靠谱 ...

  9. C#在Win10与非Win10 Windows系统鼠标滚动编程的一点区别。

    C#在win10和非Win10上处理鼠标滚动有一些区别,建一个Form1,放置一个FlowLayoutPanel ,类型的Panel1 Panel.MouseWheel += PanelOnMouse ...

随机推荐

  1. 各种id生成策略

    package com.taotao.utils; import java.util.Random; /** * 各种id生成策略 */ public class IDUtils { /** * 图片 ...

  2. JavaScript 实现双向队列并用此来测试一个单词是否为回文

    题目出自<数据结构与算法 JavaScript 描述>一书第五章,习题 5.2 代码如下: /*************** Deque 类的实现 *************/ funct ...

  3. 杨辉三角(生成器generator)

    生成器:(Python中,这种一边循环一边计算的机制,称为生成器:generator) 创建generator的方法: 1.把列表生成式的[]变为(),就创建了一个generator 例: 可以通过n ...

  4. Memtester——Linux内存测试工具

    一.Memtester简单介绍 Memtester主要是捕获内存错误和一直处于很高或者很低的坏位, 其测试的主要项目有随机值,异或比较,减法,乘法,除法,与或运算等等. 通过给定测试内存的大小和次数, ...

  5. 121. Best Time to Buy and Sell Stock买卖股票12

    一 [抄题]: If you were only permitted to complete at most one transaction (ie, buy one and sell one sha ...

  6. 6-Linux 上mysql的常用命令 以及 tomcat的相关指定

    mysql -u root -p 进入Mysql //注意一下有逗号!!! show databases; //显示所有的数据库 drop database mydb; // 删除mydb这个数据库 ...

  7. 16-图片上传那些事 org.springframework.web.multipart.MultipartException: The current request is not a multipart request

    https://blog.csdn.net/u010974598/article/details/46458039 我曾尝试了: @RequestMapping(value="/user/r ...

  8. 【SQL模板】四.插入/更新 列模板TSQL

    ---Name: 插入/更新 列模板.sql ---Purpose: 用于更新 数据库中 列 的脚本模板 ---Author: xx ---Time: 2015-12-18 10:26:06 ---R ...

  9. js记录

    --获取后缀名,结果 .jpg var extName = "/upload/head_img/20150902102539.jpg";var ta = extName.subst ...

  10. myschool 相思树

    题目描述 一群妖王排成一排站在苦情巨树下,寻找自己的转世恋人.虽然都是妖王,但按照涂山的规定必须进行标号,标号为1的妖王排在最后面,标号为n的妖王排在最前面.每个妖王只有一个妖力值a[i]表示它们现在 ...