18 August 2013

A behavior to animate folding in and out of GUI elements on Windows Phone

imageLike I wrote in my earlier post about extension methods for animating scales and opacity – those were merely building building blocks for something bigger. Today, as part two of my opacity and scaling animation series, I present you a behavior that kind of automates all the stuff you had to do in code.

Say hello to UnfoldBehavior, another offspring of the game I have just submitted. All you basically have to do is drag it on top of an element you want to control, bind it’s “Activated” property to a boolean in your view model, and off you go. Activate == true means panel is unfolded (open), Activated == false means panel is closed. Of course, to make stuff more fun, in the demo app that goes with this post I have bound some more properties as well, so you can control how the panel folds and unfolds. How this works, you can see in this video below:

 

The UnfoldBehavior in action

The view model controlling the demo app is laughingly simple:

using System.Windows.Input;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using Wp7nl.Behaviors;

namespace UnfoldBehaviorDemo.Viewmodel
{
  public class DemoViewModel : ViewModelBase
  {
    private bool isUnFolded = true;
    public bool IsUnFolded
    {
      get { return isUnFolded; }
      set
      {
        if (isUnFolded != value)
        {
          isUnFolded = value;
          RaisePropertyChanged(() => IsUnFolded);
        }
      }
    }

    public ICommand FoldCommand
    {
      get
      {
        return new RelayCommand(
            () =>
            {
              IsUnFolded = !IsUnFolded;
            });
      }
    }

    private bool horizontalChecked = true;
    public bool HorizontalChecked
    {
      get { return horizontalChecked; }
      set
      {
        if (horizontalChecked != value)
        {
          horizontalChecked = value;
          RaisePropertyChanged(() => HorizontalChecked);
          RaisePropertyChanged(() => FoldDirection);
        }
      }
    }

    private bool verticalChecked;
    public bool VerticalChecked
    {
      get { return verticalChecked; }
      set
      {
        if (verticalChecked != value)
        {
          verticalChecked = value;
          RaisePropertyChanged(() => VerticalChecked);
          RaisePropertyChanged(() => FoldDirection);
        }
      }
    }

    public Direction FoldDirection
    {
      get
      {
        if (HorizontalChecked && VerticalChecked)
        {
          return Direction.Both;
        }

        if (VerticalChecked)
        {
          return Direction.Vertical;
        }

        return Direction.Horizontal;
      }
    }
  }
} 

A property to control the actual folding and unfolding, a command to toggle that value, two properties for the check boxes where you can choose the fold direction with and one property to bind the behavior to. So how does this work?

It’s actually much more simple than you would expect, as all the heavy lifting is done by the stuff that was put into my wp7nl library on codeplex. But to start at the very bottom, we need an enum to determine direction:

namespace Wp7nl.Behaviors
{
  public enum Direction
  {
    Horizontal,
    Vertical,
    Both
  }
}
and then I defined a base class for this type of behaviors - as there are lots of more fun things to be done with scaling and opacity than just folding and unfolding alone. This base class looks like this and is (of course) built again as a SafeBehavior :
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Animation;

namespace Wp7nl.Behaviors
{
  /// <summary>
  /// A base class for behaviors doing 'something' with scaling
  /// </summary>
  public abstract class BaseScaleBehavior : SafeBehavior<FrameworkElement>
  {
    protected override void OnSetup()
    {
      base.OnSetup();
      SetRenderTransForm();
      AssociatedObject.RenderTransform = BuildTransform();
    }

    private void Activate()
    {
      if (AssociatedObject != null && 
          AssociatedObject.RenderTransform is CompositeTransform)
      {
        var storyboard = BuildStoryBoard();
        if (storyboard != null)
        {
          storyboard.Begin();
        }
      }
    }

    private void SetRenderTransForm()
    {
      if (AssociatedObject != null)
      {
        AssociatedObject.RenderTransformOrigin = 
new Point(RenderTransformX, RenderTransformY); } } protected abstract CompositeTransform BuildTransform(); protected abstract Storyboard BuildStoryBoard(); // Direction Dependency Property (Direction) ommitted // Duration Property (double) ommitted // Activated Property (bool) ommitted /// <summary> /// Activated property changed callback. /// </summary> public static void ActivatedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var thisobj = d as BaseScaleBehavior; if (thisobj != null && e.OldValue != e.NewValue) { thisobj.Activate(); } } // RenderTransformX Property (bool) ommitted // RenderTransformY Property (bool) ommitted /// <summary> /// RenderTransform X/Y property changed callback. /// </summary> public static void RenderTransformChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var thisobj = d as BaseScaleBehavior; if (thisobj != null) { thisobj.SetRenderTransForm(); } } } }

What we have here are five dependency properties, for which I have omitted most of the code because it’s pretty boring repetitive stuff, save for some callback methods that actually do something. The properties are:

  • Direction – in which direction should the object be scaled. Horizontal, Vertical, or Both
  • Duration – the time in milliseconds the animated transition should take.
  • Activated – it’s up to the implementing behavior to what that means (in the case of UnfoldingBehavior, false = invisible, true = visible)
  • RenderTransformX – 0 to 1, the horizontal origin of the animation. See here for more explanation
  • RenderTransformY – 0 to 1, the vertical origin of the animation

We have two abstract methods that should be implemented in by the actual behavior:

  • BuildTransform should build a suitable CompositeTransform for use in the animation
  • BuildStoryBoard should build a suitable Storyboard to perform the actual animation.

All that’s left is the SetRenderTransForm, that sets the RenderTransformOrigin based upon RenderTransformX and RenderTransformY, and the Activate method that checks if the animation can be performed and if so, builds and executes the storyboard. Oh, and the OnSetup does the initial setup. Duh ;). So basically all child classes need to do is fill in those two abstract methods!

Which is exactly what UnfoldBehavior does:

using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Animation;
using Wp7nl.Utilities;

namespace Wp7nl.Behaviors
{
  /// <summary>
  /// Behavior that unfolds an object using an animation
  /// </summary>
  public class UnfoldBehavior : BaseScaleBehavior
  {
    protected override void OnSetup()
    {
      base.OnSetup();
      AssociatedObject.Opacity = GetOpacity();
      ListenToPageBackEvent = true;
    }

    protected override Storyboard BuildStoryBoard()
    {
      var transform = BuildTransform();
      if (AssociatedObject.GetOpacityProperty() != GetOpacity() ||
          transform.ScaleX != AssociatedObject.GetScaleXProperty() ||
          transform.ScaleY != AssociatedObject.GetScaleYProperty())
      {
        var storyboard = new Storyboard {FillBehavior = FillBehavior.HoldEnd};
        var duration = new Duration(TimeSpan.FromMilliseconds(Duration));
        storyboard.AddScalingAnimation(
          AssociatedObject,
          AssociatedObject.GetScaleXProperty(), transform.ScaleX,
          AssociatedObject.GetScaleYProperty(), transform.ScaleY,
          duration);
        storyboard.AddOpacityAnimation(AssociatedObject, 
                 AssociatedObject.GetOpacityProperty(), 
                 GetOpacity(), duration);
        return storyboard;
      }
      return null;
    }

    protected override CompositeTransform BuildTransform()
    {
      return new CompositeTransform
      {
        ScaleX = (Direction == Direction.Horizontal || Direction == Direction.Both) && 
!Activated ? 0 : 1, ScaleY = (Direction == Direction.Vertical || Direction == Direction.Both) &&
!Activated ? 0 : 1 }; } private double GetOpacity() { return Activated ? 1 : 0; } } }

OnSetup does the necessary extract setup, the BuildTransform is used in BaseScaleBehavior to set the initial state of the element based upon the initial property values, but in this child class also for something else – in BuildStoryBoard is it used to build a transformation base upon the desired new state if anything changes. In the same way works the GetOpacity method – it returns the desired opacity based upon the currrent state of the behavior’s properties. So then the behavior checks if the desired state is different from the AssociatedObject’s current state, and if so, it uses the extension methods described in the previous post to build the actual animation Storyboard.

And that’s (almost) all there’s to it. An attentive reader might have noticed, though, that there are no such things as methods to get the value of  current scaling and opacity of an UI element, which are after all dependency properties too. That is why I have created this little batch of extension methods as a shortcut:

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

namespace Wp7nl.Utilities
{
  /// <summary>
  /// Class to help animations from code using the CompositeTransform
  /// </summary>
  public static class FrameworkElementExtensions2
  {
    public static double GetDoubleTransformProperty(this FrameworkElement fe, 
                            DependencyProperty property)
    {
      var translate = fe.GetCompositeTransform();
      if (translate == null) throw new ArgumentNullException("CompositeTransform");
      return (double)translate.GetValue(property);
    }

    public static double GetScaleXProperty(this FrameworkElement fe)
    {
      return fe.GetDoubleTransformProperty(CompositeTransform.ScaleXProperty);
    }

    public static double GetScaleYProperty(this FrameworkElement fe)
    {
      return fe.GetDoubleTransformProperty(CompositeTransform.ScaleYProperty);
    }

    public static double GetOpacityProperty(this FrameworkElement fe)
    {
      return (double)fe.GetValue(UIElement.OpacityProperty);
    }
  }
}

Nothing special, just a way to extract the date a little easier.

So, with some drag, drop and binding actions in Blend you know have a reusable piece of code that can be easily applied to multiple user interface elements without have to duplicate storyboards and timelines again and again. With this behavior, Blend indeed is your friend ;-). Oh and by the way – this should work in both Windows Phone 7 and 8. For Windows 8 I don’t dare to give any guarantees – just have not tried it out yet.

Demo application of course available here, and I will soon add all this stuff to the wp7nl Nuget package. Stay tuned.

No comments: