08 March 2011

Simple Windows Phone 7 / Silverlight drag/flick behavior

In this post I describe a behavior that will essentially make anything draggable and ‘flickable’, that is, you can drag a GUI element along with your finger and it seems to have a little inertia when you let it go. The behavior mimics part of the Microsoft Surface’s ScatterView functionality and will feature in the small children’s game for Windows Phone 7 game I am currently developing. As a bonus, you will see some generally applicable extension methods for creating storyboards and translation GUI elements too.

I have not tried this in Silverlight but I am not doing anything Windows Phone 7 specific as far as I am aware, so I guess it should work in plain Silverlight too.

Setting the stage

I created a simple solution containing three assemblies: a Windows Phone application, a class library “LocalJoost” holding my utilities including this behavior, and one containing Phone.Fx.Preview, not for the BindableApplicationBar this time, but because I make extensive use of its VisualTreeHelperExtensions. You will also need System.Windows.Interactivity.dll, which I nicked from the MVVMLight toolkit.

Extensions for FrameworkElement

First of all, I created a set of extension methods on FrameworkElement that allow me to easily find and manipulate an attached CompositeTransform from code. A CompositeTransform is only one of the many possibilities to manipulate a GUI element, but it basically is just a descriptor of how far from its original place the GUI element should be drawn, if it should be rotated, scaled, etc. I only use the translation attributes – to move it around.

To be able to translated and animated, the object must have a CompositeTransform. Well, at least it must for the solution I have chosen. You will see later that the behavior checks for this transform and if it’s not present, it simply makes it. But you can also create it in XAML, of course.

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Linq;
using Phone7.Fx.Preview;

namespace LocalJoost.Utilities
{
  /// <summary>
  /// Class to help animations from code using the CompositeTransform
  /// </summary>
  public static class FrameworkElementExtensions
  {
    /// <summary>
    /// Finds the composite transform either direct
    /// or as part of a TransformGroup
    public static CompositeTransform GetCompositeTransform(
       this FrameworkElement fe)
    {
      if (fe.RenderTransform != null)
      {
        var tt = fe.RenderTransform as CompositeTransform;
        if( tt != null) return tt;

        var tg = fe.RenderTransform as TransformGroup;
        if (tg != null)
        {
          return tg.Children.OfType<CompositeTransform>().FirstOrDefault();
        }
      }
      return null;
    }

    /// <summary>
    /// Gets the point to where FrameworkElement is translated
    /// </summary>
    public static Point GetTranslatePoint(this FrameworkElement fe)
    {
      var translate = fe.GetCompositeTransform();
      if (translate == null) 
         throw new ArgumentNullException("CompositeTransform");

      return new Point(
        (double) translate.GetValue(CompositeTransform.TranslateXProperty),
        (double) translate.GetValue(CompositeTransform.TranslateYProperty));

    }

    /// <summary>
    /// Translates a FrameworkElement to a new location
    /// </summary>
    public static void SetTranslatePoint(this FrameworkElement fe, Point p)
    {
      var translate = fe.GetCompositeTransform();
      if (translate == null) 
         throw new ArgumentNullException("CompositeTransform");

      translate.SetValue(CompositeTransform.TranslateXProperty, p.X);
      translate.SetValue(CompositeTransform.TranslateYProperty, p.Y);
    }

    /// <summary>
    /// Translates a FrameworkElement to a new location
    /// </summary>
    public static void SetTranslatePoint(
       this FrameworkElement fe, double x, double y )
    {
      fe.SetTranslatePoint(new Point(x, y));
    }

    public static FrameworkElement GetElementToAnimate( 
      this FrameworkElement fe)
    {
      var parent = fe.GetVisualParent();
      return parent is ContentPresenter ? parent : fe;
    }
  }
}

Note in the last method that the element to animated is either the element itself or a ContentPresenter that is its parent. I noticed that stuff created by databinding is always surrounded by a ContentPresenter, and that animating an object within its ContentPresenter does not work – logically. So then then the ContentPresenter is animated in stead – dragging its contents with it.

Extensions for StoryBoard

Next up, a set of extension methods to easily create translation animations, and add them to storyboard:

using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Animation;

namespace LocalJoost.Utilities
{
  public static class StoryboardExtensions
  {
    /// <summary>
    /// Create a animation with a default easing function
    /// </summary>
    public static Timeline CreateDoubleAnimation(this Storyboard storyboard,
      Duration duration, double from, double to)
    {
      return storyboard.CreateDoubleAnimation(duration, from, to,
        new SineEase
        {
          EasingMode = EasingMode.EaseInOut
        });
    }

    /// <summary>
    /// Create a animation with a custom easing function
    /// </summary>
    public static Timeline CreateDoubleAnimation(this Storyboard storyboard, 
      Duration duration, double from, double to, IEasingFunction easingFunction)
    {
      var animation = new DoubleAnimation
      {
        From = from,
        To = to,
        Duration = duration,
        EasingFunction = easingFunction
      };
      return animation;
    }

    /// <summary>
    /// Add an animation to an existing storyboard
    /// </summary>
    public static void AddAnimation(this Storyboard storyboard, 
      DependencyObject item, Timeline t, DependencyProperty p)
    {
      if (p == null) throw new ArgumentNullException("p");
      Storyboard.SetTarget(t, item);
      Storyboard.SetTargetProperty(t, new PropertyPath(p));
      storyboard.Children.Add(t);
    }

    /// <summary>
    /// Add a translation animation to an existing storyboard with a 
    /// default easing function
    /// </summary>
    public static void AddTranslationAnimation(this Storyboard storyboard, 
      FrameworkElement fe, Point from, Point to, Duration duration)
    {
      storyboard.AddTranslationAnimation(fe, from, to, duration, null);
    }

    /// <summary>
    /// Add a translation animation to an existing storyboard with a 
    /// custom easing function
    /// </summary>
    public static void AddTranslationAnimation(this Storyboard storyboard, 
      FrameworkElement fe, Point from, Point to, Duration duration,
      IEasingFunction easingFunction)
    {
      storyboard.AddAnimation(fe.RenderTransform,
             storyboard.CreateDoubleAnimation(duration, from.X, to.X, 
                                              easingFunction),
              CompositeTransform.TranslateXProperty);
      storyboard.AddAnimation(fe.RenderTransform,
            storyboard.CreateDoubleAnimation(duration, from.Y, to.Y, 
                                             easingFunction),
              CompositeTransform.TranslateYProperty);
    }
  }
}

You will probably only use the last two methods. Create an empty StoryBoard, pop in a FrameworkElement, the “from” and “to” point, the time it should take and optionally an easing function, then start your StoryBoard and off your GUI element goes.

An easing function, by the way, describes a bit about the way an objects starts, stops and moves. No easing function means the object moves from start to end with a constant speed. If you take a SineEase with an EaseInOut mode, it will first move slowly, than increase speed, and at the end it more or less slowly comes to a halt. This makes for a more fluid behavior. There are about a gazillion easing functions, and I encourage you to explore them with Expression Blend.

The DragFlickBehavior itself

Then finally the behavior itself, which is surprisingly simple now all the groundwork has been laid by the extension methods:

using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interactivity;
using System.Windows.Media;
using System.Windows.Media.Animation;
using LocalJoost.Utilities;

namespace LocalJoost.Behaviors
{
  public class DragFlickBehavior: Behavior<FrameworkElement>
  {
    private FrameworkElement _elementToAnimate;
    
    protected override void OnAttached()
    {
      base.OnAttached();
      AssociatedObject.Loaded += AssociatedObjectLoaded;
      AssociatedObject.ManipulationDelta += 
         AssociatedObjectManipulationDelta;
      AssociatedObject.ManipulationCompleted += 
         AssociatedObjectManipulationCompleted;
    }
    
    void AssociatedObjectLoaded(object sender, RoutedEventArgs e)
    {
      _elementToAnimate = AssociatedObject.GetElementToAnimate();
      if (! (_elementToAnimate.RenderTransform is CompositeTransform))
      {
        _elementToAnimate.RenderTransform = new CompositeTransform();
        _elementToAnimate.RenderTransformOrigin = new Point(0.5, 0.5);
      }
    }

    void AssociatedObjectManipulationDelta(object sender, 
        ManipulationDeltaEventArgs e)
    {
      var dx = e.DeltaManipulation.Translation.X;
      var dy = e.DeltaManipulation.Translation.Y;
      var currentPosition = _elementToAnimate.GetTranslatePoint();
      _elementToAnimate.SetTranslatePoint(currentPosition.X + dx, 
         currentPosition.Y + dy);
    }

    private void AssociatedObjectManipulationCompleted(object sender, 
      ManipulationCompletedEventArgs e)
    {
      // Create a storyboard that will emulate a 'flick'
      var currentPosition = _elementToAnimate.GetTranslatePoint();
      var velocity = e.FinalVelocities.LinearVelocity;
      var storyboard = new Storyboard { FillBehavior = FillBehavior.HoldEnd };

      var to = new Point(currentPosition.X + (velocity.X / BrakeSpeed),
        currentPosition.Y + (velocity.Y / BrakeSpeed));
      storyboard.AddTranslationAnimation(
        _elementToAnimate, currentPosition, to, 
        new Duration(TimeSpan.FromMilliseconds(500)), 
        new CubicEase {EasingMode = EasingMode.EaseOut});
      storyboard.Begin();
    }


    protected override void OnDetaching()
    {
      AssociatedObject.Loaded -= AssociatedObjectLoaded;
      AssociatedObject.ManipulationCompleted -= 
          AssociatedObjectManipulationCompleted;
      AssociatedObject.ManipulationDelta-= 
          AssociatedObjectManipulationDelta;

      base.OnDetaching();
    }

    #region BrakeSpeed
    public const string BrakeSpeedPropertyName = "BrakeSpeed";

    /// <summary>
    /// Describes how fast the element should brake, i.e. come to rest,
    /// after a flick. Higher = apply more brake ;-)
    /// </summary>
    public int BrakeSpeed
    {
      get { return (int)GetValue(BrakeSpeedProperty); }
      set { SetValue(BrakeSpeedProperty, value); }
    }

    public static readonly DependencyProperty BrakeSpeedProperty = 
      DependencyProperty.Register(
      BrakeSpeedPropertyName,
      typeof(int),
      typeof(DragFlickBehavior),
      new PropertyMetadata(10));

    #endregion
  }
}

And half of it is a dependency property, too ;-). What this behavior does, is:

  • When attached, it checks which element to animate, checks for an CompositeTransform to be present and if not, simply create it.
  • While the element is being manipulated, change the translate point by the delta x and y of the drag event, therefore the element will follow your finger – and causing the element apparently to be ‘dragged’
  • When you stop dragging, it creates a simple storyboard that will move the element a bit further in the direction you last dragged it, seemingly causing a bit on inertia. It does that by looking at the FinalVelocities’s LinearVelocity property values. If you move it fast enough, it will move right out of the screen, never to return ;-). That’s more or less what I mean by ‘flick’. The BrakeSpeed property of the behavior determines how fast the element comes to a half. I find the default value 10 to have quite a natural feeling to it – on my phone, that is.

Demo

For the sample solution I fired up Expression Blend, dragged some elements on a Windows Phone 7 page and attached the behavior to everything - up to the page name and application name, as you can see in the demo video. That is not particularly useful in itself, but it drives home the point. Your designer can now have fun with this behavior ;-), and I hope I have showed you some little things on how to manipulate storyboard and elements from code in the process.

The DragFlickBehavior in action

4 comments:

Anonymous said...

Nice work Joost! I can remember I wanted to have something like that before, I only forgot what I needed it for ;-).

Unknown said...

This is excellent

Rakesh said...

Too Good, Would appreciate if you can help me with my issue:
I am trying to create a protractor in windows phone 7 using silverlight framework and I am stuck in getting the protractor needle to rotate.

I am using rotate transform and specifying the angle value to the transform name in the code behind.

I need a perfect angle calculation and currently the protractor needle is not rotating as required.

Any help would be truly appreciated, thanks.

Joost van Schaik said...

@rakesh I am happy to help if you would care to elaborate a little more on your problem. Mail me, for instance, some code.