4
\$\begingroup\$

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();
}
\$\endgroup\$

2 Answers 2

5
\$\begingroup\$

I would focus on BeginAnimationDetail method.

  • Variable names. I would rename at least these variables - tempTrans and trans. tempTrans name doesn't give me any idea what is this variable about. I would name it originalTransform for example - it will be enough to understand why do you add it to another transform and later resetting control's transform to this one. From tempTrans name it is unclear how will you use it.

  • In the beginning of that method you have several variables defined and then for ~60 lines (two switch statements) you're setting some values to those variables. Besides the fact that some part of code is repeated there, I find it very difficult to read when variables are assigned far from the line when they were defined. I would rewrite method in order to avoid it. Algorithm could be:

    • Calculate positions of in and out points. In = {0, 0}, Out depends on Direction and Distance.

    • Decide at which point animation will start and at which point it will finish.

    • Create animations and start them.

Result (it is around two times shorter):

   protected override void BeginAnimationDetail()
    {
        var inPoint = new Point(0, 0);
        Point outPoint;

        switch (Direction)
        {
            case Direction.Up:
                outPoint = new Point(0, -Distance);
                break;
            case Direction.Down:
                outPoint = new Point(0, Distance);
                break;
            case Direction.Left:
                outPoint = new Point(-Distance, 0);
                break;
            case Direction.Right:
                outPoint = new Point(Distance, 0);
                break;
            default:
                throw new InvalidOperationException();
        }

        Transform originalTransform = Target.RenderTransform;

        var easing = Type == AnimationType.In ? EasingMode.EaseOut : EasingMode.EaseIn;
        double opacityTarget = Type == AnimationType.In ? 1 : 0;
        Point from = Type == AnimationType.In ? outPoint : inPoint;
        Point to = Type == AnimationType.In ? inPoint : outPoint;

        var animatedTranslate = new TranslateTransform(from.X, from.Y);

        var group = new TransformGroup();
        if (originalTransform != null) group.Children.Add(originalTransform);
        group.Children.Add(animatedTranslate);
        Target.RenderTransform = group;

        var animFade = new DoubleAnimation(opacityTarget, Duration) {FillBehavior = OpacityBehavior};
        animFade.Completed += delegate
            {
                Target.RenderTransform = originalTransform;
                OnCompleted();
            };

        Target.BeginAnimation(UIElement.OpacityProperty, animFade);
        animatedTranslate.BeginAnimation(TranslateTransform.XProperty, new DoubleAnimation(to.X, Duration) {EasingFunction = new CubicEase {EasingMode = easing}});
        animatedTranslate.BeginAnimation(TranslateTransform.YProperty, new DoubleAnimation(to.Y, Duration) {EasingFunction = new CubicEase {EasingMode = easing}});
    }
\$\endgroup\$
1
  • \$\begingroup\$ Thank you for this great response! I suspected that there would be a better way to handle those switches and variable initializations, very intelligent how you did that; also interesting how somewhat doing "more" by considering both axes is actually a lot shorter. \$\endgroup\$
    – brunnerh
    Commented Apr 2, 2011 at 0:47
3
\$\begingroup\$

Nowadays using auto-implemented properties is the way to go.

/// <summary> 
/// Gets or sets the type of the animation. Default is In.
/// </summary>
public AnimationType Type { get; set; } = AnimationType.In;

looks neater than

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; }
}  

Using C# 6 (VS 2015) we can simplify the null check in the OnCompleted() method like so

protected void OnCompleted()
{
    _animatedElements.Remove(Target);

    Completed?.Invoke(this, null);
}  

If we take advantage of the result of the HashSet.Add() method we can simplify the BeginAnimation() method like so

public void BeginAnimation()
{
    if (_animatedElements.Add(Target))
    {
       BeginAnimationDetail();
    }
}  

but I would throw a NullReferenceException if Target == null so it will be thrown from that public method instead of the protected abstract void BeginAnimationDetail() method. IMO this is neccessary because the Target property is public hence it can by error be set to null.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.