01 April 2012

Porting the DragFlickBehavior from Windows Phone 7 to Windows 8 Metro Style

Preface

A little over a year ago I made DragFlickBehavior, a behavior for Windows Phone that makes essentially 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. In my previous post, I described the basics of how to make a behavior at all for Windows 8 Metro style. The testing of this was done using a ported version of the DragFlickBehavior. I’ve retraced my steps to how I got it to work, and will describe the process of porting an existing behavior here.

For the DragFlickBehavior to work, some groundwork needed to be layed first. For Windows Phone, I made a couple of extension methods for both FrameworkElement and StoryBoard first. To make matters worse, one of those extension methods in FrameworkElementExtensions used yet another extension method – GetVisualParent in VisualTreeHelperExtensions from Phone7.Fx… Nil desperandum… I’ll start at the beginning

Porting  VisualTreeHelperExtensions

I created a class library Win8nl.External, copied VisualTreeHelperExtensions.cs from it’s codeplex location, and opened the it the editor. And then the process was pretty simple:

  • The namespace System.Windows.Media is gone. So I deleted it’s using.
  • I basically clicked every red line, hit Control-.  (that’s Control-dot) and in most cases the editor would suggest a name space to add

In the end I seemed to have added

using Windows.UI.Xaml;
using Windows.UI.Xaml.Media;

And then there was this slight matter of 2 places where the author calls VisualStateManager.GetVisualStateGroups and expects the result to be an IList. Now it’s an IEnumerable. Anyway, I solved this by changing the

IList groups = VisualStateManager.GetVisualStateGroups(root);

into

var groups = VisualStateManager.GetVisualStateGroups(root);

on both occasions. One file done. I won’t even pretend I understand what all those methods in this file are actually doing. I just ported them.

Porting FrameworkElementExtensions

I then created a library Win8nl, added references to Win8nl.External and WinRtBehaviors, and started on the FrameworkElementExtensions . This proved to be a pretty trivial matter. I needed to remove

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

And add after Control-dotting trough the errors I found I had added

using Windows.UI.Xaml.Media;
using Windows.UI.Xaml;
using Windows.Foundation
using Windows.UI.Xaml.Controls

Two files done!

Porting StoryboardExtensions

Routine starts to settle in. Remove

using System.Windows.Media;
using System.Windows.Media.Animation;
Control-dot around, and you will see you've added
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Media.Animation;

But then we hit our first snag. Two methods use a parameter of type IEasingFunction, that does no longer exist. But that can be fixed, by changing it into EasingFunctionBase.

Then I found out that Storyboard.SetTargetProperty apparently no longer wants to have a PropertyPath – which can be made from a DependencyProperty object – but a string. So method

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);
}
Need to be changed to
 public static void AddAnimation(this Storyboard storyboard,
 DependencyObject item, Timeline t, string property)
{
  if (string.IsNullOrWhiteSpace(property)) throw new ArgumentNullException("property");
  Storyboard.SetTarget(t, item);
  Storyboard.SetTargetProperty(t, property);
  storyboard.Children.Add(t);
}
This is bad news, since it breaks the public interface. And it breaks even more, namely another public extension method
public static void AddTranslationAnimation(this Storyboard storyboard,
   FrameworkElement fe, Point from, Point to, Duration duration,
   EasingFunctionBase 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);
}
Needs to be changed to
public static void AddTranslationAnimation(this Storyboard storyboard,
  FrameworkElement fe, Point from, Point to, Duration duration,
  EasingFunctionBase easingFunction)
{
  storyboard.AddAnimation(fe.RenderTransform,
  storyboard.CreateDoubleAnimation(duration, from.X, to.X, easingFunction),
                                   "TranslateX");
  storyboard.AddAnimation(fe.RenderTransform,
  storyboard.CreateDoubleAnimation(duration, from.Y, to.Y, easingFunction),
                                   "TranslateY");
}

I must honestly say I find the apparent need to specify storyboard target properties verbatim, as in strings, quite peculiar, but apparently this is the way it needs to be done. I am only the messenger here.

Porting DragFlickBehavior

Here we go again. Delete
using System.Windows.Interactivity;
using System.Windows.Media;
using System.Windows.Media.Animation;
using Wp7nl.Utilities;
And Control-dotting learns you the following needs to be added:
using Win8nl.Utilities;
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Media.Animation;
using WinRtBehaviors;
Soon after that, you'll learn that the second parameter of a ManipulationDelta event is no longer of type ManipulationDeltaEventArgs but of ManipulationDeltaRoutedEventArgs, and that it does no longer have a “DeltaManipulation” property but a plain “Delta” property. So the AssociatedObjectManipulationDelta method capturing the event was this:
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);
}
and now needs to be this
void AssociatedObjectManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e)
{
  var dx = e.Delta.Translation.X;
  var dy = e.Delta.Translation.Y;
  var currentPosition = elementToAnimate.GetTranslatePoint();
  elementToAnimate.SetTranslatePoint(currentPosition.X + dx, currentPosition.Y + dy);
}

No rocket science in there, right? And almost identical set of rework needs to be done to the method capturing ManipulationCompleted. Its second parameter was of type ManipulationCompletedEventArgs and is now – you’ve probably guessed it – ManipulationCompletedRoutedEventArgs. And that does no longer have a property e.FinalVelocities.LinearVelocity.X and Y but is does have a Velocities.Linear.X and Y.

For some reason though, those properties return values that are somewhere between 0 and 1, or at least it seems so. So I made a rule-of-thumb conversion multiplying them by 1000. Wrapping that up: AssociatedObjectManipulationCompleted used to be

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();
}
and it now is
private void AssociatedObjectManipulationCompleted(object sender,
                                                   ManipulationCompletedRoutedEventArgs e)
{
  // Create a storyboard that will emulate a 'flick'
  var currentPosition = elementToAnimate.GetTranslatePoint();
  var xVelocity = e.Velocities.Linear.X * 1000;
  var yVelocity = e.Velocities.Linear.Y * 1000;
  var storyboard = new Storyboard { FillBehavior = FillBehavior.HoldEnd };
  var to = new Point(currentPosition.X + (xVelocity / BrakeSpeed),
                     currentPosition.Y + (yVelocity / BrakeSpeed));
  storyboard.AddTranslationAnimation(elementToAnimate, currentPosition, to,
      new Duration(TimeSpan.FromMilliseconds(500)),
   new CubicEase { EasingMode = EasingMode.EaseOut });
  storyboard.Begin();
}

The final thing you will need to take into consideration when you port behaviors is the fact that you used to have an OnAttached and Loaded event. You still have those, but by the very nature I implemented behaviors everything that happened in OnAttached and OnLoaded needs to be in OnAttached. Same goes for Unloaded and OnDetaching – the last one is fired by the first one. So follow the pattern I set out: initialize in OnAttached only, and only clean up in OnDetached.

So, the behavior used to have a setup like this:

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);
  }
}
And that should now be 
protected override void OnAttached()
{
  elementToAnimate = AssociatedObject.GetElementToAnimate();
  if (!(elementToAnimate.RenderTransform is CompositeTransform))
  {
    elementToAnimate.RenderTransform = new CompositeTransform();
    elementToAnimate.RenderTransformOrigin = new Point(0.5, 0.5);
  }
  AssociatedObject.ManipulationDelta += AssociatedObjectManipulationDelta;
  AssociatedObject.ManipulationCompleted += AssociatedObjectManipulationCompleted;
  AssociatedObject.ManipulationMode = 
    ManipulationModes.TranslateX | ManipulationModes.TranslateY;
  base.OnAttached();
}
Notice a couple of interesting things:
  • The capture of “Loaded” is gone. We don’t need that any longer
  • There is an extra last line, setting the “ManipulationMode”. Apparently you need to set that up to make ManipulationDelta and ManipulationCompleted happen at all. It accidently stumbled upon that

Finally, the last part: OnDetaching. It used to be

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

  base.OnDetaching();
}
And the only thing that needs to be changed to use that is the removal of the first line: AssociatedObject.Loaded -= AssociatedObjectLoaded;;

And then we’re done. If you add this behavior to any object on the screen, like I showed in the previous post:

<Page
  x:Class="Catchit8.BlankPage"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:local="using:Catchit8"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  xmlns:Win8nl_Behaviors="using:Win8nl.Behaviors"
  xmlns:WinRtBehaviors="using:WinRtBehaviors"
  mc:Ignorable="d">

  <Grid Background="{StaticResource ApplicationPageBackgroundBrush}">
    <TextBlock HorizontalAlignment="Left" Margin="503,213,0,0" TextWrapping="Wrap" 
   VerticalAlignment="Top" FontSize="18" Text="Drag me">
      <WinRtBehaviors:Interaction.Behaviors>
         <Win8nl_Behaviors:DragFlickBehavior BrakeSpeed ="5"/>
      </WinRtBehaviors:Interaction.Behaviors>
    </TextBlock>
    <Button Content="Drag me too!" HorizontalAlignment="Left" Margin="315,269,0,0" 
   VerticalAlignment="Top" >
      <WinRtBehaviors:Interaction.Behaviors>
          <Win8nl_Behaviors:DragFlickBehavior BrakeSpeed ="5"/>
      </WinRtBehaviors:Interaction.Behaviors>
     </Button>

  </Grid>
</Page>

You will get an effect like this (I added a slider just for kicks)

DragFlickBehavior on Windows 8 demonstrated

Conclusion

At first glance, Windows 8 development does not seem to differ that much from Windows Phone development. After I made my behavior framework, porting a fairly complex behavior like this was pretty easy, so I’d say that holds true at second glance as well. Sure, some things are different – mostly namespaces and some property names. The XAML is a wee bit different as well. As to why Microsoft have decided to change namespaces, rename properties or methods or even let return values be a bit different – I don’t know. What I do know is that bitching about it will probably raise your blood pressure but it won’t help you very much as a developer. Just think of this: Microsoft sold 450 million copies of Windows 7. I don’t think those will all be Windows 8 next year, but I think the 100 million mark will be hit pretty soon. The choice is yours – either you are spending time and energy on getting angry that Microsoft moved your cheese (or actually, only some of it) or you can go out and find new and probably a bloody lot of cheese.

Well, I’ve made my choice

As usual, a complete demo solution for those who, like me, are too lazy to do all the typing themselves, can be found here. So you can get started even faster. ¡Arriba! ¡Andale! ;-)

9 comments:

Arnoud said...

Great work! One issue i'm having with the DragFlickBehavior is that it won't work when the control is in a scaled container (e.g viewbox or win8 split screen). It still works with the screen delta's.

Joost van Schaik said...

@Arnoud confirmed. As you may have read this comes from Windows Phone where there is no such thing as a Viewbox ;-) I really have no quick solution for this - if *you* find one, please let me know

Anonymous said...

Joost,
I am trying to use your DragFlickBehavior to easeout my element back to it's origin. I got it working by changing 'to' like this: var to = new Point(0, 0);

It works with one glitch. I am not able to drag the item when it's already easing out. Any suggestions?

Your work is incredible and what I have been trying to do for while but limited success.
Thank you so much for putting this together.

Joost van Schaik said...

@oms well thank you for the praise, although I like to point out that for the base library I got quite some help from 3 heavyweights that have been doing XAML a lot longer than I did.

Now to your problem - I am not sure what you are trying to do. Are you trying to get some rubber banding effect? Please elaborate a little more, or just mail me.

Anonymous said...

Joost,
Yes, rubber banding effect. Once I release the object, it should go back to it's original position but with exponential Ease.
As I said it works with an issue that if start dragging the object when it is returning back to it's original position it wouldn't drag until it reaches back to it's original place.
e.g. of what I am trying to do: http://www.youtube.com/watch?v=OQuFQ6GVSIQ&feature=player_embedded

Joost van Schaik said...

@oms I suppose you start some kind of storyboard to get 'move back to original position'? That storyboard has to be stopped as soon as you detect another touch, I suppose. I have done something like that on my game Catch'em birds - the birds move freely controlled by a storyboard, but stop as soon as you touch them.

Anonymous said...

Joost,
Yes, I used the storyboard you have in the ManipulationCompleted event and modified as per my need. I will look into making that storyboard global and see if I can stop into pointerpressed or other events.
When I was trying the same before I find your DragFlickBehavior, I had the same issue and even stopping my storyboard wasn't working.
Will update my findings here.
Thanks.

Anonymous said...

Joost,
I was able to figure that out. I was trying to Stop the animation and that was the problem.
1: I first made the storyboard as global variable accessible within DragFlicker.
2: Pause the Sb on animateObject's PointerPressed
3: Resume the Sb on animateObject's PointerReleased.

It looks to be working fine in a stand alone sample. Time to bring it into my main code.

Thanks for your help.

Joost van Schaik said...

@oms you are welcome. If you have created something nice and generic I'd be happy to add it to the #win8nl library ;-)