Animating the Fill Color of a Silverlight Ellipse

March 28, 2010 18:14

I'm working on this project where I intend to have a Silverlight control that auto-receives push notifications from a WCF service. The client end of the project involves displaying simple "pass" (green light) and "fail" (red light) indicators. Of course I want the indicators to change automatically as new information is received.

This then is a fairly basic tutorial on animating an ellipse to change from a "red" state to a "green" state and back. I'll use a timer to simulate receiving data from a WCF service, alternating the display state between "pass" (green) and "fail" (red). There is, of course, a smooth transition from one color to the other and back, and because of the 2 second timer interval and the 2 seconds it takes for the storyboard animation to complete, we wind up with a strobing ellipse that is constantly changing. This isn't necessarily the end-goal I had in mind, but it did have me captivated for a brief time.

Here is the "Fail" state:

image

And the "Pass" state:

image

The UI Element

For no particular reason I started with a button whose contents consist of a Ellipse and a TextBlock. The initial code for everything inside and including the Grid defined on line 4 came from Mark.NET's post Circular WPF Button Template. I contributed the button itself and the TextBlock, and I changed some of the colors. ;-)

   1: <Button BorderThickness="1" Width="400" Height="125">
   2:         <Button.Content>
   3:                 <StackPanel Orientation="Horizontal">
   4:                         <Grid Width="100" Height="100" Margin="5">
   5:                                 <Ellipse Name="buttonEllipse" Fill="Green">
   6:                                 </Ellipse>
   7:                                 <Ellipse>
   8:                                         <Ellipse.Fill>
   9:                                                 <RadialGradientBrush>
  10:                                                         <GradientStop Offset="0" Color="#00000000"/>
  11:                                                         <GradientStop Offset="0.88" Color="Black"/>
  12:                                                         <GradientStop Offset="1" Color="#80000000"/>
  13:                                                 </RadialGradientBrush>
  14:                                         </Ellipse.Fill>
  15:                                 </Ellipse>
  16:                                 <Ellipse Margin="10">
  17:                                         <Ellipse.Fill>
  18:                                                 <LinearGradientBrush>
  19:                                                         <GradientStop Offset="0" Color="#50FFFFFF"/>
  20:                                                         <GradientStop Offset="0.5" Color="#00FFFFFF"/>
  21:                                                         <GradientStop Offset="1" Color="#50FFFFFF"/>
  22:                                                 </LinearGradientBrush>
  23:                                         </Ellipse.Fill>
  24:                                 </Ellipse>
  25:                         </Grid>
  26:                         <TextBlock x:Name="testResultText" Text="Pass" FontSize="30" Margin="10" VerticalAlignment="Center" />
  27:                 </StackPanel>
  28:         </Button.Content>
  29: </Button>

The Storyboard

I wanted a smooth transition from one color to another when the (mock) test result state changes. I accomplished this by using two Storyboards: one for the green –> red transition, and another for the red –> green. Here's the XAML, which I defined inside the first Ellipse as an Ellipse.Resource:

   1: <Ellipse.Resources>
   2:     <Storyboard x:Name="colorStoryboardGreenToRed">
   3:         <ColorAnimationUsingKeyFrames BeginTime="00:00:00" 
   4:                 Storyboard.TargetName="buttonEllipse" 
   5:                 Storyboard.TargetProperty="(Ellipse.Fill).(SolidColorBrush.Color)">
   6:     
   7:             <!-- LinearColorKeyFrame creates a smooth, linear animation between values. -->
   8:             <LinearColorKeyFrame Value="Red" KeyTime="00:00:02" />
   9:     
  10:         </ColorAnimationUsingKeyFrames>
  11:     </Storyboard>
  12:     <Storyboard x:Name="colorStoryboardRedToGreen">
  13:         <ColorAnimationUsingKeyFrames BeginTime="00:00:00" 
  14:                 Storyboard.TargetName="buttonEllipse" 
  15:                 Storyboard.TargetProperty="(Ellipse.Fill).(SolidColorBrush.Color)">
  16:     
  17:             <!-- LinearColorKeyFrame creates a smooth, linear animation between values. -->
  18:             <LinearColorKeyFrame Value="Green" KeyTime="00:00:02" />
  19:     
  20:         </ColorAnimationUsingKeyFrames>
  21:     </Storyboard>
  22:     </Ellipse.Resources>

I defined a Storyboard, which is a container for other animation effects, that contains a single animation action: a ColorAnimationUsingKeyFrames, which at it's most basic is a way to transition a UI element's color from one to another. The two important attributes are "Storyboard.TargetName" and "Storyboard.TargetProperty". The former points to the previously defined Ellipse object as the target of this animation and the latter identifies the property we wish to transition. In this case it's Ellipse.Fill. LinearColorKeyFrame defines the smooth transition effect, changing our Ellipse from green to read and back again one frame at a time. Since KeyTime is set to 2 seconds, that's how long the transition will take.

If you want to read up on the basics of Silverlight animation then Animation Overview on MSDN is a great place to start.

Here's the full XAML:

   1: <UserControl x:Class="StrobingEllipse.MainPage"
   2:     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   3:     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   4:     xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
   5:     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
   6:     mc:Ignorable="d"
   7:     d:DesignHeight="125" d:DesignWidth="400">
   8:  
   9:     <StackPanel Orientation="Vertical">
  10:         <Button BorderThickness="1" Width="400" Height="125">
  11:             <Button.Content>
  12:                 <StackPanel Orientation="Horizontal">
  13:                     <Grid Width="100" Height="100" Margin="5">
  14:                         <Ellipse Name="buttonEllipse" Fill="Green">
  15:                             <Ellipse.Resources>
  16:                                 <Storyboard x:Name="colorStoryboardGreenToRed">
  17:                                     <ColorAnimationUsingKeyFrames BeginTime="00:00:00" 
  18:                                             Storyboard.TargetName="buttonEllipse" 
  19:                                             Storyboard.TargetProperty="(Ellipse.Fill).(SolidColorBrush.Color)">
  20:  
  21:                                         <!-- LinearColorKeyFrame creates a smooth, linear animation between values. -->
  22:                                         <LinearColorKeyFrame Value="Red" KeyTime="00:00:02" />
  23:  
  24:                                     </ColorAnimationUsingKeyFrames>
  25:                                 </Storyboard>
  26:                                 <Storyboard x:Name="colorStoryboardRedToGreen">
  27:                                     <ColorAnimationUsingKeyFrames BeginTime="00:00:00" 
  28:                                             Storyboard.TargetName="buttonEllipse" 
  29:                                             Storyboard.TargetProperty="(Ellipse.Fill).(SolidColorBrush.Color)">
  30:  
  31:                                         <!-- LinearColorKeyFrame creates a smooth, linear animation between values. -->
  32:                                         <LinearColorKeyFrame Value="Green" KeyTime="00:00:02" />
  33:  
  34:                                     </ColorAnimationUsingKeyFrames>
  35:                                 </Storyboard>
  36:                             </Ellipse.Resources>
  37:                         </Ellipse>
  38:                         <Ellipse>
  39:                             <Ellipse.Fill>
  40:                                 <RadialGradientBrush>
  41:                                     <GradientStop Offset="0" Color="#00000000"/>
  42:                                     <GradientStop Offset="0.88" Color="Black"/>
  43:                                     <GradientStop Offset="1" Color="#80000000"/>
  44:                                 </RadialGradientBrush>
  45:                             </Ellipse.Fill>
  46:                         </Ellipse>
  47:                         <Ellipse Margin="10">
  48:                             <Ellipse.Fill>
  49:                                 <LinearGradientBrush>
  50:                                     <GradientStop Offset="0" Color="#50FFFFFF"/>
  51:                                     <GradientStop Offset="0.5" Color="#00FFFFFF"/>
  52:                                     <GradientStop Offset="1" Color="#50FFFFFF"/>
  53:                                 </LinearGradientBrush>
  54:                             </Ellipse.Fill>
  55:                         </Ellipse>
  56:                     </Grid>
  57:                     <TextBlock x:Name="testResultText" Text="Pass" FontSize="30" Margin="10" VerticalAlignment="Center" />
  58:                 </StackPanel>
  59:             </Button.Content>
  60:         </Button>
  61:     </StackPanel>
  62: </UserControl>

The Codebehind

In the MainPage constructor I'll add a couple of events to capture the completion of each of the storyboards as well as kick off a timer which will initiate the Ellipse color change.

   1: public MainPage ()
   2: {
   3:     InitializeComponent ();
   4:  
   5:     // we'll change the test result text at the completion of each storyboard
   6:     colorStoryboardRedToGreen.Completed += colorStoryboardRedToGreen_Completed;
   7:     colorStoryboardGreenToRed.Completed += colorStoryboardGreenToRed_Completed;
   8:  
   9:     // simulate receiving results from WCF service
  10:     _receiveResults = new Timer (TimerReceiveResults, null, 2 * 1000, 0);
  11: }

The timer code is as follows:

   1: private void TimerReceiveResults (object state)
   2: {
   3:     // timer can stop for now
   4:     _receiveResults.Change (UInt32.MaxValue, UInt32.MaxValue);
   5:  
   6:     if (_whichStory)
   7:     {
   8:         Dispatcher.BeginInvoke (() => colorStoryboardGreenToRed.Begin ());
   9:     }
  10:     else
  11:     {
  12:         Dispatcher.BeginInvoke (() => colorStoryboardRedToGreen.Begin ());
  13:     }
  14:  
  15:     _whichStory = !_whichStory;
  16:  
  17:     // timer back to normal
  18:     _receiveResults.Change (2 * 1000, 0);
  19: }

I stop the timer while we're processing the event (always good practice), then check a flag to see if we have a "pass" or a "fail". Each Storyboard animation is started with the Begin method, and since I hooked up event handlers to the completion events I'll get a chance to do some more work when those are received.

Here's the code for those event handlers:

   1: void colorStoryboardRedToGreen_Completed (object sender, EventArgs e)
   2: {
   3:     testResultText.Text = "Pass";
   4: }
   5:  
   6: void colorStoryboardGreenToRed_Completed (object sender, EventArgs e)
   7: {
   8:     testResultText.Text = "Fail";
   9: }

Not much to it. I'm just updating the text of the button.

Conclusion

This is one of those things where the code is more a means to an end. In other words, a way to familiarize myself with a few new concepts which I can then take and actually do something useful with. Hope you also get something useful out of it.

Download: StrobingEllipse.zip

[ Follow me on Twitter ]