Animated Gauges for XAML

ComponentOne Gauges offer an interesting way to visualize your data. The C1Gauge libraries for Silverlight, WPF, Windows Phone and WinRT are all identical so I will refer to them as a whole as Gauges for XAML. While the gauges do not have built-in animation features, they do support standard XAML animation techniques. This blog post describes a common scenario and shows how you can apply an animation to the gauge pointer upon value change. It’s common to have a gauge bound to a single value within a collection, and provide some means for the user to traverse the collection. Through standard binding you can easily connect a gauge control to your data context by binding the Value property to some double.


<c1:C1RadialGauge x:Name= “gauge”  Value= “{Binding MyValue}” />  

When the underlying value changes, the C1RadialGauge will update instantly without any animation. In some platforms, like WinRT and Windows Phone especially, a small animation can add value to the overall vibe of your app.

Simple Storyboard

The concept of animating a gauge is to animate the pointer from typically 0 or the previous value to the new desired value. You can do this by applying a regular StoryBoard that changes the Value property of the gauge from one value to another. For example, create a StoryBoard that changes the value from 0 to the calculated value. You can dynamically adjust the To and From properties in code.


<Storyboard x:Name="AnimateRadial">  
    <DoubleAnimation Storyboard.TargetName="myGauge" Storyboard.TargetProperty="Value" From="0" Duration="00:00:08" BeginTime="00:00:00"/>  
</Storyboard>  


(AnimateRadial.Children[0] as DoubleAnimation).To = 100;  
AnimateRadial.Begin();  

That’s the basic concept of animating a gauge pointer in its simplest implementation. Next, I will show how you can remove the need to write any code at all in your code-behind by extending the C1RadialGauge control with the animation built-in.

Custom C1AnimatedGauge Implementation

The custom C1AnimatedGauge class inherits the C1RadialGauge (you could change this to any type of gauge) and it applies a StoryBoard animation upon value changes. This removes the need for you to write any code that updates and begins the StoryBoard each time the value changes. So it's more MVVM-friendly. The C1AnimatedGauge class also adds Duration and EasingFunction properties to the control so you can make modifications to the animation easily.


public class C1AnimatedGauge : C1.Silverlight.Gauge.C1RadialGauge  
{  
    /// <summary>  
    /// Gets or sets the target for the control's Value property.  
    /// </summary>  
    public double AnimatedValue  
    {  
        get { return (double)GetValue(AnimatedValueProperty); }  
        set { SetValue(AnimatedValueProperty, value); }  
    }  
    /// <summary>  
    /// Identifies the <see cref="AnimatedValue"/> dependency property.  
    /// </summary>  
    public static readonly DependencyProperty AnimatedValueProperty =  
        DependencyProperty.Register(  
            "AnimatedValue", typeof(double), typeof(C1AnimatedGauge),  
            new PropertyMetadata(OnTargetValuePropertyChanged));  
    static void OnTargetValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)  
    {  
        // get animated gauge  
        var ag = (C1AnimatedGauge)d;  

        // create animation  
        var da = new DoubleAnimation();  
        da.To = (double)e.NewValue;  
        da.Duration = new Duration(TimeSpan.FromMilliseconds(ag.Duration));  
        Storyboard.SetTargetProperty(da, new PropertyPath("Value"));  
        Storyboard.SetTarget(da, d);  

        // apply easing function  
        var ef = ag.EasingFunction;  
        if (ef == null)  
        {  
            var ee = new ElasticEase();  
            ee.Oscillations = 1;  
            ee.Springiness = 3;  
            ef = ee;  
        }  
        da.EasingFunction = ef;  

        // play animation  
        var sb = new Storyboard();  
        sb.Children.Add(da);  
        sb.Begin();  
    }  
    /// <summary>  
    /// Gets or sets the easing function that animates the gauge pointer.  
    /// </summary>  
    public IEasingFunction EasingFunction  
    {  
        get { return (IEasingFunction)GetValue(EasingFunctionProperty); }  
        set { SetValue(EasingFunctionProperty, value); }  
    }  
    /// <summary>  
    /// Identifies the <see cref="EasingFunction"/> dependency property.  
    /// </summary>  
    public static readonly DependencyProperty EasingFunctionProperty =  
        DependencyProperty.Register(  
            "EasingFunction", typeof(IEasingFunction), typeof(C1AnimatedGauge),  
            new PropertyMetadata(null));  
    /// <summary>  
    /// Gets or sets the duration of the animation, in milliseconds.  
    /// </summary>  
    public double Duration  
    {  
        get { return (double)GetValue(DurationProperty); }  
        set { SetValue(DurationProperty, value); }  
    }  
    /// <summary>  
    /// Identifies the <see cref="Duration"/> dependency property.  
    /// </summary>  
    public static readonly DependencyProperty DurationProperty =  
        DependencyProperty.Register(  
            "Duration", typeof(double), typeof(C1AnimatedGauge),  
            new PropertyMetadata(250.0));  
}  

Add this class to your project, build and replace any instances of C1RadialGauge with the C1AnimatedGauge class instead. Then bind or set the AnimatedValue property rather than the Value property to see an animation each time the value changes. Example:


<local:C1AnimatedGauge x:Name="gauge" Duration="800">  
    <local:C1AnimatedGauge.EasingFunction>  
        <ElasticEase Oscillations="3" Springiness="3"/>  
    </local:C1AnimatedGauge.EasingFunction>  
    <c1:C1GaugeMark Interval="10" />  
</local:C1AnimatedGauge>  


private void btnQ1_Click(object sender, RoutedEventArgs e)  
{  
    // animate a changed value  
    gauge.AnimatedValue = 20;  
}  

Notice I am also setting a Duration of 800 milliseconds and specifying an elastic easing function to make the animation a bit more interesting. The inner C1Gauge marks, ranges and labels still work as expected. The same code works in WPF, Silverlight, Windows Phone and even WinRT XAML. Please note that with WinRT there are two lines of code that are different. WinRT Differences:


…  
// add or change these lines of code for C1AnimatedGauge in WinRT  
da.EnableDependentAnimation = true;  
Storyboard.SetTargetProperty(da, "Value");  
…  

Download Sample

ComponentOne Product Manager Greg Lutz

Greg Lutz

comments powered by Disqus