一、先看看效果 二、原理 雖然效果很簡單,但是網上的一些資料涉及的代碼量非常可觀,而且效果也不是很理想,滾動的時候沒有一個順滑感。我這裡提供的源碼一共120多行,就能實現上圖的效果。 本質上我們只要接管ScrollViewer的滾動邏輯,並且把這個邏輯替換成帶有慣性的即可,那麼如何去接管呢?這裡的關 ...
一、先看看效果
二、原理
雖然效果很簡單,但是網上的一些資料涉及的代碼量非常可觀,而且效果也不是很理想,滾動的時候沒有一個順滑感。我這裡提供的源碼一共120多行,就能實現上圖的效果。
本質上我們只要接管ScrollViewer的滾動邏輯,並且把這個邏輯替換成帶有慣性的即可,那麼如何去接管呢?這裡的關鍵是先屏蔽ScrollViewer的滑鼠滾輪事件:
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 前,動畫已經結束,用戶可能已經手動改變了滾動條位置(也可能沒有,但這並不影響),所以就要給之前倆兄弟更新一下值了。
因為常見的慣性滾動以垂直方向居多,所以我沒有寫水平方向的邏輯,但也很容易擴展,有興趣的博友可以下載源代碼自己研究。
三、源碼
本文所討論的控制項源碼已經在github開源:https://github.com/NaBian/HandyControl