WPF 控件库——带有惯性的ScrollViewer

WPF 控件库系列博文地址:html

WPF 控件库——仿制Chrome的ColorPickergit

WPF 控件库——仿制Windows10的进度条github

WPF 控件库——轮播控件ide

WPF 控件库——带有惯性的ScrollViewer函数

WPF 控件库——可拖动选项卡的TabControl动画

 

1、先看看效果spa

 

 

2、原理code

  虽然效果很简单,可是网上的一些资料涉及的代码量很是可观,并且效果也不是很理想,滚动的时候没有一个顺滑感。我这里提供的源码一共120多行,就能实现上图的效果。
htm

  本质上咱们只要接管ScrollViewer的滚动逻辑,而且把这个逻辑替换成带有惯性的便可,那么如何去接管呢?这里的关键是先屏蔽ScrollViewer的鼠标滚轮事件:blog

1 protected override void OnMouseWheel(MouseWheelEventArgs e)
2 {
3      e.Handled = true;
4 }

  这样一来,ScrollViewer就不会响应滚轮事件了,咱们就在这里作文章。首先咱们给这个ScrollViewer添加一个属性 IsEnableInertia ,用来控制是否使用惯性,由于萝卜青菜各有所爱,不要想着强制全部人使用惯性,因此滚轮响应方法变为:

1 protected override void OnMouseWheel(MouseWheelEventArgs e)
2 {
3     if (!IsEnableInertia)
4     {
5         base.OnMouseWheel(e);
6         return;
7     }
8     e.Handled = true;
9 }    

  控制ScrollViewer的垂直滚动可使用 ScrollViewer.ScrollToVerticalOffset ,横向也同样。为何不能用 VerticalOffset ?由于 VerticalOffset 在注册的时候就说明了是只读的:

1 private static readonly DependencyPropertyKey VerticalOffsetPropertyKey = DependencyProperty.RegisterReadOnly(nameof (VerticalOffset), typeof (double), typeof (ScrollViewer), (PropertyMetadata) new FrameworkPropertyMetadata((object) 0.0));
2 
3 public static readonly DependencyProperty VerticalOffsetProperty = ScrollViewer.VerticalOffsetPropertyKey.DependencyProperty;

  好了,接下来就是怎么在滚轮响应方法中实现惯性运动了,也就是一种减速运动。想到这儿,熟悉动画的博友很快就知道要用WPF的动画来实现了,默认的动画都是一次线性的,要有惯性效果就得用缓动函数,WPF的缓动函数有不少,而 CubicEase 很是适合用来作惯性,它的描述图以下:

  图中,横轴表示时间,纵轴表示运动距离。很明显,中间的 EaseOut 模式就是咱们想要的。到了这里思路就清晰了,咱们能够定义一个属性 CurrentVerticalOffset ,咱们会在它上面实现动画,在它的值回调函数中调用 ScrollViewer.ScrollToVerticalOffset 来更新ScrollViewer的滚动位置。固然咱们还须要一个私有字段 _totalVerticalOffset ,这个是用来存放ScrollViewer滚动目标位置的,滚轮向下滚动一个单位咱们就给它减去一次 e.Delta ,这里的e是滚轮响应方法传进来的参数,每次给它赋值以后,就能够在 CurrentVerticalOffset 上执行动画了: BeginAnimation(CurrentVerticalOffsetProperty, animation) ,须要特别注意的是,当一个依赖属性用了动画改变后,再对其赋值则不会生效,缘由是在一个动画到达活动期的终点后,时间线默认会保持其进度,直到其父级的活动期和保持期结束为止。若是想在动画结束后还能够手动更改依赖属性的值,则须要把 FillBehavior 设置为Stop。不过这样又会出现一个问题,一旦动画结束,这个依赖属性又会恢复初始值,因此还要给这个动画订阅一个 Completed 事件,在事件响应方法中为 CurrentVerticalOffset 给定目标值,也就是 _totalVerticalOffset 。

  最后还有一个冲突问题,当手动拖动滑块或者当用上下文菜单改变滚动条位置时是不能用动画的,由于这时候没有触发 OnMouseWheel ,不要紧,这正是咱们想要的,可是若是再次触发 OnMouseWheel 就有问题了,由于手动触发滚动的时候咱们没有给 CurrentVerticalOffset 和 _totalVerticalOffset 赋值( CurrentVerticalOffset 和 _totalVerticalOffset 只在 OnMouseWheel 中赋值),因此在用动画执行滚动操做前要先判断一下是否须要先更新一下它们俩,如何判断?咱们能够用一个私有字段 _isRunning 来维护状态,每当动画开始就给它赋值true,结束则赋值false。这样一来,当 _isRunning = false 时,说明在调用 OnMouseWheel 前,动画已经结束,用户可能已经手动改变了滚动条位置(也可能没有,但这并不影响),因此就要给以前俩兄弟更新一下值了。

  由于常见的惯性滚动以垂直方向居多,因此我没有写水平方向的逻辑,但也很容易扩展,有兴趣的博友能够下载源代码本身研究。

 

3、源码

  本文所讨论的控件源码已经在github开源:https://github.com/NaBian/HandyControl