Scrollable Friction Canvas For Silverlight

A while back I published a post about creating a friction enabled scrolling canvas in WPF (the old post can be found at http://sachabarber.net/?p=225), which I thought was way cool. It turns out that I was not the only one that thought this, and one of my WPF Buddies and fellow WPF Disciples Jeremiah Morrill thought it was so cool that he turned it into a Silverlight Content control. Thoughtful old Jerimiah even sent me the code back, which I think is awesome of him.

As a result I thought I would let you all have it. So here goes.

This is what Jer has done, here is the actual ContentControl, which has my friction stuff in it

   1:  using System;
   2:  using System.Windows;
   3:  using System.Windows.Controls;
   4:  using System.Windows.Input;
   5:  using System.Windows.Threading;
   6:  
   7:  namespace FlickScrollerApp
   8:  {
   9:      [TemplatePart(Name = "PART_ScrollViewer",
             Type = typeof(ScrollViewer))]
  10:      public class FlickScrollView : ContentControl
  11:      {
  12:          private readonly DispatcherTimer m_animationTimer =
  13:          new DispatcherTimer();
  14:          private double m_friction;
  15:          private Point m_currentMousePos;
  16:          private Point m_previousPoint;
  17:          private Point m_scrollStartOffset = new Point();
  18:          private Point m_scrollStartPoint;
  19:          private Point m_scrollTarget = new Point();
  20:          private Vector m_velocity;
  21:          private ScrollViewer scrollViewer;
  22:  
  23:          public FlickScrollView()
  24:          {
  25:              DefaultStyleKey = typeof(FlickScrollView);
  26:  
  27:              m_friction = 0.98;
  28:  
  29:              m_animationTimer.Interval = new
                     TimeSpan(0, 0, 0, 0, 20);
  30:              m_animationTimer.Tick += HandleWorldTimerTick;
  31:              m_animationTimer.Start();
  32:  
  33:              MouseMove += MouseMoveHandler;
  34:              MouseLeftButtonUp += MouseLeftButtonUpHandler;
  35:              MouseLeftButtonDown += MouseLeftButtonDownHandler;
  36:          }
  37:  
  38:          private bool IsMouseCaptured { get; set; }
  39:  
  40:          public double Friction
  41:          {
  42:              get { return 1.0 - m_friction; }
  43:              set { m_friction =
                       Math.Min(Math.Max(1.0 - value, 0), 1.0); }
  44:          }
  45:  
  46:          public override void OnApplyTemplate()
  47:          {
  48:              base.OnApplyTemplate();
  49:              scrollViewer = GetTemplateChild("PART_ScrollViewer")
                      as ScrollViewer;
  50:          }
  51:  
  52:          private void MouseLeftButtonDownHandler(object sender,
  53:          MouseButtonEventArgs e)
  54:          {
  55:              if (scrollViewer == null)
  56:                  return;
  57:  
  58:              m_scrollStartPoint = e.GetPosition(this);
  59:              m_scrollStartOffset.X = scrollViewer.HorizontalOffset;
  60:              m_scrollStartOffset.Y = scrollViewer.VerticalOffset;
  61:  
  62:              CaptureMouse();
  63:  
  64:              IsMouseCaptured = true;
  65:          }
  66:  
  67:          private void MouseLeftButtonUpHandler(object sender,
  68:          MouseButtonEventArgs e)
  69:          {
  70:              if (!IsMouseCaptured)
  71:                  return;
  72:  
  73:              ReleaseMouseCapture();
  74:              IsMouseCaptured = false;
  75:          }
  76:  
  77:          private void MouseMoveHandler(object sender, MouseEventArgs e)
  78:          {
  79:              if (scrollViewer == null)
  80:                  return;
  81:  
  82:              m_currentMousePos = e.GetPosition(this);
  83:  
  84:              if (IsMouseCaptured)
  85:              {
  86:                  Point currentPoint = e.GetPosition(this);
  87:  
  88:                  // Determine the new amount to scroll.
  89:                  var delta = new Point(m_scrollStartPoint.X -
  90:              currentPoint.X, m_scrollStartPoint.Y - currentPoint.Y);
  91:  
  92:                  m_scrollTarget.X = m_scrollStartOffset.X + delta.X;
  93:                  m_scrollTarget.Y = m_scrollStartOffset.Y + delta.Y;
  94:  
  95:                  // Scroll to the new position.
  96:                  scrollViewer.ScrollToHorizontalOffset(m_scrollTarget.X);
  97:                  scrollViewer.ScrollToVerticalOffset(m_scrollTarget.Y);
  98:              }
  99:          }
 100:  
 101:          private void HandleWorldTimerTick(object sender, EventArgs e)
 102:          {
 103:              if (scrollViewer == null)
 104:                  return;
 105:  
 106:              if (IsMouseCaptured)
 107:              {
 108:                  Point currentPoint = m_currentMousePos;
 109:                  m_velocity.X = m_previousPoint.X - currentPoint.X;
 110:                  m_velocity.Y = m_previousPoint.Y - currentPoint.Y;
 111:                  m_previousPoint = currentPoint;
 112:              }
 113:              else
 114:              {
 115:                  if (m_velocity.Length > 1)
 116:                  {
 117:                      scrollViewer.ScrollToHorizontalOffset(m_scrollTarget.X);
 118:                      scrollViewer.ScrollToVerticalOffset(m_scrollTarget.Y);
 119:                      m_scrollTarget.X += m_velocity.X;
 120:                      m_scrollTarget.Y += m_velocity.Y;
 121:                      m_velocity *= m_friction;
 122:                  }
 123:              }
 124:          }
 125:      }
 126:  }

.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; }And here is how you would use it in XAML

   1:  <UserControl x:Class="FlickScrollerApp.MainPage"
   2:               xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   3:               xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   4:               xmlns:FlickScrollerApp="clr-namespace:FlickScrollerApp"
   5:               Width="400"
   6:               Height="300">
   7:      <Grid x:Name="LayoutRoot"
   8:            Background="White">
   9:          <FlickScrollerApp:FlickScrollView>
  10:              <StackPanel>
  11:                  <Rectangle Fill="Red"
  12:                             Width="400"
  13:                             Height="200"
  14:                             IsHitTestVisible="False" />
  15:                  <Rectangle Fill="Blue"
  16:                             Width="400"
  17:                             Height="200"
  18:                             IsHitTestVisible="False" />
  19:                  <Rectangle Fill="Red"
  20:                             Width="400"
  21:                             Height="200"
  22:                             IsHitTestVisible="False" />
  23:                  <Rectangle Fill="Blue"
  24:                             Width="400"
  25:                             Height="200"
  26:                             IsHitTestVisible="False" />
  27:                  <Rectangle Fill="Red"
  28:                             Width="400"
  29:                             Height="200"
  30:                             IsHitTestVisible="False" />
  31:                  <Rectangle Fill="Blue"
  32:                             Width="400"
  33:                             Height="200"
  34:                             IsHitTestVisible="False" />
  35:                  <Rectangle Fill="Red"
  36:                             Width="400"
  37:                             Height="200"
  38:                             IsHitTestVisible="False" />
  39:              </StackPanel>
  40:          </FlickScrollerApp:FlickScrollView>
  41:      </Grid>
  42:  </UserControl>

.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; }

And another nice thing Jer has done is provide a Theme

   1:  <ResourceDictionary xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   2:                      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   3:                      xmlns:FlickScrollerApp="clr-namespace:FlickScrollerApp">
   4:      <Style TargetType="FlickScrollerApp:FlickScrollView">
   5:          <Setter Property="IsEnabled"
   6:                  Value="true" />
   7:          <Setter Property="HorizontalContentAlignment"
   8:                  Value="Left" />
   9:          <Setter Property="VerticalContentAlignment"
  10:                  Value="Top" />
  11:          <Setter Property="Cursor"
  12:                  Value="Arrow" />
  13:          <Setter Property="Background"
  14:                  Value="#00000000" />
  15:          <Setter Property="Template">
  16:              <Setter.Value>
  17:                  <ControlTemplate TargetType="FlickScrollerApp:FlickScrollView">
  18:                      <Border Background="{TemplateBinding Background}"
  19:                              BorderBrush="{TemplateBinding BorderBrush}"
  20:                              BorderThickness="{TemplateBinding BorderThickness}"
  21:                              CornerRadius="2">
  22:                          <ScrollViewer x:Name="PART_ScrollViewer">
  23:                              <ContentControl Content="{TemplateBinding Content}"
  24:                                ContentTemplate="{TemplateBinding ContentTemplate}"
  25:                                Cursor="{TemplateBinding Cursor}"
  26:                                HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
  27:                                HorizontalContentAlignment=
  28:                  "{TemplateBinding HorizontalContentAlignment}"
  29:                                FontFamily="{TemplateBinding FontFamily}"
  30:                                FontSize="{TemplateBinding FontSize}"
  31:                                FontStretch="{TemplateBinding FontStretch}"
  32:                                Foreground="{TemplateBinding Foreground}"
  33:                                Margin="{TemplateBinding Padding}"
  34:                                VerticalAlignment="{TemplateBinding VerticalAlignment}"
  35:                                VerticalContentAlignment=
  36:                  "{TemplateBinding VerticalContentAlignment}" />
  37:                          </ScrollViewer>
  38:                      </Border>
  39:                  </ControlTemplate>
  40:              </Setter.Value>
  41:          </Setter>
  42:      </Style>
  43:  </ResourceDictionary>

Here is a small Silverlight 3 project that demonstrates this.

flickscrollerapp.zip

Cheers Jer, you rock

About these ads

5 thoughts on “Scrollable Friction Canvas For Silverlight

  1. Matt M says:

    I’m getting an error on the property Vector m_velocity. I’m using Silverlight 4 and ‘Vector’ doesn’t seem to be apart of the framework. Do I need to add a reference to use Vectors? Or is it a custom class you created? Thanks.

    • sacha says:

      Its part of framework (from memory the one this uses), so it may not be available in SL. Reflect the demo app and make your own SL version by using reflector code

  2. Really cool, thanks for this. One future request – would it possible to have a bouce/rebound when you reach the start or end, instead of it just instantly stopping dead ?

    • sacha says:

      I wish I had the time, up to my head at work with business logic and FX deals right now…..Arggghhh

  3. No prob, I’ve worked out a quick and dirty implementation by adding an additional m_velocity.Length if condition in your HandleWorldTimerTick method. It checks for the extremeties of the scrollViewer’s ScrollableWidth and inverts the X and Y with a smaller velocity, thus giving a rebounded bounce effect. Cheers, LA

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