WPF - sequential animation simple example

2019-01-24 14:58发布

问题:

I'm learning about WPF animation, and am confused about how to apply animations sequentially. As a simple example, I've got four rectangles in a uniform grid, and would like to change the color of each one sequentially. Here's what I have so far:

public partial class Window1 : Window
{
    Rectangle blueRect;
    Rectangle redRect;
    Rectangle greenRect;
    Rectangle yellowRect;

    public Window1()
    {
        InitializeComponent();
        blueRect = new Rectangle() { Fill = System.Windows.Media.Brushes.Blue, Name="Blue"};
        redRect = new Rectangle() { Fill = System.Windows.Media.Brushes.Red, Name="Yellow"};
        greenRect = new Rectangle() { Fill = System.Windows.Media.Brushes.Green, Name="Green" };
        yellowRect = new Rectangle() { Fill = System.Windows.Media.Brushes.Yellow, Name="Yellow" };

        UniformGrid1.Children.Add(blueRect);
        UniformGrid1.Children.Add(redRect);
        UniformGrid1.Children.Add(greenRect);
        UniformGrid1.Children.Add(yellowRect);

    }

    private void Window_Loaded(object sender, RoutedEventArgs e)
    {
        animateCell(blueRect, Colors.Blue);
        animateCell(redRect, Colors.Red);
    }

    private void animateCell(Rectangle rectangle, Color fromColor)
    {
        Color toColor = Colors.White;
        ColorAnimation ani = new ColorAnimation(toColor, new Duration(TimeSpan.FromMilliseconds(300)));
        ani.AutoReverse = true;

        SolidColorBrush newBrush = new SolidColorBrush(fromColor);
        ani.BeginTime = TimeSpan.FromSeconds(2);
        rectangle.Fill = newBrush;
        newBrush.BeginAnimation(SolidColorBrush.ColorProperty, ani);
        //NameScope.GetNameScope(this).RegisterName(rectangle.Name, rectangle);
        //Storyboard board = new Storyboard();
        //board.Children.Add(ani);
        //Storyboard.SetTargetName(rectangle, rectangle.Name);
        //Storyboard.SetTargetProperty(ani, new PropertyPath(SolidColorBrush.ColorProperty));
        //board.Begin();

    }

What's the easiest way of accomplishing this? The code in the comments is my first guess, but it's not working correctly.

回答1:

There should be an event ani.Completed - handle that event and start the next phase of the animation, then start the first one running and each phase will trigger the next.

ColorAnimation ani = // whatever...

ani.Completed += (s, e) => 
   {
       ColorAnimation ani2 = // another one...

       // and so on
   };

newBrush.BeginAnimation(SolidColorBrush.ColorProperty, ani);

UPDATE:

public partial class Window1 : Window
{
    Rectangle blueRect;
    Rectangle redRect;
    Rectangle greenRect;
    Rectangle yellowRect;

    public Window1()
    {
        InitializeComponent();
        blueRect = new Rectangle() { Fill = System.Windows.Media.Brushes.Blue, Name = "Blue" };
        redRect = new Rectangle() { Fill = System.Windows.Media.Brushes.Red, Name = "Yellow" };
        greenRect = new Rectangle() { Fill = System.Windows.Media.Brushes.Green, Name = "Green" };
        yellowRect = new Rectangle() { Fill = System.Windows.Media.Brushes.Yellow, Name = "Yellow" };

        UniformGrid1.Children.Add(blueRect);
        UniformGrid1.Children.Add(redRect);
        UniformGrid1.Children.Add(greenRect);
        UniformGrid1.Children.Add(yellowRect);
    }

    IEnumerable<Action<Action>> AnimationSequence()
    {
        for (; ; )
        {
            yield return AnimateCell(blueRect, Colors.Blue);
            yield return AnimateCell(redRect, Colors.Red);
            yield return AnimateCell(greenRect, Colors.Green);
            yield return AnimateCell(yellowRect, Colors.Yellow);
        }
    }

    private IEnumerator<Action<Action>> _actions;

    private void RunNextAction()
    {
        if (_actions.MoveNext())
            _actions.Current(RunNextAction);
    }

    private void Window_Loaded(object sender, RoutedEventArgs e)
    {
        _actions = AnimationSequence().GetEnumerator();
        RunNextAction();
    }

    private Action<Action> AnimateCell(Rectangle rectangle, Color fromColor)
    {
        return completed =>
        {
            Color toColor = Colors.White;
            ColorAnimation ani = new ColorAnimation(toColor, 
                                    new Duration(TimeSpan.FromMilliseconds(300)));
            ani.AutoReverse = true;
            ani.Completed += (s, e) => completed();

            SolidColorBrush newBrush = new SolidColorBrush(fromColor);
            ani.BeginTime = TimeSpan.FromSeconds(2);
            rectangle.Fill = newBrush;
            newBrush.BeginAnimation(SolidColorBrush.ColorProperty, ani);
        };
    }
}

Try pasting the above into your program. It does what you need, but in a way that may be useful to you in other contexts. It's still event driven, but it uses an "iterator method" (with yield return) to create the impression that it is sequential coding that blocks while the animation is going on.

The nice thing about this is that you can play around with the AnimationSequence method in a very intuitive way - you could write out the timeline of the animation in a series of statements, or use loops, or whatever you want.



回答2:

The solution I've tried is to use a Queue like so. This will let you add to the animation chain dynamically. I'm not sure if the lock is necessary, but I left it in just to be safe.

Queue<Object[]> animationQueue = new Queue<Object[]>();

void sequentialAnimation(DoubleAnimation da, Animatable a, DependencyProperty dp)
{
    da.Completed += new EventHandler(da_Completed);

    lock (animationQueue)
    {
        if (animationQueue.Count == 0) // no animation pending
        {
            animationQueue.Enqueue(new Object[] { da, a, dp });
            a.BeginAnimation(dp, da);
        }
        else
        {
            animationQueue.Enqueue(new Object[] { da, a, dp });
        }
    }
}

void da_Completed(object sender, EventArgs e)
{
    lock (animationQueue)
    {
        Object[] completed = animationQueue.Dequeue();
        if (animationQueue.Count > 0)
        {
            Object[] next = animationQueue.Peek();
            DoubleAnimation da = (DoubleAnimation)next[0];
            Animatable a = (Animatable)next[1];
            DependencyProperty dp = (DependencyProperty)next[2];

            a.BeginAnimation(dp, da);
        }
    }
}


回答3:

This can be accomplished by using a class with the contradictory name ParallelTimeline and carefully adjusting the BeginTime property. Note in the example below how the BeginTime property of the second DoubleAnimation is set to the duration of the first.

<ParallelTimeline>
      <DoubleAnimation
           Storyboard.TargetName="FlashRectangle" 
           Storyboard.TargetProperty="Opacity"
           From="0.0" To="1.0" Duration="0:0:1"/>
      <DoubleAnimation BeginTime="0:0:0.05"
           Storyboard.TargetName="FlashRectangle" 
           Storyboard.TargetProperty="Opacity"
           From="1.0" To="0.0" Duration="0:0:2"/>
 </ParallelTimeline>