Take the 2-minute tour ×
Code Review Stack Exchange is a question and answer site for peer programmer code reviews. It's 100% free, no registration required.

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 accross 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.

I would be glad about any feedback, if something is unclear i will try to explain it.

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();
}
share|improve this question
add comment

1 Answer 1

up vote 3 down vote accepted

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}});
    }
share|improve this answer
    
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. –  H.B. Apr 2 '11 at 0:47
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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