Silverlight Playground
about Silverlight and other Amenities

A bit of Physics for an InertialScrollViewer

2009-05-10T01:22:27+01:00 by Andrea Boschin

Silverlight 2.0 and 3.0 come with an useful control called ScrollViewer, that helps when long list of elements needs to be scrolled with a traditional scrollbar. New mobile devices like the iPhone introduced a new kind of ScrollViewer where the user only needs to use his thumb to move the lists of elements on the screen. This beautiful kind of ScrollViewer often give an impressive feedback with a fluid scrolling that ends in a real soft deceleration giving the impression that the bend of elements is subject to the physics rules. This is not only scenic, but give the user a natural feedback that take usability to the maximum levels. So, in this article I would want to illustrate how to apply some simple physics formula to create a reusable InertialScrollViewer to take this feedback into Silverlight applications.

Please note that I'm not a physicist. I have only searched my school notions and tried to get something to work. And it works fine as you will see... :)

How the control has to work

Before entering the coding phase of the control, we need to understand how the control has to work. From the developer perspective the InertialScrollViewer must work similar to the original ScrollViewer. It have to scroll the content generated inside of it and let the user scroll Vertically or Horizontally. My implementation of this control let the developer to choice if scroll Vertically or Horizontally but not in both the directions at the same time.

From the user perspective we have two different phases: when the user click on the band he can move it using the mouse (e.g from left to the right). While the user is moving, the band is simply locked to the mouse and if the user stop scrolling the bend also stops. Only when the user leave the mouse button while he is scrolling, it has to maintain the current speed and decrease it to zero in a variable distance. The calculation of the speed is the most difficult thing, infact we have to take note of the change of direction and not only of the difference from the starting point to the end of the scrolling. If the user start scrolling from left to right and at a given point stops he turn scrolling from the right to left, the speed needs to be recalculated from the direction change and not from the start of the scrolling action. To avoid this kind of problems I decided to sample the speed at every MouseMove event. Every time this event will be raised I take note of "ticks" and "distance" from the previous event and calculate the speed with the simple formula

speed = distance / time

For a better precision I record the last two samples and when the user leave the mouse button I calculate the medium of the two available samples.

The other phase of the user interaction enter when the user stop dragging and the band start decrease its speed. All calculation is done when this event occur. First we have to calculate the distance that the band will race with the starting speed and then we have to calculate how much time it take to decrease speed to zero. The first part use this formula:

distance = speed ^ 2 / 20 * f

This formula, I found in my physics schoolbook, use f as friction factor to determine the type of surface where the bend is moving. A number less then 0.5 determine a slippery surface so the bend will run a long distance. A number from .5 to 1 determine a rough surface. This value is exposed by my control so the developer can change it and give different feedback to the user.

To calculate the time needed for the bend going from the start speed to zero I found another formula that get a deceleration value. Here is the formula

time = -speed / -a 

Where "-a" is the negative acceleration. This formula result from a more complicated formula that I've simplified due to some 0 occurrence. The simplification process is out of the scope of this article.

Now that we have all calculation done, it is time to start implementing the control.

The InertialScrollViewer ControlTemplate

The first thing to do is implementing a templated control. Like every control in Silverlight the InertialScrollViewer will take advantage of a template that will be saved in Themes/generic.xaml. Here we cannot inherit from the ScrollViewer control because it is sealed so our custom scroller will be a ContentControl and it allow to scroll content put into itself.

The template or the InertialScrollViewer is made of a Border containing a Canvas and then a ContentControl. I put some defaults to give the InertialScrollViewer an appearance similato to the original ScrollViewer. Here is the template:

   1: <Style TargetType="local:InertialScrollViewer">
   2:         <Setter Property="Padding" Value="0"/>
   3:         <Setter Property="Background" Value="#FFFFFFFF"/>
   4:         <Setter Property="BorderThickness" Value="1"/>
   5:         <Setter Property="VerticalContentAlignment" Value="Stretch"/>
   6:         <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
   7:         <Setter Property="BorderBrush">
   8:             <Setter.Value>
   9:                 <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
  10:                     <GradientStop Color="#FFA3AEB9" Offset="0"/>
  11:                     <GradientStop Color="#FF8399A9" Offset="0.375"/>
  12:                     <GradientStop Color="#FF718597" Offset="0.375"/>
  13:                     <GradientStop Color="#FF617584" Offset="1"/>
  14:                 </LinearGradientBrush>
  15:             </Setter.Value>
  16:         </Setter>
  17:         <Setter Property="Template">
  18:             <Setter.Value>
  19:                 <ControlTemplate>
  20:                     <Border x:Name="RootElement" 
  21:                             CornerRadius="2"                            
  22:                             Padding="{TemplateBinding Padding}"
  23:                             Background="{TemplateBinding Background}"
  24:                             BorderBrush="{TemplateBinding BorderBrush}" 
  25:                             BorderThickness="{TemplateBinding BorderThickness}">
  26:                         <Border.Resources>
  27:                             <Storyboard x:Name="EasingStoryboard">
  28:                                 <DoubleAnimation />
  29:                             </Storyboard>
  30:                         </Border.Resources>
  31:                         <Canvas x:Name="CanvasElement">
  32:                             <ContentControl x:Name="ContentElement"
  33:                                             Content="{TemplateBinding Content}"
  34:                                             VerticalAlignment="Stretch"
  35:                                             HorizontalAlignment="Stretch"
  36:                                             VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
  37:                                             HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" />
  38:                         </Canvas>
  39:                     </Border>
  40:                 </ControlTemplate>
  41:             </Setter.Value>
  42:         </Setter>
  43:     </Style>

The <Border /> "RootElement" inside the template will be the outer border of the control. It receive the default values and have a shaded BorderBrush. It contains a StoryBoard into its resources. I will return on it later in the article.

The <Canvas /> "CanvasElement" is required to be able to scroll the ContentControl I put inside of it. After doing some tests I found that the Canvas is the only control that let me apply some Transform to the ContentControl and continue to show the content without any clipping. People using the control will have no knowledge about the Canvas.

Finally the ContentControl has been configured to let the content stretch to fill all the available space. Its VerticalContentAlignment and HorizontalContentAlignment property has been binded to the corresponding template properties so the user can configure the control behavior.

Let's do some code...

Now it is time to start coding. The control is about 300 line long so it is impossible to explain all the code. I will explain the key point following the control lifetime. After doing some inizialization in the control constructor the first place where it is required we write some code is the OnApplyTemplate method. Inside this method we have to hook every element of our template. The elements will be assigned to some member properties using the GetTemplateChild method. To let the control customizable I have also specified the TemplatePart attributes on top of the class. Here is the parts:

   1: [TemplatePart(Name = InertialScrollViewer.RootElementName, Type = typeof(Border))]
   2: [TemplatePart(Name = InertialScrollViewer.CanvasElementName, Type = typeof(Canvas))]
   3: [TemplatePart(Name = InertialScrollViewer.ContentElementName, Type = typeof(ContentControl))]
   4: [TemplatePart(Name = InertialScrollViewer.EasingStoryboardName, Type = typeof(Storyboard))]

Every element has its own wrapping constant to centralize the names in a single point. During the OnApplyTemplate this names will be used to pick up the elements and assign to the corresponding properties. I also create a TranslateTransform into this method and assign it to the RenderTransform property of the ContentControl. The reason to create the TranslateTransform - used to scroll the content - is that if the user customize it can easily break the control functionality.

   1: public override void OnApplyTemplate()
   2: {
   3:     // Border
   4:     this.RootElement = GetTemplateChild(InertialScrollViewer.RootElementName) as Border;
   5:  
   6:     // Canvas
   7:     this.CanvasElement = GetTemplateChild(InertialScrollViewer.CanvasElementName) as Canvas;
   8:     this.CanvasElement.SizeChanged += new SizeChangedEventHandler(CanvasElement_SizeChanged);
   9:  
  10:     // clip the content to the Width and Height
  11:     this.CanvasElement.Clip =
  12:         this.CanvasClipping = new RectangleGeometry
  13:         {
  14:             Rect = new Rect(0, 0, this.CanvasElement.ActualWidth, this.CanvasElement.ActualHeight)
  15:         };
  16:  
  17:     // ContentControl
  18:     this.ContentElement = GetTemplateChild(InertialScrollViewer.ContentElementName) as ContentControl;
  19:     this.ContentElement.RenderTransform =
  20:         this.Translate = new TranslateTransform { X = 0, Y = 0 };
  21:  
  22:     this.ContentElement.MouseLeftButtonDown += (a, b) => this.HandleMouseDown(this.GetMousePosition(b, this.Orientation));
  23:     this.ContentElement.MouseLeftButtonUp += (a, b) => this.HandleMouseUp(this.GetMousePosition(b, this.Orientation));
  24:     this.ContentElement.MouseMove += (a, b) => this.HandleMouseMove(this.GetMousePosition(b, this.Orientation));
  25:  
  26:     // StoryBoard
  27:     this.EasingStoryboard = this.RootElement.Resources[InertialScrollViewer.EasingStoryboardName] as Storyboard;
  28:     this.EasingAnimation = this.EasingStoryboard.Children.OfType<DoubleAnimation>().FirstOrDefault();
  29:  
  30:     // let do some inizialization
  31:     this.OnOrientationChanged(this.Orientation);
  32:     this.OnEasingFunctionChanged(this.EasingFunction);
  33:  
  34:     // and call the base class
  35:     base.OnApplyTemplate();
  36: }

The last thing to take note is the Clipping. The Canvas is a control that do not have a frame. The content can be put at negative coordinates but it is not clipped if there is some space available. So I have created a RectangleGeometry and I use it to clip the Canvas at his ActualWidth and ActualHeight. This is done in the CanvasElement_SizeChanged event handler every time the size of the element will be changed.

During the OnApplyTemplate method I attach some event handlers to the ContentControl to handle the Mouse events MouseMove, MouseLeftButtonDown and MouseLeftButtonUp. This events let me know when the user try to scroll the content.

In the MouseLeftButtonDown I do some inizialization. I Capture the mouse, inizializa the Translation and then start the sampling using an instance of the SpeedSample class. In the MouseMove I do the sampling using the same specialized class SpeedSampler. The SpeedSampler class is responsible to count the incoming events from MouseMove and record the last two samples. In my opinon the class is pretty simple and do not need any explanation. In the same event I also apply the value to the TranslateTransform to move the content to the required position.

When the user leave the mouse button the flow goes to the MouseLeftButtonUp event handler and inside of it I do alle the required calculation. First of all I take note of the current speed and then I calculate the distance and the duration of the deceleration.

   1: /// <summary>
   2: /// Handles the mouse up.
   3: /// </summary>
   4: /// <param name="mousePosition">The mouse position.</param>
   5: private void HandleMouseUp(double mousePosition)
   6: {
   7:     if (this.GetBoundary(this.Orientation) >= 0) return;
   8:  
   9:     this.IsCaptured = false;
  10:     this.ContentElement.ReleaseMouseCapture();
  11:     double speed = this.Sampler.GetSpeed();
  12:  
  13:     if (this.EasingStoryboard != null && this.EasingAnimation != null)
  14:     {
  15:         this.EasingAnimation.To = this.ComputeNextPosition(Math.Pow(speed, 2) / (20 * this.FrictionFactor) / 1000 * this.Sampler.Direction);
  16:         this.EasingAnimation.Duration = TimeSpan.FromSeconds(-(speed / 1000) / -this.Deceleration);
  17:         this.EasingStoryboard.Begin();
  18:     }
  19: }

Once I have calculated this parameters the only thing I have to do is to assign them to the Storyboard and start the animation. The EasingStoryboard has been picked up from the template and will run the scroller to its calculated end position. If the user will click on the content during the easing animation I simply stop the animation and forget all the calculation.

The Storyboard has a default QuadraticEasing function applied that to me is the better choice to give a natural behavior. The control expose an EasingFunction property to let the user cutomize the animation easing. Putting and ElasticEase give the control an astounding elastic behavior.

The control expose some other properties to let the user customize it. Orientation let you decide if scrolling is vertical or horizontal. FrictionFactor and Deceleration let customize the physics parameters. I suggest you to try by yourself changing the properties and the easing to give the control the preferred behavior.

Some final words.

Attached to this article you will find the complete source of the control. I would like to have your feedback about how it works. I made many tests and to me it is working fine but I'm sure that it is not perfect. If you find some strange behavior please post a comment and I will try to get rid of it.

Download: (~1.1 MB)

Video: (~2.6 MB)

MIX09 Keynote Demo Available

2009-03-22T08:21:49+01:00 by Andrea Boschin

The source code of the demo showed by Scott Guthrie during the Keynote of the MIX 2009 last week has been made available by Henry Hahn. It is a very amazing demonstration on how it is possible to do with the new features of Silverlight 3.0.

The demo derive from a similar project crated the last year for MIX 08 about WPF.

ripple

| | Download Project

Categories:   MIX09 | Demo | Animations | Effects
Actions:   E-mail | del.icio.us | Permalink | Comments (2) | Comment RSSRSS comment feed

Silverlight 3.0: Easing Functions

2009-03-18T18:44:00+01:00 by Andrea Boschin

I know. Every time you try to animate something in a Silverlight scene, but also in WPF, the result is impressive, but also is always slightly unnatural. The problem here is that in the real world the movements is ruled by some natural forces that simply modify the path and speed of moving object. Friction, gravity,  centrifugal force and other phisical rules modify movements in a non linear way. We live in a world filled by this rules and every thing not respecting them appear unnatural.

Simulating a bounce or a gravity deceleration or acceleration in Silverlight 2.0 require to handle complex animations made of a big amount of keyframe, and is very hard to accomplish. All this rules are often easily to write as a mathematical formula so the Silverlight 3.0 team have introduced the Easing functions, a way to let the maths do its work and to let our animation to become very easy and simple.

Using an Easing function is really easy. You have to declare the function in the resources, using one of the built-in functions, and then refer to it from an animation EasingFuncion attribute. Here is an example:

   1: <Canvas.Resources>
   2:  
   3:     <SineEase x:Key="easeOut" EasingMode="EaseOut" />
   4:  
   5:     <Storyboard x:Name="animRXOut">
   6:         <DoubleAnimation To="0" Duration="00:00:00.300"
   7:                          Completed="animRXOut_Completed"
   8:                          EasingFunction="{StaticResource easeOut}"
   9:                          Storyboard.TargetName="ball1" Storyboard.TargetProperty="(RenderTransform).(Angle)" />
  10:     </Storyboard>
  11:  
  12: </Canvas.Resources>

Every easing function let us choice if we need an EaseIn, EaseOut or both EaseInOut. This cause the easing function to be applied to the start of the animation, to the end or to both start and end. There are a couple of built-in functions in silverlight 3.0:

  • BackEase: This moves the animation backwards a little before continuing. It’s a little bit like starting a car on a hill, you roll back a little before you move forward.
  • BounceEase: As we saw in the previous example, this creates a bouncing effect.
  • CircleEase: This accelerates the animation based on a circular function, where the initial acceleration is slower and the latter acceleration is higher.
  • CubicEase: This is similar to the CircleEase, but is based on the cubic formula of time causing a slower acceleration in the beginning and a more rapid one towards the end of the animation.
  • ElasticEase: This is similar to the BounceEase in that it oscillates the value until it comes to a rest.
  • ExponentialEase: Similar to the Circle and Cubic ease in that it is an exponential acceleration from one value to the next.
  • PowerEase: This is an exponential acceleration where the value of the ease is proportional to the power of the time.
  • QuadraticEase: This is very similar to the CubicEase except that in this case the value is based on the square of the time.
  • QuarticEase: Similar to Quadratic and Cubic. This time the value is based on the cube of the time.
  • QuinticEase: Again, similar to Quadratic, Cubic and Quartic. This time the value is based on the time to the power of 5.
  • SineEase: This accelerates the value along a sine wave.

Using Easing functions in practice

Now let you imagine to have to create a simple animation. We have to simulate two iron balls bouncing each on the other. Think at the games were you have two or more balls suspended by a wire.

The animation is composed of 4 segments:

1) the left ball goes to an angle of 40 degree

2) the left ball returns to an angle of 0 degrees

3) the right ball goes to an angle of -40 degrees

4) the right ball returns to an angle of 0 degrees

This animations are concatenated each other using the completed event. Every time an animation completes it start the next animation so we have a perpetual movement. If we run the example without any easing we see the balls moving in an unnatural way. We need to add at least two easing function. The first one is to easing-out the animations 1) and 3). The second is to easing-in the animations 2) and 4). The ball decelerate using a SineEase when goes to 40 (or -40) degrees because of the gravity and accelerate for the same reason when returns to zero using a CubicEase.

You may see a full size screenshot here:

How the Easing Function works

We already said that an easing function is simply a mathematical function applied to the steps of an animation. It is really simple to create your own easing functions by implementing the abstract class EasingFunctionBase, but you need to understand how the function has to calculate the values. If you think at the animation as a 0 when it starts and 1 when it stops, then you will have a value from 0 to 1 to represent a fraction of the animation itself.

The easing function has to work with this values to calculate a resulting position for each step. The values may also have a value out of the range of 0 to 1 resulting in the animation to overcome the bounds of the animation. This is the case of an ElasticEase where the end of the animation go up and down of the final value.

To better understand the easing function let me do another example using the previous scene. To avoid the balls moving perpetually after you start the animation clicking the button, every time the ball reach the upper bound the maximum limit has to be decreased so it reach the value of zero after a couple of bounces. In a real world also the ramp down is not linear. For this sample I've used a QuadraticEase initializing it in the code and calling by myself the Ease method. Here is the code snippet:

   1: /// <summary>
   2: /// Initializes a new instance of the <see cref="Page"/> class.
   3: /// </summary>
   4: public Page()
   5: {
   6:     this.OscillationEase = new QuadraticEase { EasingMode = EasingMode.EaseOut };
   7:     InitializeComponent();
   8: }

I've managed to have a number from 1 to 0 representing the steps of the animation decreased by 0.025 every time an animation reach the end. This number multiplied by 40 give the maximum amount of degree to rotate. To ease the transition from 1 to 0 I call the Ease method:

   1: private void animRXIn_Completed(object sender, EventArgs e)
   2: {
   3:     this.current -= 0.025;
   4:  
   5:     if (this.current > 0)
   6:     {
   7:         ((DoubleAnimation)animLXOut.Children[0]).To = this.OscillationEase.Ease(this.current) * 40.0;
   8:         animLXOut.Begin();
   9:     }
  10:     else
  11:         startButton.IsEnabled = true;
  12: }

I think this sample have explained how the EasingFunction work. Now you have to try by yourself. Easing Function are powerful tools in the hands of a designer because give an interface a better appeal and a more fluid and believable animations.

Download Code: (552 KB)

Demo Video: (720 KB)

Tags:   ,
Categories:   Animations | MIX09
Actions:   E-mail | del.icio.us | Permalink | Comments (0) | Comment RSSRSS comment feed