Friction Scrolling Now An 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:

http://sachabarber.net/?p=225

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:

http://sachabarber.net/?p=481

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 its a Friction enabled surface. Neato I say.

Here is Marlons 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:          /// <summary>
  17:          /// Friction Attached Dependency Property
  18:          /// </summary>
  19:          public static readonly DependencyProperty FrictionProperty =
  20:              DependencyProperty.RegisterAttached("Friction", typeof(double), typeof(KineticBehaviour),
  21:                  new FrameworkPropertyMetadata((double)0.95));
  22:   
  23:          /// <summary>
  24:          /// Gets the Friction property.  This dependency property 
  25:          /// indicates ....
  26:          /// </summary>
  27:          public static double GetFriction(DependencyObject d)
  28:          {
  29:              return (double)d.GetValue(FrictionProperty);
  30:          }
  31:   
  32:          /// <summary>
  33:          /// Sets the Friction property.  This dependency property 
  34:          /// indicates ....
  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", typeof(Point), typeof(KineticBehaviour),
  50:                  new FrameworkPropertyMetadata((Point)new Point()));
  51:   
  52:          /// <summary>
  53:          /// Gets the ScrollStartPoint property.  This dependency property 
  54:          /// indicates ....
  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.  This dependency property 
  63:          /// indicates ....
  64:          /// </summary>
  65:          private static void SetScrollStartPoint(DependencyObject d, 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", typeof(Point), typeof(KineticBehaviour),
  79:                  new FrameworkPropertyMetadata((Point)new Point()));
  80:   
  81:          /// <summary>
  82:          /// Gets the ScrollStartOffset property.  This dependency property 
  83:          /// indicates ....
  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.  This dependency property 
  92:          /// indicates ....
  93:          /// </summary>
  94:          private static void SetScrollStartOffset(DependencyObject d, 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", typeof(InertiaHandler), typeof(KineticBehaviour),
 108:                  new FrameworkPropertyMetadata((InertiaHandler)null));
 109:   
 110:          /// <summary>
 111:          /// Gets the InertiaProcessor property.  This dependency property 
 112:          /// indicates ....
 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.  This dependency property 
 121:          /// indicates ....
 122:          /// </summary>
 123:          private static void SetInertiaProcessor(DependencyObject d, 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 HandleKineticScrollingProperty =
 136:              DependencyProperty.RegisterAttached("HandleKineticScrolling", typeof(bool), 
 137:              typeof(KineticBehaviour),
 138:                  new FrameworkPropertyMetadata((bool)false,
 139:                      new PropertyChangedCallback(OnHandleKineticScrollingChanged)));
 140:   
 141:          /// <summary>
 142:          /// Gets the HandleKineticScrolling property.  This dependency property 
 143:          /// indicates ....
 144:          /// </summary>
 145:          public static bool GetHandleKineticScrolling(DependencyObject d)
 146:          {
 147:              return (bool)d.GetValue(HandleKineticScrollingProperty);
 148:          }
 149:   
 150:          /// <summary>
 151:          /// Sets the HandleKineticScrolling property.  This dependency property 
 152:          /// indicates ....
 153:          /// </summary>
 154:          public static void SetHandleKineticScrolling(DependencyObject d, bool value)
 155:          {
 156:              d.SetValue(HandleKineticScrollingProperty, value);
 157:          }
 158:   
 159:          /// <summary>
 160:          /// Handles changes to the HandleKineticScrolling property.
 161:          /// </summary>
 162:          private static void OnHandleKineticScrollingChanged(DependencyObject d, 
 163:              DependencyPropertyChangedEventArgs e)
 164:          {
 165:              ScrollViewer scoller = d as ScrollViewer;
 166:              if ((bool)e.NewValue)
 167:              {
 168:                  scoller.MouseDown += OnMouseDown;
 169:                  scoller.MouseMove += OnMouseMove;
 170:                  scoller.MouseUp += OnMouseUp;
 171:                  SetInertiaProcessor(scoller, new InertiaHandler(scoller));
 172:              }
 173:              else
 174:              {
 175:                  scoller.MouseDown -= OnMouseDown;
 176:                  scoller.MouseMove -= OnMouseMove;
 177:                  scoller.MouseUp -= OnMouseUp;
 178:                  var inertia = GetInertiaProcessor(scoller);
 179:                  if (inertia != null)
 180:                      inertia.Dispose();
 181:              }
 182:              
 183:          }
 184:   
 185:          #endregion
 186:   
 187:          #region Mouse Events
 188:          private static void OnMouseDown(object sender, MouseButtonEventArgs e)
 189:          {
 190:              var scrollViewer = (ScrollViewer)sender;
 191:              if (scrollViewer.IsMouseOver)
 192:              {
 193:                  // Save starting point, used later when determining how much to scroll.
 194:                  SetScrollStartPoint(scrollViewer, e.GetPosition(scrollViewer));
 195:                  SetScrollStartOffset(scrollViewer, new 
 196:                      Point(scrollViewer.HorizontalOffset, scrollViewer.VerticalOffset));
 197:                  scrollViewer.CaptureMouse();
 198:              }
 199:          }
 200:   
 201:   
 202:          private static void OnMouseMove(object sender, MouseEventArgs e)
 203:          {
 204:              var scrollViewer = (ScrollViewer)sender;
 205:              if (scrollViewer.IsMouseCaptured)
 206:              {
 207:                  Point currentPoint = e.GetPosition(scrollViewer);
 208:   
 209:                  var scrollStartPoint = GetScrollStartPoint(scrollViewer);
 210:                  // Determine the new amount to scroll.
 211:                  Point delta = new Point(scrollStartPoint.X - currentPoint.X, 
 212:                      scrollStartPoint.Y - currentPoint.Y);
 213:   
 214:                  var scrollStartOffset = GetScrollStartOffset(scrollViewer);
 215:                  Point scrollTarget = new Point(scrollStartOffset.X + delta.X, 
 216:                      scrollStartOffset.Y + delta.Y);
 217:   
 218:                  var inertiaProcessor = GetInertiaProcessor(scrollViewer);
 219:                  if (inertiaProcessor != null)
 220:                      inertiaProcessor.ScrollTarget = scrollTarget;
 221:                  
 222:                  // Scroll to the new position.
 223:                  scrollViewer.ScrollToHorizontalOffset(scrollTarget.X);
 224:                  scrollViewer.ScrollToVerticalOffset(scrollTarget.Y);
 225:              }
 226:          }
 227:   
 228:          private static void OnMouseUp(object sender, MouseButtonEventArgs e)
 229:          {
 230:              var scrollViewer = (ScrollViewer)sender;
 231:              if (scrollViewer.IsMouseCaptured)
 232:              {
 233:                  scrollViewer.ReleaseMouseCapture();
 234:              }
 235:          }
 236:          #endregion
 237:   
 238:          #region Inertia Stuff
 239:   
 240:          /// <summary>
 241:          /// Handles the inertia 
 242:          /// </summary>
 243:          class InertiaHandler : IDisposable
 244:          {
 245:              private Point previousPoint;
 246:              private Vector velocity;
 247:              ScrollViewer scroller;
 248:              DispatcherTimer animationTimer;
 249:   
 250:              private Point scrollTarget;
 251:              public Point ScrollTarget { 
 252:                  get { return scrollTarget; } 
 253:                  set { scrollTarget = value; } }
 254:   
 255:              public InertiaHandler(ScrollViewer scroller)
 256:              {
 257:                  this.scroller = scroller;
 258:                  animationTimer = new DispatcherTimer();
 259:                  animationTimer.Interval = new TimeSpan(0, 0, 0, 0, 20);
 260:                  animationTimer.Tick += new EventHandler(HandleWorldTimerTick);
 261:                  animationTimer.Start();
 262:              }
 263:   
 264:              private void HandleWorldTimerTick(object sender, EventArgs e)
 265:              {
 266:                  if (scroller.IsMouseCaptured)
 267:                  {
 268:                      Point currentPoint = Mouse.GetPosition(scroller);
 269:                      velocity = previousPoint - currentPoint;
 270:                      previousPoint = currentPoint;
 271:                  }
 272:                  else
 273:                  {
 274:                      if (velocity.Length > 1)
 275:                      {
 276:                          scroller.ScrollToHorizontalOffset(ScrollTarget.X);
 277:                          scroller.ScrollToVerticalOffset(ScrollTarget.Y);
 278:                          scrollTarget.X += velocity.X;
 279:                          scrollTarget.Y += velocity.Y;
 280:                          velocity *= KineticBehaviour.GetFriction(scroller);
 281:                      }
 282:                  }
 283:              }
 284:   
 285:              #region IDisposable Members
 286:   
 287:              public void Dispose()
 288:              {
 289:                  animationTimer.Stop();
 290:              }
 291:   
 292:              #endregion
 293:          }
 294:   
 295:          #endregion
 296:      }
 297:  }

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

Which to use you would simply do this :

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

   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>

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

As always here is a small demo app:

http://dl.dropbox.com/u/2600965/ScrollableAreaAttachedBehaviour.zip

About these ads

11 thoughts on “Friction Scrolling Now An WPF Attached Behaviour Too

  1. larry says:

    i read the codeproject for the attached behavior, where you can rightclick to scroll, and clicking the buttons brings up “grr”. efrost was messaging you about using stylus controls, did you ever get that working? and if you did, can you post the code? thanks.

  2. Jens says:

    I got this behavior working on Windows 7 by hooking the PreviewMouseDown event in KineticBehavior.cs instead of MouseDown.

  3. Nice behavior Sacha!

    Another way to get it to work is to attach to the mouse down event like this:
    scoller.AddHandler
    (
    ScrollViewer.MouseDownEvent,
    new MouseButtonEventHandler(OnMouseDown),
    true
    );

    Instead of the normal:
    scoller.MouseDown += OnMouseDown;

    It seems as if the Grid is swallowing the mouse down event.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s