Having fancy animations in WPF is quite nice so I tried to implement a generic fading animation for UIElements
which I use for the Popups in my application. I was wondering if there was anything wrong with it, and thought about posting it here yesterday. There were indeed many things wrong at that time and I have improved the code greatly since then (I think) but there probably are still some issues with it now.
One of the first flaws I came across was that by temporarily changing the RenderTransform
the original can get messed up if the same animation is started while another one is still playing because the Completed
event of he first would never be called and the original RenderTransform
is merged with a half-animated transform that comes from my disrupted animation.
To prevent this I now have a HashSet
which is checked for the UIElement
in question and no animation will be started if another one is still in progress, not sure if that is the best approach here.
namespace HB.Animation
{
public enum Direction { Up, Down, Left, Right }
public enum AnimationType { In, Out }
public abstract class UIElementAnimationBase
{
private static readonly HashSet<UIElement> _animatedElements = new HashSet<UIElement>();
private AnimationType _type = AnimationType.In;
/// <summary>
/// Gets or sets the type of the animation. Default is In.
/// </summary>
public AnimationType Type
{
get { return _type; }
set { _type = value; }
}
private UIElement _target = null;
/// <summary>
/// Gets or sets the target of the animation.
/// </summary>
public UIElement Target
{
get { return _target; }
set { _target = value; }
}
private TimeSpan _duration = TimeSpan.FromSeconds(0.3);
/// <summary>
/// Gets or sets the duration of the animation. Default is 0.3 seconds.
/// </summary>
public TimeSpan Duration
{
get { return _duration; }
set { _duration = value; }
}
public event EventHandler Completed;
protected void OnCompleted()
{
_animatedElements.Remove(Target);
if (Completed != null)
{
Completed(this, null);
}
}
public void BeginAnimation()
{
if (_animatedElements.Contains(Target))
{
return;
}
else
{
_animatedElements.Add(Target);
}
BeginAnimationDetail();
}
protected abstract void BeginAnimationDetail();
}
public class FadeAnimation : UIElementAnimationBase
{
private Direction _direction = Direction.Down;
/// <summary>
/// Gets or sets the direction of the animation. Default is Down.
/// </summary>
public Direction Direction
{
get { return _direction; }
set { _direction = value; }
}
private double _distance = 50;
/// <summary>
/// Gets or sets the distance of the target travels in the animation. Default is 50.
/// </summary>
public double Distance
{
get { return _distance; }
set { _distance = value; }
}
private FillBehavior _opacityBehavior = FillBehavior.HoldEnd;
/// <summary>
/// Gets or sets the fill behavior of the opacity sub-animation. Default is HoldEnd.
/// </summary>
public FillBehavior OpacityBehavior
{
get { return _opacityBehavior; }
set { _opacityBehavior = value; }
}
public FadeAnimation(UIElement target)
{
Target = target;
}
public FadeAnimation(UIElement target, AnimationType type, Direction direction)
: this(target)
{
Type = type;
Direction = direction;
}
protected override void BeginAnimationDetail()
{
Transform tempTrans = Target.RenderTransform;
TranslateTransform trans;
EasingMode easing;
double opacityTarget;
double translateOrigin;
double translateTarget;
DependencyProperty translateProperty;
switch (Type)
{
case AnimationType.In:
easing = EasingMode.EaseOut;
opacityTarget = 1;
translateTarget = 0;
switch (Direction)
{
case Direction.Up:
trans = new TranslateTransform(0, -Distance);
translateProperty = TranslateTransform.YProperty;
break;
case Direction.Down:
trans = new TranslateTransform(0, Distance);
translateProperty = TranslateTransform.YProperty;
break;
case Direction.Left:
trans = new TranslateTransform(-Distance, 0);
translateProperty = TranslateTransform.XProperty;
break;
case Direction.Right:
trans = new TranslateTransform(Distance, 0);
translateProperty = TranslateTransform.XProperty;
break;
default:
throw new InvalidOperationException();
}
break;
case AnimationType.Out:
easing = EasingMode.EaseIn;
opacityTarget = 0;
trans = new TranslateTransform(0, 0);
switch (Direction)
{
case Direction.Up:
translateTarget = -Distance;
translateProperty = TranslateTransform.YProperty;
break;
case Direction.Down:
translateTarget = Distance;
translateProperty = TranslateTransform.YProperty;
break;
case Direction.Left:
translateTarget = -Distance;
translateProperty = TranslateTransform.XProperty;
break;
case Direction.Right:
translateTarget = Distance;
translateProperty = TranslateTransform.XProperty;
break;
default:
throw new InvalidOperationException();
}
break;
default:
throw new InvalidOperationException();
}
TransformGroup group = new TransformGroup();
if (tempTrans != null) group.Children.Add(tempTrans);
group.Children.Add(trans);
Target.RenderTransform = group;
DoubleAnimation animTranslate = new DoubleAnimation(translateTarget, (Duration)Duration);
animTranslate.EasingFunction = new CubicEase() { EasingMode = easing };
DoubleAnimation animFade = new DoubleAnimation(opacityTarget, (Duration)Duration) { FillBehavior = OpacityBehavior };
animTranslate.Completed += delegate
{
Target.RenderTransform = tempTrans;
OnCompleted();
};
Target.BeginAnimation(UIElement.OpacityProperty, animFade);
trans.BeginAnimation(translateProperty, animTranslate);
}
}
}
Here is a usage example:
private void DisplayMode_QuickSelect_Executed(object sender, ExecutedRoutedEventArgs e)
{
Popup popup = FindResource("DisplayModePopup") as Popup;
FadeAnimation anim = new FadeAnimation(popup.Child) { Duration = Settings.BalloonAnimationDuration };
if (popup.IsOpen)
{
anim.Type = HB.Animation.AnimationType.Out;
anim.Completed += delegate { popup.IsOpen = false; };
}
else
{
popup.Child.Opacity = 0;
popup.IsOpen = true;
}
anim.BeginAnimation();
}