Click here to Skip to main content
14,162,626 members
Click here to Skip to main content
Add your own
alternative version

Tagged as

Stats

68.9K views
5K downloads
119 bookmarked
Posted 21 Oct 2014
Licenced CPOL

Control Animation in Winforms

, 2 Dec 2014
Rate this:
Please Sign up or sign in to vote.
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:

Linear fade effect:

Linear color shift effect:

Previous effects combined:

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:

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:

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:

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:

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:

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:

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:

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:

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):

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:

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:

   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:

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)

Share

About the Author

Mauro Sampietro
Software Developer
Italy Italy
No Biography provided

You may also be interested in...

Pro

Comments and Discussions

 
GeneralHow to use in VB.Net Pin
Member 1108200412hrs 26mins ago
memberMember 1108200412hrs 26mins ago 
QuestionThanks and mixing Pin
Member 141074374-Jan-19 7:30
memberMember 141074374-Jan-19 7:30 
Questionusing EffectInteractions.LOCATION Pin
Member 1388785026-Jun-18 8:27
memberMember 1388785026-Jun-18 8:27 
QuestionI cant use in vb.net Pin
Member 1179252529-May-18 4:54
memberMember 1179252529-May-18 4:54 
QuestionStopping the animation with CancellationToken Pin
teasdfaudfas17-Apr-18 2:45
memberteasdfaudfas17-Apr-18 2:45 
Questionhelp using code Pin
O.abdelaaziz3-Nov-17 0:06
memberO.abdelaaziz3-Nov-17 0:06 
AnswerRe: help using code Pin
Mauro Sampietro11-Dec-17 23:35
memberMauro Sampietro11-Dec-17 23:35 
GeneralRe: help using code Pin
O.abdelaaziz29-Dec-17 5:09
memberO.abdelaaziz29-Dec-17 5:09 
QuestionHow many effect to apply implementing IEffect? Pin
Member 1325143018-Jun-17 19:31
memberMember 1325143018-Jun-17 19:31 
AnswerRe: How many effect to apply implementing IEffect? Pin
Mauro Sampietro4-Dec-17 23:31
memberMauro Sampietro4-Dec-17 23:31 
QuestionHave you a sample to use your library ? Pin
Z@clarco26-Apr-17 4:34
memberZ@clarco26-Apr-17 4:34 
AnswerRe: Have you a sample to use your library ? Pin
Mauro Sampietro23-May-17 2:27
memberMauro Sampietro23-May-17 2:27 
QuestionBug and a question Pin
Russ Suter22-Jul-16 12:35
memberRuss Suter22-Jul-16 12:35 
AnswerRe: Bug and a question Pin
Mauro Sampietro24-Jul-16 22:53
memberMauro Sampietro24-Jul-16 22:53 
GeneralRe: Bug and a question Pin
Russ Suter25-Jul-16 4:57
memberRuss Suter25-Jul-16 4:57 
GeneralRe: Bug and a question Pin
Mauro Sampietro26-Jul-16 2:01
memberMauro Sampietro26-Jul-16 2:01 
GeneralRe: Bug and a question Pin
GuilhermeAlencar8-Aug-18 6:28
memberGuilhermeAlencar8-Aug-18 6:28 
QuestionGIT Repository Pin
slxSlashi13-Mar-16 23:06
memberslxSlashi13-Mar-16 23:06 
AnswerRe: GIT Repository Pin
Mauro Sampietro15-Mar-16 0:27
memberMauro Sampietro15-Mar-16 0:27 
PraiseRe: GIT Repository Pin
slxSlashi13-Jul-16 23:19
memberslxSlashi13-Jul-16 23:19 
GeneralRe: GIT Repository Pin
Mauro Sampietro18-Jul-16 5:24
memberMauro Sampietro18-Jul-16 5:24 
QuestionThank you for sharing Pin
almas19921-Jan-16 22:50
memberalmas19921-Jan-16 22:50 
AnswerRe: Thank you for sharing Pin
Mauro Sampietro11-Jan-16 5:13
memberMauro Sampietro11-Jan-16 5:13 
QuestionI found problem Pin
tornadofay17-Sep-15 14:42
professionaltornadofay17-Sep-15 14:42 
AnswerRe: I found problem Pin
Mauro Sampietro17-Sep-15 20:46
memberMauro Sampietro17-Sep-15 20:46 

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

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

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web02 | 2.8.190518.1 | Last Updated 2 Dec 2014
Article Copyright 2014 by Mauro Sampietro
Everything else Copyright © CodeProject, 1999-2019
Layout: fixed | fluid