WPF 如何流暢地滾動ScrollViewer 簡單實現下

  • 2020 年 6 月 13 日
  • 筆記

看了看原生UWP的ScrollViewer,滑動很流暢(例如 開始菜單),但是WPF自帶的ScrollViewer滾動十分生硬..

突發奇想,今天來實現一個流暢滾動的ScrollViewer.

一、目標

查閱網上的實現方法,要麼直接重寫控制項,要麼一堆Storyboard..很是無奈,還有些許bug.

主要目標如下:

1.能夠流暢地滾動ScrollViewer   要求:有慣性的物理滾動 而不是 生硬的 內容滾動.

2.在使用動畫的過程中,避免多個begin導致卡頓

3.好用(? )

二、準備工作

實現以上目標需要到的幾件東西:

1.對於Listbox之類的控制項需要先講滾動方式變為 像素滾動  (若沒有的話就可以不設置)

<ListBox
VirtualizingPanel.ScrollUnit = "Pixel" 
VirtualizingPanel.VirtualizationMode="Recycling"/>

 

2.選一種動畫使用的 緩動函數 這裡推薦使用 CubicEase的EaseOut函數 (優點:接近於慣性滾動  對於CPU比較友好)

 

3.要對ScrollViewer啟用滾動動畫 需要自己寫依賴屬性:

public static class ScrollViewerBehavior
{
      public static readonly DependencyProperty VerticalOffsetProperty = DependencyProperty.RegisterAttached("VerticalOffset", typeof(double), typeof(ScrollViewerBehavior), new UIPropertyMetadata(0.0, OnVerticalOffsetChanged));
      public static void SetVerticalOffset(FrameworkElement target, double value) => target.SetValue(VerticalOffsetProperty, value);
      public static double GetVerticalOffset(FrameworkElement target) => (double)target.GetValue(VerticalOffsetProperty);
      private static void OnVerticalOffsetChanged(DependencyObject target, DependencyPropertyChangedEventArgs e) => (target as ScrollViewer)?.ScrollToVerticalOffset((double)e.NewValue);
}

 

三、編碼

    //繼承ScrollViewer 通過截獲MouseWheel事件控制滾動
    public class MyScrollViewer : ScrollViewer
    {
        //記錄上一次的滾動位置
        private double LastLocation = 0;
        //重寫滑鼠滾動事件
        protected override void OnMouseWheel(MouseWheelEventArgs e)
        {
            double WheelChange = e.Delta;
            //可以更改一次滾動的距離倍數 (WheelChange可能為正負數!)
            double newOffset = LastLocation - (WheelChange * 2);
            //Animation並不會改變真正的VerticalOffset(只是它的依賴屬性) 所以將VOffset設置到上一次的滾動位置 (相當於銜接上一個動畫)
            ScrollToVerticalOffset(LastLocation);
            //碰到底部和頂部時的處理
            if (newOffset < 0)
                newOffset = 0;
            if (newOffset > ScrollableHeight)
                newOffset = ScrollableHeight;

            AnimateScroll(newOffset);
            LastLocation = newOffset;
            //告訴ScrollViewer我們已經完成了滾動
            e.Handled = true;
        }
        private void AnimateScroll(double ToValue)
        {
            //為了避免重複,先結束掉上一個動畫
            BeginAnimation(ScrollViewerBehavior.VerticalOffsetProperty, null);
            DoubleAnimation Animation = new DoubleAnimation();
            Animation.EasingFunction = new CubicEase() { EasingMode = EasingMode.EaseOut };
            Animation.From = VerticalOffset;
            Animation.To = ToValue;
            //動畫速度
            Animation.Duration = TimeSpan.FromMilliseconds(800);
            //考慮到性能,可以降低動畫幀數
            //Timeline.SetDesiredFrameRate(Animation, 40);
            BeginAnimation(ScrollViewerBehavior.VerticalOffsetProperty, Animation);
        }
    }

使用方法:直接創建 <MyScrollViewer/>

如果是在ListBox中,可以通過修改模板的方式,把<ScrollViewer/>換成MyScrollViewer即可.

四、關於性能

按照上述方法實現的功能,CPU佔用率基本穩定在10% ,但是要瘋狂地上下滑動滾輪的話,25%+   但是動畫還是很流暢的

以下有幾點可以改進的措施:

滾動的CPU佔用不只是對偏移量的一個計算,與你滾動的UI畫面內容具有很大的關係

例如大量的圖片Image   3D對象    複雜的自定義控制項 等等 都需要在滾動時計算並繪製。

你可以嘗試以下方法:

     1).啟用ListBox的虛擬化技術

     2).對於影像的繪製,可以通過降低其渲染度(可能會損壞圖片品質 特別是低清小圖 ),對你的圖片對象使用:

RenderOptions.SetBitmapScalingMode(控制項對象,BitmapScalingMode.LowQuality);

     3).就像上文中所說,可以通過降低渲染幀數來控制CPU佔用率(可能會有失流暢度)

     4).如果能找到更好的緩動函數

     5).參閱: 優化控制項性能 -WPF |Microsoft Docs