Click here to Skip to main content
15,867,453 members
Articles / Desktop Programming / WPF

Friction Scrolling Now a WPF Attached Behaviour too

Rate me:
Please Sign up or sign in to vote.
4.92/5 (11 votes)
29 Dec 2009CPOL 84K   17   35
Friction scrolling is now a WPF attached behaviour too

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:

C#
  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:          /// <summary>
 17:          /// Friction Attached Dependency Property
 18:          /// </summary>
 19:          public static readonly DependencyProperty FrictionProperty =
 20:              DependencyProperty.RegisterAttached("Friction",
 21:              typeof(double), typeof(KineticBehaviour),
 22:                  new FrameworkPropertyMetadata((double)0.95));
 23:
 24:          /// <summary>
 25:          /// Gets the Friction property.  This dependency property
 26:          /// indicates ....
 27:          /// </summary>
 28:          public static double GetFriction(DependencyObject d)
 29:          {
 30:              return (double)d.GetValue(FrictionProperty);
 31:          }
 32:
 33:          /// <summary>
 34:          /// Sets the Friction property.
 35:          /// </summary>
 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:          /// <summary>
 46:          /// ScrollStartPoint Attached Dependency Property
 47:          /// </summary>
 48:          private static readonly DependencyProperty ScrollStartPointProperty =
 49:              DependencyProperty.RegisterAttached("ScrollStartPoint",
 50:              typeof(Point), typeof(KineticBehaviour),
 51:                  new FrameworkPropertyMetadata((Point)new Point()));
 52:
 53:          /// <summary>
 54:          /// Gets the ScrollStartPoint property.
 55:          /// </summary>
 56:          private static Point GetScrollStartPoint(DependencyObject d)
 57:          {
 58:              return (Point)d.GetValue(ScrollStartPointProperty);
 59:          }
 60:
 61:          /// <summary>
 62:          /// Sets the ScrollStartPoint property.
 63:          /// </summary>
 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:          /// <summary>
 75:          /// ScrollStartOffset Attached Dependency Property
 76:          /// </summary>
 77:          private static readonly DependencyProperty ScrollStartOffsetProperty =
 78:              DependencyProperty.RegisterAttached("ScrollStartOffset",
 79:              typeof(Point), typeof(KineticBehaviour),
 80:                  new FrameworkPropertyMetadata((Point)new Point()));
 81:
 82:          /// <summary>
 83:          /// Gets the ScrollStartOffset property.
 84:          /// </summary>
 85:          private static Point GetScrollStartOffset(DependencyObject d)
 86:          {
 87:              return (Point)d.GetValue(ScrollStartOffsetProperty);
 88:          }
 89:
 90:          /// <summary>
 91:          /// Sets the ScrollStartOffset property.
 92:          /// </summary>
 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:          /// <summary>
104:          /// InertiaProcessor Attached Dependency Property
105:          /// </summary>
106:          private static readonly DependencyProperty InertiaProcessorProperty =
107:              DependencyProperty.RegisterAttached("InertiaProcessor",
108:              typeof(InertiaHandler), typeof(KineticBehaviour),
109:                  new FrameworkPropertyMetadata((InertiaHandler)null));
110:
111:          /// <summary>
112:          /// Gets the InertiaProcessor property.
113:          /// </summary>
114:          private static InertiaHandler GetInertiaProcessor(DependencyObject d)
115:          {
116:              return (InertiaHandler)d.GetValue(InertiaProcessorProperty);
117:          }
118:
119:          /// <summary>
120:          /// Sets the InertiaProcessor property.
121:          /// </summary>
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:          /// <summary>
133:          /// HandleKineticScrolling Attached Dependency Property
134:          /// </summary>
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:          /// <summary>
144:          /// Gets the HandleKineticScrolling property.
145:          /// </summary>
146:          public static bool GetHandleKineticScrolling(DependencyObject d)
147:          {
148:              return (bool)d.GetValue(HandleKineticScrollingProperty);
149:          }
150:
151:          /// <summary>
152:          /// Sets the HandleKineticScrolling property.
153:          /// </summary>
154:          public static void SetHandleKineticScrolling(DependencyObject d,
155:              bool value)
156:          {
157:              d.SetValue(HandleKineticScrollingProperty, value);
158:          }
159:
160:          /// <summary>
161:          /// Handles changes to the HandleKineticScrolling property.
162:          /// </summary>
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:                  // Save starting point, used later when
196:                  //determining how much to scroll.
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:                  // Determine the new amount to scroll.
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:                  // Scroll to the new position.
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:          /// <summary>
247:          /// Handles the inertia
248:          /// </summary>
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:

C#
 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:

This article was originally posted at http://sachabarber.net?p=628

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United Kingdom United Kingdom
I currently hold the following qualifications (amongst others, I also studied Music Technology and Electronics, for my sins)

- MSc (Passed with distinctions), in Information Technology for E-Commerce
- BSc Hons (1st class) in Computer Science & Artificial Intelligence

Both of these at Sussex University UK.

Award(s)

I am lucky enough to have won a few awards for Zany Crazy code articles over the years

  • Microsoft C# MVP 2016
  • Codeproject MVP 2016
  • Microsoft C# MVP 2015
  • Codeproject MVP 2015
  • Microsoft C# MVP 2014
  • Codeproject MVP 2014
  • Microsoft C# MVP 2013
  • Codeproject MVP 2013
  • Microsoft C# MVP 2012
  • Codeproject MVP 2012
  • Microsoft C# MVP 2011
  • Codeproject MVP 2011
  • Microsoft C# MVP 2010
  • Codeproject MVP 2010
  • Microsoft C# MVP 2009
  • Codeproject MVP 2009
  • Microsoft C# MVP 2008
  • Codeproject MVP 2008
  • And numerous codeproject awards which you can see over at my blog

Comments and Discussions

 
QuestionSacha, you are my WPF God now :-) Pin
BrightShadow4-Dec-17 9:47
BrightShadow4-Dec-17 9:47 
GeneralArticle broken Pin
GravityPhazer9-Jul-16 11:59
professionalGravityPhazer9-Jul-16 11:59 
QuestionCan you optimize it please? Pin
qakmak7-Dec-15 5:45
qakmak7-Dec-15 5:45 
QuestionWhy don't you use animation replace timer? Pin
qakmak24-Nov-15 0:13
qakmak24-Nov-15 0:13 
AnswerRe: Why don't you use animation replace timer? Pin
qakmak24-Nov-15 2:40
qakmak24-Nov-15 2:40 
GeneralRe: Why don't you use animation replace timer? Pin
Sacha Barber24-Nov-15 3:37
Sacha Barber24-Nov-15 3:37 
GeneralRe: Why don't you use animation replace timer? Pin
qakmak26-Nov-15 0:46
qakmak26-Nov-15 0:46 
QuestionFlowDocumentScroll Viewer Pin
sharathkumar112-Oct-12 23:19
professionalsharathkumar112-Oct-12 23:19 
QuestionListBox. Pin
Brummell224-Aug-12 12:10
Brummell224-Aug-12 12:10 
AnswerRe: ListBox. Pin
Sacha Barber24-Aug-12 16:52
Sacha Barber24-Aug-12 16:52 
GeneralRe: ListBox. Pin
Brummell224-Aug-12 17:35
Brummell224-Aug-12 17:35 
GeneralRe: ListBox. Pin
qakmak24-Nov-15 0:11
qakmak24-Nov-15 0:11 
GeneralMy vote of 3 Pin
buyong27-Sep-11 17:17
buyong27-Sep-11 17:17 
GeneralMy vote of 3 Pin
mheskol1-Feb-11 6:29
mheskol1-Feb-11 6:29 
GeneralRe: My vote of 3 Pin
MmPizza5-Apr-12 6:33
MmPizza5-Apr-12 6:33 
QuestionQuestion Pin
stefan++15-Feb-10 23:08
stefan++15-Feb-10 23:08 
AnswerRe: Question Pin
Sacha Barber16-Feb-10 0:25
Sacha Barber16-Feb-10 0:25 
GeneralRe: Question Pin
stefan++16-Feb-10 3:16
stefan++16-Feb-10 3:16 
GeneralRe: Question Pin
Sacha Barber16-Feb-10 3:19
Sacha Barber16-Feb-10 3:19 
GeneralControls in the scrollviewer Pin
dev11223329-Dec-09 9:15
dev11223329-Dec-09 9:15 
GeneralRe: Controls in the scrollviewer Pin
Sacha Barber29-Dec-09 20:21
Sacha Barber29-Dec-09 20:21 
If you use the Preview mouse events it should be ok.

Sacha Barber
  • Microsoft Visual C# MVP 2008/2009
  • Codeproject MVP 2008/2009
Your best friend is you.
I'm my best friend too. We share the same views, and hardly ever argue

My Blog : sachabarber.net

GeneralRe: Controls in the scrollviewer Pin
Sacha Barber29-Dec-09 20:31
Sacha Barber29-Dec-09 20:31 
GeneralRe: Controls in the scrollviewer Pin
dev1122337-Jan-10 4:55
dev1122337-Jan-10 4:55 
GeneralRe: Controls in the scrollviewer Pin
Sacha Barber7-Jan-10 5:01
Sacha Barber7-Jan-10 5:01 
GeneralRe: Controls in the scrollviewer Pin
dev1122337-Jan-10 11:15
dev1122337-Jan-10 11:15 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.