次の方法で共有


Fun With Animation Part 1 - CompositionTarget.Rendering

image

Rich runtime support for animation is a key component of WPF and a lot can be said about it.  That is why I've started this multi part series of posts on the topic of having fun with WPF animations. 

Typically application animations are used as subtle visual cues (think blinking caret) or not-so-subtle transitions (think Flip3D).  Anyone how has played with the iPhone knows that a little animation can go a long way to making the difference between a boring app and something new and exciting.

In this series you'll learn how use animations in WPF to meet your applications needs, and hopefully I'll get you thinking about how to use animations to enhance the users experience.  Each post in this series will focus on a small part of WPF animation story, starting with today's topic: CompositionTarget.Rendering.

I decided to start with the Rendering event because I think it's the most familiar way to do animation for people coming from a Flash background, and because it's an easy way to dig into the basic concepts of animation before we dig into Timelines, Clocks, Storyboards, and the rest of WPF's animation classes.

Hooking the CompositionTarget.Rendering event will get you inside WPF's rendering loop.  This event is fired once each time WPF decides to render a frame.  Furthermore, and this is important for people trying to save battery life, hooking this event _causes_ WPF to continue rendering frames.  This is why it's important to only hook this event while you're animating, and remove your event handler when you're done.

Inside the event handler you have complete freedom to do whatever you want to any object.  In other words, hooking CompositionTarget.Rendering is very much a "do it yourself" animation system.  Later we'll learn about the "let WPF do it for me" style animation system, but for now we'll learn to manually animate things because it's good to know what's going on under the covers.  Besides, there are times when the Rendering event is the best choice, such as when you want to animate something based interdependencies between multiple objects (as you'll see below) or if you just want to control an animation in a procedural way such as a bouncing ball.

The term "Animation" simply means that something is changing over time.  It's most commonly associated with changing motion over time (movement), but in WPF you can animate almost anything.  In this posts sample we'll create a simple procedural motion animation which tells an element to follow or avoid another element.  This creates interesting motion patterns based on who's following/avoiding who.

The first step is to hook the Rendering event:

 
.cf1 { color: rgb(43,145,175) }
.bg1 { background: rgb(43,145,175) }
            CompositionTarget.Rendering += DancingChildren;

Next we need to create the elements to animate and define which elements to follow/avoid.  I'll use XAML to define the elements (in this case various Shapes) and I'll use the Tag property to define who to follow and who to avoid.  An attached property would also work for this purpose, but we'll keep it simple for now.

 
.cf1 { color: rgb(0,0,255) }
.bg1 { background: rgb(0,0,255) }
.cf2 { color: rgb(163,21,21) }
.bg2 { background: rgb(163,21,21) }
.cf3 { color: rgb(255,0,0) }
.bg3 { background: rgb(255,0,0) }
.cf4 { color: rgb(0,128,0) }
.bg4 { background: rgb(0,128,0) }
<Window x:Class="RenderingEventDemo.Window1"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    Height="300" Width="300"
    >
  <Canvas>
    <!-- the format of Tag should be "element_to_follow;element_to_avoid" -->
    <Rectangle Fill="Red" Stroke="Black" StrokeThickness="3" Width="20" Height="20" 
               Name="Square" Canvas.Left="200" Canvas.Top="80" Tag="Rectangle;Squircle"/>
    <Rectangle Fill="Pink" Stroke="Black" StrokeThickness="3" Width="40" Height="20" 
               Name="Rectangle" Canvas.Left="80" Canvas.Top="300" Tag="Circle;Square"/>
    <Ellipse   Fill="Green" Stroke="Black" StrokeThickness="3" Width="20" Height="20" 
               Name="Circle" Canvas.Left="0" Canvas.Top="0" Tag="Ellipse;Rectangle"/>
    <Ellipse   Fill="LightGreen" Stroke="Black" StrokeThickness="3" Width="40" Height="20" 
               Name="Ellipse" Canvas.Left="0" Canvas.Top="200" Tag="Squircle;Circle"/>
    <Rectangle Fill="Blue" Stroke="Black" StrokeThickness="3" Width="20" Height="20" RadiusX="7" RadiusY="7" 
               Name="Squircle" Canvas.Left="30" Canvas.Top="80" Tag="Square;Ellipse"/>
  </Canvas>
</Window>

Next we'll need to change the location of the elements in our Rendering event handler.  This can be done in a number of ways, but I've chose to use the Canvas.Top & Canvas.Left attached properties.

 
.cf1 { color: rgb(0,0,255) }
.bg1 { background: rgb(0,0,255) }
.cf2 { color: rgb(43,145,175) }
.bg2 { background: rgb(43,145,175) }
.cf3 { color: rgb(163,21,21) }
.bg3 { background: rgb(163,21,21) }
.cf4 { color: rgb(0,128,0) }
.bg4 { background: rgb(0,128,0) }
        private void DancingChildren(object sender, EventArgs e)
        {
            Canvas root = this.Content as Canvas;
            Point center = new Point(this.ActualWidth / 2.0, this.ActualHeight / 2.0);
            foreach (FrameworkElement child in root.Children)
            {
                string[] tag = ((string)child.Tag).Split(';');
                Point follow = GetLocation((FrameworkElement)FindName(tag[0]));
                Point avoid = GetLocation((FrameworkElement)FindName(tag[1]));
                Point me = GetLocation(child);

                // impulse's tweaked to come close to an orbit around the center
                Vector attract = (follow - me) * 0.1;
                Vector repel = (me - avoid) * 0.1555;
                Vector toCenter = (center - me) * 0.099;

                SetLocation(child, me + attract + repel + toCenter);
            }
        }

        private void SetLocation(FrameworkElement child, Point point)
        {
            Canvas.SetLeft(child, point.X);
            Canvas.SetTop(child, point.Y);
        }

        private Point GetLocation(FrameworkElement frameworkElement)
        {
            return new Point(Canvas.GetLeft(frameworkElement), Canvas.GetTop(frameworkElement));
        }

As you can see we're incrementally moving all elements in the direction of the center & in the direction of the follow element & in the direction away from the avoid element.  The relative strength of each term is tweaked to put the elements into an orbit, but you can modify it to converge/diverge.  Changing the follow/avoid Tags also changes the movement behavior in interesting ways.

To see it in action you can run the attached project.

The astute reader may notice a major shortcoming with the sample I've shown.  Is the motion frame-rate independent?  No.  In the example I've shown the motion is dependent on how fast WPF decides to render.  On Vista this is typically clamped to the monitors refresh rate, so on my system it's 75Hz.  On a system with a refresh rate of 60Hz this animation would be slower.  It's usually a good idea to avoid being frame-rate dependent while animating, so I'll talk about strategies for doing this in the next post in this series.

RenderingEventDemo.zip

Comments

  • Anonymous
    August 19, 2008
    Good one. Now I'm eager for part 2 ;).
  • Anonymous
    September 05, 2013
    Useful! it's been quite a while I searched for a stable animation way not linked to Storyboards or DispatcherTimer...
  • Anonymous
    April 08, 2014
    Useful