1. 程式人生 > >在WPF中實現平滑滾動

在WPF中實現平滑滾動

原文: 在WPF中實現平滑滾動

WPF實現滾動條還是比較方便的,只要在控制元件外圍加上ScrollViewer即可,但美中不足的是:滾動的時候沒有動畫效果。在滾動的時候新增過渡動畫能給我們的軟體增色不少,例如Office 2013的滾動的時候支援動畫看起來就舒服多了。 之前倒是研究過如何實現這個平滑滾動,不過網上的方案大部分大多數如下:

  1. 通過VisualTree找到ScrollViewer
  2. 在ScrollChanged事件中新增動畫

這種方案效果並不好,以為我們的滾動很多時候都是一口氣滾動好幾格滾輪的,這個時候上一個動畫還沒有結束,下一個動畫就來了,反而還出現了卡頓的感覺,並且網上的一些演算法大部分還都會導致偏移錯位。

趁著這兩天有點時間,就研究了一下ScorllViewer,從MSDN文件中看到,它是支援兩種滾動方式的:

物理滾動:

系統預設的滾動方案,控制元件本身啥都不用幹,完全由ScrollViewer來實現滾動。這種方式的好處是簡單,但也正由於簡單,控制元件本身完全感知不到ScorllViewer的存在,也就無法加以控制了。

邏輯滾動:

將這種方式需要設定ScrollViewer的CanContentScroll"True"才能生效,同時需要控制元件實現IScrollInfo介面。此時ScrollViewer只是將滾動事件通過IScrollInfo介面傳遞給控制元件,由控制元件本身自己去實現滾動。同時從IScrollInfo介面中讀取相關的屬性更新滾動條介面。

也就是說,邏輯滾動才是我們所需要的方案。由於它要求控制元件實現IScrollInfo介面,自行控制滾動。也就是說我們要實現自己的Panel,並且實現IScrollInfo介面。關於這個介面,MSDN上有一系列文章介紹過如何實現它:

這個介面實現也不算麻煩,我倒沒有細看這幾篇文章,自己照著最後的一個例子嘗試著弄了一陣子也弄出來了。實際上麻煩的地方不在於實現這個介面,而是實現Panel,我這裡為了簡單,直接繼承了WrapPanel類,程式碼如下: 

  1     class MyWrapPanel : WrapPanel, IScrollInfo
  2     {
  3         TranslateTransform _transForm;
  4         public MyWrapPanel()
  5         {
  6             _transForm = new TranslateTransform();
  7             this.RenderTransform = _transForm;
  8         }
  9 
 10         #region Layout
 11 
 12         Size _screenSize;
 13         Size _totalSize;
 14 
 15         protected override Size MeasureOverride(Size availableSize)
 16         {
 17             _screenSize = availableSize;
 18 
 19             if (Orientation == Orientation.Horizontal)
 20                 availableSize = new Size(availableSize.Width, double.PositiveInfinity);
 21             else
 22                 availableSize = new Size(double.PositiveInfinity, availableSize.Height);
 23 
 24             _totalSize = base.MeasureOverride(availableSize);
 25             return _totalSize;
 26         }
 27 
 28         protected override Size ArrangeOverride(Size finalSize)
 29         {
 30             var size = base.ArrangeOverride(finalSize);
 31             if (ScrollOwner != null)
 32             {
 33                 _transForm.Y = -VerticalOffset;
 34                 _transForm.X = -HorizontalOffset;
 35                 
 36                 ScrollOwner.InvalidateScrollInfo();
 37             }
 38             return _screenSize;
 39         }
 40         #endregion
 41 
 42         #region IScrollInfo
 43 
 44         public ScrollViewer ScrollOwner { get; set; }
 45         public bool CanHorizontallyScroll { get; set; }
 46         public bool CanVerticallyScroll { get; set; }
 47 
 48         public double ExtentHeight { get { return _totalSize.Height; } }
 49         public double ExtentWidth { get { return _totalSize.Width; } }
 50 
 51         public double HorizontalOffset { get; private set; }
 52         public double VerticalOffset { get; private set; }
 53 
 54         public double ViewportHeight { get { return _screenSize.Height; } }
 55         public double ViewportWidth { get { return _screenSize.Width; } }
 56 
 57         void appendOffset(double x, double y)
 58         {
 59             var offset = new Vector(HorizontalOffset + x, VerticalOffset + y);
 60 
 61             offset.Y = range(offset.Y, 0, _totalSize.Height - _screenSize.Height);
 62             offset.X = range(offset.X, 0, _totalSize.Width - _screenSize.Width);
 63 
 64             HorizontalOffset = offset.X;
 65             VerticalOffset = offset.Y;
 66 
 67             InvalidateArrange();
 68         }
 69 
 70         double range(double value, double value1, double value2)
 71         {
 72             var min = Math.Min(value1, value2);
 73             var max = Math.Max(value1, value2);
 74 
 75             value = Math.Max(value, min);
 76             value = Math.Min(value, max);
 77 
 78             return value;
 79         }
 80 
 81 
 82         const double _lineOffset = 30;
 83         const double _wheelOffset = 90;
 84 
 85         public void LineDown()
 86         {
 87             appendOffset(0, _lineOffset);
 88         }
 89 
 90         public void LineUp()
 91         {
 92             appendOffset(0, -_lineOffset);
 93         }
 94 
 95         public void LineLeft()
 96         {
 97             appendOffset(-_lineOffset, 0);
 98         }
 99 
100         public void LineRight()
101         {
102             appendOffset(_lineOffset, 0);
103         }
104 
105         public Rect MakeVisible(Visual visual, Rect rectangle)
106         {
107             throw new NotSupportedException();
108         }
109 
110         public void MouseWheelDown()
111         {
112             appendOffset(0, _wheelOffset);
113         }
114 
115         public void MouseWheelUp()
116         {
117             appendOffset(0, -_wheelOffset);
118         }
119 
120         public void MouseWheelLeft()
121         {
122             appendOffset(0, _wheelOffset);
123         }
124 
125         public void MouseWheelRight()
126         {
127             appendOffset(_wheelOffset, 0);
128         }
129 
130         public void PageDown()
131         {
132             appendOffset(0, _screenSize.Height);
133         }
134 
135         public void PageUp()
136         {
137             appendOffset(0, -_screenSize.Height);
138         }
139 
140         public void PageLeft()
141         {
142             appendOffset(-_screenSize.Width, 0);
143         }
144 
145         public void PageRight()
146         {
147             appendOffset(_screenSize.Width, 0);
148         }
149 
150         public void SetVerticalOffset(double offset)
151         {
152             this.appendOffset(HorizontalOffset, offset - VerticalOffset);
153         }
154 
155         public void SetHorizontalOffset(double offset)
156         {
157             this.appendOffset(offset - HorizontalOffset, VerticalOffset);
158         }
159         #endregion
160     }
View Code

基本上從程式碼中也能看出IScrollInfo介面的互動流程,這裡就不多介紹了。

主介面程式碼如下: 

    <ItemsControl ItemsSource="{Binding}" >
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <Border BorderThickness="1" BorderBrush="Black" Margin="8" Width="150" Height="50">
                    <Rectangle Fill="{Binding}"  />
                </Border>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <local:MyWrapPanel />
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
        <ItemsControl.Template>
            <ControlTemplate>
                <ScrollViewer CanContentScroll="True">
                    <ItemsPresenter />
                </ScrollViewer>
            </ControlTemplate>
        </ItemsControl.Template>
    </ItemsControl>

需要注意的是,這兒需要設定<ScrollViewer CanContentScroll="True">否則使用的不是邏輯滾動。

資料來源程式碼如下:

    var brushes = from property in typeof(Brushes).GetProperties()
                    let value = property.GetValue(null)
                    select value;
this.DataContext = brushes.Take(100).ToArray();

由於使用了IscrollInfo介面,所有的滾動操作是自己實現的,這裡我是通過設定Panel的RenderTransFrom的X,Y偏移來實現滾動操作的。執行後看上去上和WrapPanel沒有什麼區別,但是由於是自己控制的滾動,加上動畫效果也只是分分鐘的事情了,把上面程式碼的RenderTransFrom的X,Y硬切換改成動畫切換即可:

    protected override Size ArrangeOverride(Size finalSize)
    {
        var size = base.ArrangeOverride(finalSize);
        if (ScrollOwner != null)
        {
            var yOffsetAnimation = new DoubleAnimation() { To = -VerticalOffset, Duration = TimeSpan.FromSeconds(0.3) };
            _transForm.BeginAnimation(TranslateTransform.YProperty, yOffsetAnimation);

            var xOffsetAnimation = new DoubleAnimation() { To = -HorizontalOffset, Duration = TimeSpan.FromSeconds(0.3) };
            _transForm.BeginAnimation(TranslateTransform.XProperty, xOffsetAnimation);
ScrollOwner.InvalidateScrollInfo(); }
return _screenSize; }

對於其它的Panel,如Grid,DockPanel等,基本上也可以按照這種方式實現,IScrollInfo介面處基本上可以保持不變,只需要重寫MeasureOverride和ArrangeOverride兩個函式即可。一個特殊的控制元件是StackPanel,由於它本身已經實現了IScrollInfo介面,也就是說它本身就有自身的自繪製滾動的方案,並且沒有提供介面在覆蓋自身的自繪製滾動,因此我們需要自己寫一個StackPanel,好在實現StackPanel並不難,由於篇幅有限,這裡我懶得繼續寫了,讀者朋友自己實現吧。至於那些非Panel的控制元件,實現就更簡單了,也留著讀者朋友自己實現吧。