Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Control Animation in Winforms

4.95/5 (84 votes)
2 Dec 2014CPOL6 min read 125.1K   7K  
Winforms animation

Introduction

VisualEffects library allows Winforms controls animation. It provides many out-of-the-box effects including control's size, location, color and opacity effects and supports easing functions. You can easily create new effects implementing IEffect interface and provide new easing functions. Effects and easing functions are independent and can be combined as you wish. Multiple effects can be combined to work together seamlessly. A few examples:

Left anchored width manipulation effect with bounce easing:

Image 1

Linear fade effect:

Image 2

Linear color shift effect:

Image 3

Previous effects combined:

Image 4

Background

To understand how VisualEffects works, I need to define what animators, effects, easing functions, animations and transitions are.

ANIMATORS

Animators are the engines that make animations work. At the very core, there's a timer and a stopwatch. At every tick, a new value is calculated using a provided easing function and the actual elapsed time, then control properties are manipulated by means of effects. The timer is stopped when elapsed time reaches the desired duration.

EASING FUNCTIONS

Easing functions are mathematical functions that are used to interpolate values between two endpoints usually with non-linear results. They determine the way something changes; or better, they determine how effects are applied.

Given an expected effect duration and the amount of time elapsed since an animator started to play the effect, an easing function calculates the correct value the control has to assume at a given point in time. At T(0) value is the initial value, at T(Max) value is the target value we want to reach.

The easing functions I'm using are taken from http://gizma.com/easing/. In my library, an easing function is a delegate function defined as follows:

C#
public delegate double EasingDelegate( double currentTime, 
    double minValue, double maxValue, double duration );

If this is the first time you hear about easing, you probably have always been animating things linearly. Linear easing is a valid easing function but it's not credible since objects in real life don't just start and stop instantly, and almost never change at a constant speed. Easing functions (usually) act in a more natural and credible way.

This example shows how linear easing is implemented:

C#
public static double Linear( double currentTime, double minHeight, 
    double maxHeight, double duration )
{
    return maxHeight * currentTime / duration + minHeight;
}

EFFECTS

Effects are the means by which control properties are manipulated. In my library, an effect is a class that implements IEffect interface, which is defined as follows:

C#
public interface IEffect
{
    EffectInteractions Interaction { get; }

    int GetCurrentValue( Control control );
    void SetValue( Control control, int originalValue, int valueToReach, int newValue );

    int GetMinimumValue( Control control );
    int GetMaximumValue( Control control );
}
EXAMPLE 1

A simple, perfectly working example of how to implement IEffect to create an effect that manipulates control's Height property is the following:

C#
public class TopAnchoredHeightManipulationlEffect : IEffect
{
    public EffectInteractions Interaction
    {
        get { return EffectInteractions.HEIGHT; }
    }

    public int GetCurrentValue( Control control )
    {
        return control.Height;
    }

    public void SetValue( Control control, int originalValue, int valueToReach, int newValue )
    {
        control.Height = newValue;
    }

    public int GetMinimumValue( Control control )
    {
        return control.MinimumSize.IsEmpty ? Int32.MinValue
            : control.MinimumSize.Height;
    }

    public int GetMaximumValue( Control control )
    {
        return control.MaximumSize.IsEmpty ? Int32.MaxValue
            : control.MaximumSize.Height;
    }
}

The effect above works with Height property only. It gets current control's Height property in GetCurrentValue and sets a new value for Height in SetValue. As a result, a control will resize from the bottom. Here, you can see it in action with Linear easing:

Image 5

EXAMPLE 2

Quote: practical tips for boosting the performance of windows forms apps

Several properties dictate the size or location of a control: Width, Height, Top, Bottom, Left, Right, Size, Location, and Bounds. Setting Width and then Height causes twice the work of setting them both together via Size.

An effect working on a single property is general purpose but it is not optimized to work together with other effects. If you apply many effects together, you can get flickering or non-smooth animations. Flickering may be noticed or not depending on how you mix effects and how effects behave.

That being so, sometimes, it's better to write a custom, more specialized effect in order to optimize painting. Let's say we want our control to stay anchored on bottom and resize its height on top; we could combine two effects, one working with Height property and one with Top property. This approach works, but it's not the best we can do. To achieve our goal efficiently, we have to work on Height and Top properties at the same time:

C#
public class BottomAnchoredHeightManipulationEffect : IEffect
{
    public int GetCurrentValue( Control control )
    {
        return control.Height;
    }

    public void SetValue( Control control, int originalValue, int valueToReach, int newValue )
    {
        //changing location and size independently can cause flickering:
        //change bounds property instead.

        var size = new Size( control.Width, newValue );
        var location = new Point( control.Left, control.Top +
            ( control.Height - newValue ) );

        control.Bounds = new Rectangle( location, size );
    }

    public int GetMinimumValue( Control control )
    {
        if( control.MinimumSize.IsEmpty )
            return Int32.MinValue;

        return control.MinimumSize.Height;
    }

    public int GetMaximumValue( Control control )
    {
        if( control.MaximumSize.IsEmpty )
            return Int32.MaxValue;

        return control.MaximumSize.Height;
    }

    public EffectInteractions Interaction
    {
        get { return EffectInteractions.BOUNDS; }
    }
}

This is how it looks like with Linear easing:

Image 6

EXAMPLE 3

Now, I will show a more complex example where I try to create a color shifting effect. Since we have to work on 4 channels (A,R,G,B), it's more convenient to create an effect that manages them all together:

C#
public class ColorShiftEffect : IEffect
{
     public EffectInteractions Interaction
     {
         get { return EffectInteractions.COLOR; }
     }

     public int GetCurrentValue( Control control )
     {
         return control.BackColor.ToArgb();
     }

     public void SetValue( Control control, int originalValue, int valueToReach, int newValue )
     {
         int actualValueChange = Math.Abs( originalValue - valueToReach );
         int currentValue = this.GetCurrentValue( control );

         double absoluteChangePerc =
             ( (double)( ( originalValue - newValue ) * 100 ) ) / actualValueChange;
         absoluteChangePerc = Math.Abs( absoluteChangePerc );

         if( absoluteChangePerc > 100.0f )
             return;

         Color originalColor = Color.FromArgb( originalValue );
         Color newColor = Color.FromArgb( valueToReach );

         int newA = (int)Interpolate( originalColor.A, newColor.A, absoluteChangePerc );
         int newR = (int)Interpolate( originalColor.R, newColor.R, absoluteChangePerc );
         int newG = (int)Interpolate( originalColor.G, newColor.G, absoluteChangePerc );
         int newB = (int)Interpolate( originalColor.B, newColor.B, absoluteChangePerc );

         control.BackColor = Color.FromArgb( newA, newR, newG, newB );
     }

     public int GetMinimumValue( Control control )
     {
         return Color.Black.ToArgb();
     }

     public int GetMaximumValue( Control control )
     {
         return Color.White.ToArgb();
     }

     private int Interpolate( int val1, int val2, double changePerc )
     {
         int difference = val2 - val1;
         int distance = (int)( difference * ( changePerc / 100 ) );
         int result = (int)( val1 + distance );

         return result;
     }
 }

The first thing to notice is that I had to find a way to transform Color to an integer to make it suitable for my interface. This is not required if you worked on each channel separately, since each channel is an integer. In this case, ToArgb() does the trick. After that in SetValue, we cannot just cast newValue back to Color and assign it to achieve a color shift effect; in facts our newValue is more likely representing a random color since the animator had no clue it was working on an integer representation of a color and thus did not take into account to change ARGB channels accordingly.

What we can do to fix that is to calculate the percentage variation of newValue compared to the originalValue and apply that variation on each of our original color channels.

Here's what we get (Linear easing):

Image 7

ANIMATIONS AND TRANSITIONS

An Animation consists of one or more effects working together on the same control. Transition define how different animations work on different controls, and thus how different animations interact.

There is no animation or transition abstraction in my library at the moment, but since they basically apply effects on controls, they can be easily written:

EXAMPLE

Most of the times, you want an animation to be able to expand and collapse, show and hide or in general perform an effect and its opposite:

C#
public class FoldAnimation
{
    public Control Control { get; private set; }
    public Size MaxSize { get; set; }
    public Size MinSize { get; set; }
    public int Duration { get; set; }
    public int Delay { get; set; }

    public FoldAnimation(Control control)
    {
        this.Control = control;
        this.MaxSize = control.Size;
        this.MinSize = control.MinimumSize;
        this.Duration = 1000;
        this.Delay = 0;
    }

    public void Show()
    {
        this.Control.Animate(new HorizontalFoldEffect(),
             EasingFunctions.CircEaseIn, this.MaxSize.Height, this.Duration, this.Delay);

        this.Control.Animate(new VerticalFoldEffect(),
             EasingFunctions.CircEaseOut, this.MaxSize.Width, this.Duration, this.Delay);
    }

    public void Hide()
    {
        this.Control.Animate(new HorizontalFoldEffect(),
            EasingFunctions.CircEaseOut, this.MinSize.Height, this.Duration, this.Delay);

        this.Control.Animate(new VerticalFoldEffect(),
            EasingFunctions.CircEaseIn, this.MinSize.Width, this.Duration, this.Delay);
    }
}

The above implementation suffers two drawbacks: the first is that it does not provide a cancellation mechanism in case Hide() method is called before Show() method is done, or the other way round; the second is that effect duration is not adjusted so that if I call Hide() method in the middle of Show() animation, it will take half the desired duration time to perform.

Fortunately, Animate() returns an AnimationStatus object that can be used to fix those issues:

C#
   public class FoldAnimation
   {
       private List<StatelessAnimator.AnimationStatus> _cancellationTokens;

       public Control Control { get; private set; }
       public Size MaxSize { get; set; }
       public Size MinSize { get; set; }
       public int Duration { get; set; }
       public int Delay { get; set; }

       public FoldAnimation( Control control )
       {
           _cancellationTokens = new List<StatelessAnimator.AnimationStatus>();

           this.Control = control;
           this.MaxSize = control.Size;
           this.MinSize = control.MinimumSize;
           this.Duration = 1000;
           this.Delay = 0;
       }

       public void Show()
       {
           int duration = this.Duration;

           if( _cancellationTokens.Any( aS => !aS.IsCompleted ) )
           {
               //residue time
               var token = _cancellationTokens.First( aS => !aS.IsCompleted );
               duration = (int)( token.ElapsedMilliseconds );
           }

           this.CancelAllPerformingEffects();

           var cT1 = this.Control.Animate( new HorizontalFoldEffect(),
                EasingFunctions.CircEaseIn, this.MaxSize.Height, duration, this.Delay );

           var cT2 = this.Control.Animate( new VerticalFoldEffect(),
                EasingFunctions.CircEaseOut, this.MaxSize.Width, duration, this.Delay );

           _cancellationTokens.Add( cT1 );
           _cancellationTokens.Add( cT2 );
       }

       public void Hide()
       {
           int duration = this.Duration;

           if( _cancellationTokens.Any( aS => !aS.IsCompleted ) )
           {
               //residue time
               var token = _cancellationTokens.First( aS => !aS.IsCompleted );
               duration = (int)( token.ElapsedMilliseconds );
           }

           this.CancelAllPerformingEffects();

           var cT1 = this.Control.Animate( new HorizontalFoldEffect(),
               EasingFunctions.CircEaseOut, this.MinSize.Height, duration, this.Delay );

           var cT2 = this.Control.Animate( new VerticalFoldEffect(),
               EasingFunctions.CircEaseIn, this.MinSize.Width, duration, this.Delay );

           _cancellationTokens.Add( cT1 );
           _cancellationTokens.Add( cT2 );
       }

       public void Cancel()
       {
           this.CancelAllPerformingEffects();
       }

       private void CancelAllPerformingEffects()
       {
           foreach (var token in _cancellationTokens)
               token.CancellationToken.Cancel();

           _cancellationTokens.Clear();
      }
}

Using the Code

To apply an effect on your control, simply call Animator.Animate method, or Animate extension method on your control like this:

C#
yourControl.Animate
(
    new XLocationEffect(), //effect to apply implementing IEffect
    EasingFunctions.BounceEaseOut, //easing to apply
    321, //value to reach
    2000, //animation duration in milliseconds
    0 //delayed start in milliseconds
);

Update 09/12/2014

I was missing the ability to reverse an animation and to perform a defined number of loops if reverse is enabled. These features are available in version 1.2.

Animator.Animate method has two more optional parameters:

  • Reverse: If set to true, the animation will reach the target value and then, play back to the initial value. By default, this parameter is set to false.
  • Loops: If you set reverse to true, then you can define how many loops the animation has to perform. By default, this parameter is set to 1. If you set 0 or a negative number, the animation will loop forever. You can stop the animation by using CancellationToken as usual.

History

  • 09/12/2014: Animation reverse and loops number
  • 16/10/2014: Project is created

License

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