Click here to Skip to main content
Click here to Skip to main content

WPF: Custom Generic Animations

, 8 Feb 2012 CPOL
Rate this:
Please Sign up or sign in to vote.
The basis for the fast custom animation creation
CustomAnimations_src

Introduction

This article was conceived as an answer to my old question to Microsoft: “Why are there so many duplications in .NET code and why not make it Generic based?” If you’ll take, for example, class DoubleAnimation and replace all instances of the word “Double” to the word “Color”, you’ll receive ColorAnimation class exactly. You’ll receive the same for most animations. Some very specific classes can be slightly different but most of the code is duplicated.

The other reason for this article (and a motivator for me) is the comments for another article on this site “WPF Tutorial - Part 2: Writing a custom animation class”. The goal of that article is to explain “how to” rather than to give a fully functional solution. So I decided to do that here.

Why Do We Need It?

Most standard types are covered by existing Animation classes, but sometimes we encounter a non-standard property, which in theory can be animated in the same or similar way, but it requires a custom class for animation. To maximally simplify this dirty work, I built some generic classes that allow making animations similar to DoubleAnimation for any relevant type. In my sample, I added animations for GridLength, CornerRadius, LinearGradientBrush and RadialGradientBrush. The GridLength animation animates Grid row and column size. For example, you can use star based units to animate from {*} to {3.5*}. The CornerRadius animation can be used with the property of the same name of the class Border. The idea (and samples) of the LinearGradientBrush animation was found in another article. It was implemented on my code base and supplemented by it sibling RadialGradientBrush animation. These animations can be used to animate whole brush instead of colors or individual properties.

Supported Animations

There are three types of animations that exist in .NET:

  • Transitions (*Animation classes)
  • Key-frame animations (*AnimationUsingKeyFrames classes)
  • Path animations (*AnimationUsingPath classes)

My classes covered the first and second types. The third type is very specific for different value types.

Implementation Troubleshoots

Nullable Problem

Implementation of the Animation<…> class required usage of Nullable for ValueType. On the other hand, I wanted to do the same implementation for reference and value types. I didn’t find a built in way to implement it for the generic classes. So I added the generic parameter TNullableValue and introduced the common class NullableHelper, which allows manipulation of value and corresponding nullable value the same way for reference and value types. To hide nullable problems and simplify inheritance and usage, I wrapped the Animation class with two inheritors for reference and value types: ValueTypeAnimation and RefTypeAnimation. They receive the value type and generate appropriated nullable type inside.

Generics Problem

Implementation of the AnimationUsingKeyFrame<…> class made plain the intolerance of WPF and especially Expression Blend 4 to generic types. The problem revealed itself when I used generic key frame collection as the content property of the animation class. Visual Studio just underlined key frames as warnings and Expression Blend refused compilation and presentation of the view and even crashed on compilation. The solution was simple, but a bit awkward. I introduced an additional parameter for AnimationUsingKeyFrame<…> class – TKeyFrameCollection. Now you need to inherit from any collection type which is derived from Freesable and implements IList, IList< KeyFrame<TValue>>. It can be FreezableCollection as in my samples or you can use your own (this is a little advantage). This non-generic type needs to be specified as the last parameter of the animation class.

How to Use?

To create your own animations, you need to do the following steps:

  • Implement interface IAnimationHelper<TValue> for your type. This is simple, although the most complicated part of the work.

    If you want to use your animations in XAML, you need to inherit from some generic classes:

  • For transitions:
    • ValueTypeAnimation<TValue, TAnimationHelper> or RefTypeAnimation<TValue, TAnimationHelper>
  • For key-frame animations
    • AnimationUsingKeyFrames<TValue, TAnimationHelper, TKeyFrameCollection> see Generics problem section to understand implementation of TKeyFrameCollection
    • DiscreteKeyFrame<…>, EasingKeyFrame<…>, LinearKeyFrame<…>, SplineKeyFrame<…> for use with key-frame animations
    • If you want to invent a new type of the key frame - derive it from KeyFrame<…>. You can make it generic to use with other types.
  • If you want to use them in code only - you can use generic versions directly.

IAnimationHelper

This interface is the most valuable part to implement, because it defines the behavior of the animation and defines correct calculations for your type. It assumes all values are non-null. NullableHelper solves Nullable problems out of this interface.

It declares the following functions:

  • IsValidValue – verifies value for validity (e.g. GridLengs is invalid if it has Auto type; Double cannot be NaN or infinity, etc.)
  • GetZeroValue – should return valid value, when adding to or subtracting from another value leaves that other value unchanged
  • AddValues – should return sum of two values
  • SubtractValue – should subtract values
  • ScaleValue – should multiply value by factor
  • InterpolateValue – should interpolate value according to scale and progress
  • GetSegmentLength – determines the length of segment between key frames if key frame time type defined as KeyTimeType.Paced. If segment length cannot be calculated, returns 1.0 for different values and 0.0 for same values.
  • IsAccumulable – determines validity of usage of IsAdditive and IsCumulative properties of the animation (e.g. String is not accumulable, but Double is.)

Implementation Example

I'll show here implementation for GridLength.

First of all implementing IAnimationHelper

    public sealed class GridLengthAnimationHelper : IAnimationHelper<GridLength>
    {
        #region IAnimationHelper

        public bool IsValidValue(GridLength value) { return !value.IsAuto; }

        public GridLength GetZeroValue() { return new GridLength(0); }

        public GridLength AddValues(GridLength value1, GridLength value2)
        {
            var targetType = VerifyCompatibility(value1, value2);
            return new GridLength(value1.Value + value2.Value, targetType);
        }

        public GridLength SubtractValue(GridLength value1, GridLength value2)
        {
            var targetType = VerifyCompatibility(value1, value2);
            return new GridLength(value1.Value - value2.Value, targetType);
        }

        public GridLength ScaleValue(GridLength value, double factor)
        {
            if (value.IsAuto)
                throw new InvalidOperationException("Cannot animate GridLengs with Auto type");
            return new GridLength(value.Value * factor, value.GridUnitType);
        }

        public GridLength InterpolateValue(GridLength from, GridLength to, double progress)
        {
            var targetType = VerifyCompatibility(from, to);
            return new GridLength(from.Value + ((to.Value - from.Value) * progress), targetType);
        }

        public double GetSegmentLength(GridLength from, GridLength to)
        {
            VerifyCompatibility(from, to);
            return Math.Abs(to.Value - from.Value);
        }

        bool IAnimationHelper<GridLength>.IsAccumulable { get { return true; } }

        #endregion IAnimationHelper

        private static GridUnitType VerifyCompatibility(GridLength value1, GridLength value2)
        {
            if (value2.Value.CompareTo(0.0) == 0)
                return value1.GridUnitType;
            if (value1.Value.CompareTo(0.0) == 0)
                return value2.GridUnitType;
            if (value1.GridUnitType != value2.GridUnitType)
                throw new InvalidOperationException("Using of different GridLengs types");
            return value1.GridUnitType;
        }
    }

Now can be created animations:

    public class GridLengthAnimation : ValueTypeAnimation<GridLength, GridLengthAnimationHelper>
    {
        #region Freezable

        public new GridLengthAnimation Clone() { return (GridLengthAnimation)base.Clone(); }

        protected override Freezable CreateInstanceCore() { return new GridLengthAnimation(); }

        #endregion
    }

    public class GridLengthKeyFrameCollection : FreezableCollection<KeyFrame<GridLength>>
    {
        #region Freezable

        protected override Freezable CreateInstanceCore() { return new GridLengthKeyFrameCollection(); }

        #endregion
    }

    public class GridLengthAnimationUsingKeyFrames : AnimationUsingKeyFrames<GridLength, GridLengthAnimationHelper, GridLengthKeyFrameCollection>
    {
        #region Freezable

        public new GridLengthAnimationUsingKeyFrames Clone() { return (GridLengthAnimationUsingKeyFrames)base.Clone(); }

        protected override Freezable CreateInstanceCore() { return new GridLengthAnimationUsingKeyFrames(); }

        #endregion
    }

    public class DiscreteGridLengthKeyFrame : DiscreteKeyFrame<GridLength> {}

    public class EasingGridLengthKeyFrame : EasingKeyFrame<GridLength, GridLengthAnimationHelper> {}

    public class LinearGridLengthKeyFrame : LinearKeyFrame<GridLength, GridLengthAnimationHelper> {}

    public class SplineGridLengthKeyFrame : SplineKeyFrame<GridLength, GridLengthAnimationHelper> {}

Usage Example

    <Storyboard>
        <Animations:GridLengthAnimation
          Storyboard.TargetName="GridColumn1"
          Storyboard.TargetProperty="Width"
          To="2*" Duration="0:0:0.2" />

        <Animations:GridLengthAnimationUsingKeyFrames 
          Storyboard.TargetName="GridColumn2"
          Storyboard.TargetProperty="Width" >
            <Animations:LinearGridLengthKeyFrame KeyTime="0" Value="*" />
            <Animations:EasingGridLengthKeyFrame KeyTime="0:0:0.2" Value="2*" >
                <Animations:EasingGridLengthKeyFrame.EasingFunction>
                    <BackEase EasingMode="EaseIn" />
                </Animations:EasingGridLengthKeyFrame.EasingFunction>
            </Animations:EasingGridLengthKeyFrame>
        </Animations:GridLengthAnimationUsingKeyFrames>
    </Storyboard>

Other Classes Used in this Project

Here I want to show some classes and interfaces used internally in the project. Maybe it will help in other projects.

NullableHelper

This helper solves the problem of usage reference and value types when you want to use Nullable<> wrapper for value type in generics.

  • IsNotNull and IsNull – verifies nullable value for null
  • Cast<T> – casts nullable value to value and back. User responsible to specify correct target type
  • AreTypesCompatible – verifies that specified types are compatible and will be processed correctly by this helper
  • IsNullable – verifies if specified type is nullable
  • Some DEBUG only functions that verify conditions and throw exceptions

SingletonOf<T>

This generic class creates application-wide singleton of a specified type. The type should be public and have a public default (parameterless) constructor.

  • Instance – property returns the single instance of the type
    public static class SingletonOf<T> where T : class, new()
    {
        private static readonly T _instance = new T();

        public static T Instance { get { return _instance; } }
    }

Note: It doesn't prevent creation of another instance of the class in a different way, but guarantees that the Instance property always returns the one instance.

History

  • 1.1 - Added LinearGradientBrush and RadialGradientBrush animations.
  • 1.0 - Initial version.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Yury Goltsman
Software Developer (Senior)
Israel Israel
Yury is Software Engineer since 1988.
His programming experience includes C#/VB.NET, WPF, C/C++(MFC/STL), Borland Delphi & C++ (VCL), JavaScript, HTML, CSS, XML, SQL, VB6, DirectX, Flash.
He has worked on PCs (DOS/Win3.1-Vista) and PocketPCs (WinCE).
 
Yury was born in Ukraine, but currently based in Jerusalem.

Comments and Discussions

 
GeneralMy vote of 5 Pinmemberjuergen196928-Feb-12 3:38 
QuestionI want more info. PinmvpPaulo Zemek8-Feb-12 3:16 
AnswerRe: I want more info. PinmemberYury Goltsman8-Feb-12 6:35 
GeneralMy vote of 3 Pinmembermariazingzing7-Feb-12 11:47 
GeneralRe: My vote of 3 PinmemberYury Goltsman7-Feb-12 18:22 
GeneralRe: My vote of 3 PinmemberGeorge Danila7-Feb-12 22:48 
GeneralRe: My vote of 3 PinmemberYury Goltsman8-Feb-12 0:04 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.141015.1 | Last Updated 8 Feb 2012
Article Copyright 2012 by Yury Goltsman
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid