A while ago, I wrote about how to create a scrollable design surface in WPF, and how you could also add friction into the mix.
My original post was called “Creating A Scrollable Control Surface In WPF” which can be found at the following URL:
This original blog post proved to be quite popular and one of my fellow WPF Disciples my homeboy Jeremiah Morrill took it upon himself to rewrite my little control to be a content control for Silverlight, which you can get to at “Scrollable Friction Canvas For Silverlight” which can be found at the following URL:
I have been asked for my original code a lot, and another of my friends, and founder of the WPF Disciples, Marlon Grech took my code and has further improved it for WPF users, by making it an attached behaviour so all you have to do is hook up one property on your ScrollViewer
and bingo, it's a Friction enabled surface. Neato, I say.
Here is Marlon's attached behaviour code:
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Text;
5: using System.Windows;
6: using System.Windows.Controls;
7: using System.Windows.Input;
8: using System.Windows.Threading;
9:
10: namespace ScrollableArea
11: {
12: public class KineticBehaviour
13: {
14: #region Friction
15:
16: 17: 18: 19: public static readonly DependencyProperty FrictionProperty =
20: DependencyProperty.RegisterAttached("Friction",
21: typeof(double), typeof(KineticBehaviour),
22: new FrameworkPropertyMetadata((double)0.95));
23:
24: 25: 26: 27: 28: public static double GetFriction(DependencyObject d)
29: {
30: return (double)d.GetValue(FrictionProperty);
31: }
32:
33: 34: 35: 36: public static void SetFriction(DependencyObject d, double value)
37: {
38: d.SetValue(FrictionProperty, value);
39: }
40:
41: #endregion
42:
43: #region ScrollStartPoint
44:
45: 46: 47: 48: private static readonly DependencyProperty ScrollStartPointProperty =
49: DependencyProperty.RegisterAttached("ScrollStartPoint",
50: typeof(Point), typeof(KineticBehaviour),
51: new FrameworkPropertyMetadata((Point)new Point()));
52:
53: 54: 55: 56: private static Point GetScrollStartPoint(DependencyObject d)
57: {
58: return (Point)d.GetValue(ScrollStartPointProperty);
59: }
60:
61: 62: 63: 64: private static void SetScrollStartPoint(DependencyObject d,
65: Point value)
66: {
67: d.SetValue(ScrollStartPointProperty, value);
68: }
69:
70: #endregion
71:
72: #region ScrollStartOffset
73:
74: 75: 76: 77: private static readonly DependencyProperty ScrollStartOffsetProperty =
78: DependencyProperty.RegisterAttached("ScrollStartOffset",
79: typeof(Point), typeof(KineticBehaviour),
80: new FrameworkPropertyMetadata((Point)new Point()));
81:
82: 83: 84: 85: private static Point GetScrollStartOffset(DependencyObject d)
86: {
87: return (Point)d.GetValue(ScrollStartOffsetProperty);
88: }
89:
90: 91: 92: 93: private static void SetScrollStartOffset(DependencyObject d,
94: Point value)
95: {
96: d.SetValue(ScrollStartOffsetProperty, value);
97: }
98:
99: #endregion
100:
101: #region InertiaProcessor
102:
103: 104: 105: 106: private static readonly DependencyProperty InertiaProcessorProperty =
107: DependencyProperty.RegisterAttached("InertiaProcessor",
108: typeof(InertiaHandler), typeof(KineticBehaviour),
109: new FrameworkPropertyMetadata((InertiaHandler)null));
110:
111: 112: 113: 114: private static InertiaHandler GetInertiaProcessor(DependencyObject d)
115: {
116: return (InertiaHandler)d.GetValue(InertiaProcessorProperty);
117: }
118:
119: 120: 121: 122: private static void SetInertiaProcessor(DependencyObject d,
123: InertiaHandler value)
124: {
125: d.SetValue(InertiaProcessorProperty, value);
126: }
127:
128: #endregion
129:
130: #region HandleKineticScrolling
131:
132: 133: 134: 135: public static readonly DependencyProperty
136: HandleKineticScrollingProperty =
137: DependencyProperty.RegisterAttached("HandleKineticScrolling",
138: typeof(bool), typeof(KineticBehaviour),
139: new FrameworkPropertyMetadata((bool)false,
140: new PropertyChangedCallback(
141: OnHandleKineticScrollingChanged)));
142:
143: 144: 145: 146: public static bool GetHandleKineticScrolling(DependencyObject d)
147: {
148: return (bool)d.GetValue(HandleKineticScrollingProperty);
149: }
150:
151: 152: 153: 154: public static void SetHandleKineticScrolling(DependencyObject d,
155: bool value)
156: {
157: d.SetValue(HandleKineticScrollingProperty, value);
158: }
159:
160: 161: 162: 163: private static void OnHandleKineticScrollingChanged(DependencyObject d,
164: DependencyPropertyChangedEventArgs e)
165: {
166: ScrollViewer scoller = d as ScrollViewer;
167: if ((bool)e.NewValue)
168: {
169: scoller.PreviewMouseDown += OnPreviewMouseDown;
170: scoller.PreviewMouseMove += OnPreviewMouseMove;
171: scoller.PreviewMouseUp += OnPreviewMouseUp;
172: SetInertiaProcessor(scoller, new InertiaHandler(scoller));
173: }
174: else
175: {
176: scoller.PreviewMouseDown -= OnPreviewMouseDown;
177: scoller.PreviewMouseMove -= OnPreviewMouseMove;
178: scoller.PreviewMouseUp -= OnPreviewMouseUp;
179: var inertia = GetInertiaProcessor(scoller);
180: if (inertia != null)
181: inertia.Dispose();
182: }
183:
184: }
185:
186: #endregion
187:
188: #region Mouse Events
189: private static void OnPreviewMouseDown(object sender,
190: MouseButtonEventArgs e)
191: {
192: var scrollViewer = (ScrollViewer)sender;
193: if (scrollViewer.IsMouseOver)
194: {
195:
196:
197: SetScrollStartPoint(scrollViewer,
198: e.GetPosition(scrollViewer));
199: SetScrollStartOffset(scrollViewer,
200: new Point(scrollViewer.HorizontalOffset,
201: scrollViewer.VerticalOffset));
202: scrollViewer.CaptureMouse();
203: }
204: }
205:
206:
207: private static void OnPreviewMouseMove(object sender, MouseEventArgs e)
208: {
209: var scrollViewer = (ScrollViewer)sender;
210: if (scrollViewer.IsMouseCaptured)
211: {
212: Point currentPoint = e.GetPosition(scrollViewer);
213:
214: var scrollStartPoint = GetScrollStartPoint(scrollViewer);
215:
216: Point delta = new Point(scrollStartPoint.X -
217: currentPoint.X, scrollStartPoint.Y - currentPoint.Y);
218:
219: var scrollStartOffset = GetScrollStartOffset(scrollViewer);
220: Point scrollTarget = new Point(scrollStartOffset.X +
221: delta.X, scrollStartOffset.Y + delta.Y);
222:
223: var inertiaProcessor = GetInertiaProcessor(scrollViewer);
224: if (inertiaProcessor != null)
225: inertiaProcessor.ScrollTarget = scrollTarget;
226:
227:
228: scrollViewer.ScrollToHorizontalOffset(scrollTarget.X);
229: scrollViewer.ScrollToVerticalOffset(scrollTarget.Y);
230: }
231: }
232:
233: private static void OnPreviewMouseUp(object sender,
234: MouseButtonEventArgs e)
235: {
236: var scrollViewer = (ScrollViewer)sender;
237: if (scrollViewer.IsMouseCaptured)
238: {
239: scrollViewer.ReleaseMouseCapture();
240: }
241: }
242: #endregion
243:
244: #region Inertia Stuff
245:
246: 247: 248: 249: class InertiaHandler : IDisposable
250: {
251: private Point previousPoint;
252: private Vector velocity;
253: ScrollViewer scroller;
254: DispatcherTimer animationTimer;
255:
256: private Point scrollTarget;
257: public Point ScrollTarget
258: {
259: get { return scrollTarget; }
260: set { scrollTarget = value; }
261: }
262:
263: public InertiaHandler(ScrollViewer scroller)
264: {
265: this.scroller = scroller;
266: animationTimer = new DispatcherTimer();
267: animationTimer.Interval =
268: new TimeSpan(0, 0, 0, 0, 20);
269: animationTimer.Tick +=
270: new EventHandler(HandleWorldTimerTick);
271: animationTimer.Start();
272: }
273:
274: private void HandleWorldTimerTick(object sender,
275: EventArgs e)
276: {
277: if (scroller.IsMouseCaptured)
278: {
279: Point currentPoint = Mouse.GetPosition(scroller);
280: velocity = previousPoint - currentPoint;
281: previousPoint = currentPoint;
282: }
283: else
284: {
285: if (velocity.Length > 1)
286: {
287: scroller.ScrollToHorizontalOffset(
288: ScrollTarget.X);
289: scroller.ScrollToVerticalOffset(
290: ScrollTarget.Y);
291: scrollTarget.X += velocity.X;
292: scrollTarget.Y += velocity.Y;
293: velocity *=
294: KineticBehaviour.GetFriction(scroller);
295: }
296: }
297: }
298:
299: #region IDisposable Members
300:
301: public void Dispose()
302: {
303: animationTimer.Stop();
304: }
305:
306: #endregion
307: }
308:
309: #endregion
310: }
311: }
Which to use you would simply do this:
1: <Window x:Class="ScrollableArea.Window1"
2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4: xmlns:local="clr-namespace:ScrollableArea"
5: Title="Window1" Height="300" Width="300">
6: <Window.Resources>
7:
8: <!-- scroll viewer -->
9: <Style x:Key="ScrollViewerStyle"
10: TargetType="{x:Type ScrollViewer}">
11: <Setter Property="HorizontalScrollBarVisibility"
12: Value="Hidden" />
13: <Setter Property="VerticalScrollBarVisibility"
14: Value="Hidden" />
15: </Style>
16:
17: </Window.Resources>
18:
19: <Grid Margin="0">
20: <ScrollViewer x:Name="ScrollViewer"
21: Style="{StaticResource ScrollViewerStyle}"
22: local:KineticBehaviour.HandleKineticScrolling="True">
23: <ItemsControl x:Name="itemsControl"
24: VerticalAlignment="Center">
25:
26: <ItemsControl.ItemsPanel>
27: <ItemsPanelTemplate>
28: <!-- Custom Panel-->
29: <StackPanel Orientation="Vertical"/>
30: </ItemsPanelTemplate>
31: </ItemsControl.ItemsPanel>
32:
33:
34: </ItemsControl>
35: </ScrollViewer>
36: </Grid>
37:
38: </Window>
As always, here is a small demo app: