KISS: Having fun with WPF - Creating cool loading animation

While searching online for a simple loading animation to use in my WPF application, I discovered Excellent WPF Loading Animation. Digging into it the solution from the link I found the code was... far from optimal in my opinion. It was long, complicated and contained a lot of XAML.

Here are the faults I found with it while stealing borrowing it:

  • Required two custom control
  • Used long and complicated keyframe animation
  • A semi-non-smooth animation (as a result)

Looking at it, I decided that I could simplify the solution and make it much more customizable.

First off, looking into LoadingAnimation.xaml we can see that the loading bar uses 18 bars in a circle. Each bar is a custom control of type Block:

        <Grid Width="10.734" Height="10.004" Canvas.Left="38.614" Canvas.Top="0.331">
            <local:Block x:Name="block" RenderTransformOrigin="0.5,4.3689" OpacityMask="#00000000" VerticalAlignment="Top" Height="10.004">
                <local:Block.RenderTransform>
                    <TransformGroup>
                        <ScaleTransform/>
                        <SkewTransform/>
                        <RotateTransform Angle="180"/>
                        <TranslateTransform/>
                    </TransformGroup>
                </local:Block.RenderTransform>
            </local:Block> 
            <local:Block x:Name="block1" RenderTransformOrigin="0.5,4.3689" OpacityMask="#0C000000" VerticalAlignment="Top" Height="10.004">
                <local:Block.RenderTransform>
                    <TransformGroup>
                        <ScaleTransform ScaleX="0.99999999999999989" ScaleY="0.99999999999999989"/>
                        <SkewTransform/>
                        <RotateTransform Angle="-160"/>
                        <TranslateTransform/>
                    </TransformGroup>
                </local:Block.RenderTransform>
            </local:Block>

            <!-- ... -->

            <local:Block x:Name="block16" RenderTransformOrigin="0.5,4.3689" OpacityMask="#F0000000" VerticalAlignment="Top" Height="10.004">
                <local:Block.RenderTransform>
                    <TransformGroup>
                        <ScaleTransform/>
                        <SkewTransform/>
                        <RotateTransform Angle="140"/>
                        <TranslateTransform/>
                    </TransformGroup>
                </local:Block.RenderTransform>
            </local:Block>
            <local:Block x:Name="block17" RenderTransformOrigin="0.5,4.3689" OpacityMask="Black" VerticalAlignment="Top" Height="10.004">
                <local:Block.RenderTransform>
                    <TransformGroup>
                        <ScaleTransform ScaleX="0.99999999999999989" ScaleY="0.99999999999999989"/>
                        <SkewTransform/>
                        <RotateTransform Angle="160"/>
                        <TranslateTransform/>
                    </TransformGroup>
                </local:Block.RenderTransform>
            </local:Block>
        </Grid>

That's a lot of copy-pasted properties and declaration.

If we look into the Block.xaml we see the following:

<UserControl x:Class="LoadingControl.Control.Block"  
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Height="Auto" Width="Auto">
    <Grid x:Name="LayoutRoot">
        <Path Fill="#FF5482A1" Stretch="Fill" RenderTransformOrigin="0.5,4.3689" Data="M291.15499,85.897525 C291.15499,85.897525 301.88917,85.87921 301.88917,85.87921 301.88917,85.87921 300.38339,94.355061 300.38339,94.355061 300.38339,94.355061 292.85366,94.355042 292.85366,94.355042 292.85366,94.355042 291.15499,85.897525 291.15499,85.897525 z"/>
    </Grid>
</UserControl>  

Okay, so the custom control Block is only a Path control. Why not replace all instances of Block with Path, style it using Style in LoadingAnimation.xaml and then delete the Block.xaml? Hell, while we're at it, let's also style all these copy-pasted properties and remove some of those redundant Transforms.

Now it's looking much better:

    <UserControl.Resources>
        <Style TargetType="Path">
            <Setter Property="Fill" Value="#FF5482A1" />
            <Setter Property="Stretch" Value="Fill" />
            <Setter Property="RenderTransformOrigin" Value="0.5,4.3689" />
            <Setter Property="Data" Value="M291.15499,85.897525 C291.15499,85.897525 301.88917,85.87921 301.88917,85.87921 301.88917,85.87921 300.38339,94.355061 300.38339,94.355061 300.38339,94.355061 292.85366,94.355042 292.85366,94.355042 292.85366,94.355042 291.15499,85.897525 291.15499,85.897525 z" />
            <Setter Property="RenderTransformOrigin" Value="0.5,4.3689" />
            <Setter Property="VerticalAlignment" Value="Top" />
            <Setter Property="Height" Value="10.004" />
        </Style>
        <!-- skip all the storyboards -->
    </UserControl.Resources>

    <!-- ... -->

    <Grid Width="10.734" Height="10.004" Canvas.Left="38.614" Canvas.Top="0.331">
        <Path x:Name="block" OpacityMask="#00000000">
            <Path.RenderTransform>
                <TransformGroup>
                    <RotateTransform Angle="180"/>
                </TransformGroup>
            </Path.RenderTransform>
        </Path>
        <Path x:Name="block1" OpacityMask="#0C000000">
            <Path.RenderTransform>
                <TransformGroup>
                    <RotateTransform Angle="-160"/>
                </TransformGroup>
            </Path.RenderTransform>
        </Path>

        <!-- ... -->

        <Path x:Name="block16" OpacityMask="#F0000000">
            <Path.RenderTransform>
                <TransformGroup>
                    <RotateTransform Angle="140"/>
                </TransformGroup>
            </Path.RenderTransform>
        </Path>
        <Path x:Name="block17" OpacityMask="Black">
            <Path.RenderTransform>
                <TransformGroup>
                    <RotateTransform Angle="160"/>
                </TransformGroup>
            </Path.RenderTransform>
        </Path>
    </Grid>

Now the only thing left is to deal with all the storyboards. Let's look into a random story block for block4 or something:

<ColorAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="block4" Storyboard.TargetProperty="(UIElement.OpacityMask).(SolidColorBrush.Color)">  
    <SplineColorKeyFrame KeyTime="00:00:00" Value="#26000000"/>
    <SplineColorKeyFrame KeyTime="00:00:00.2290000" Value="#19000000"/>
    <SplineColorKeyFrame KeyTime="00:00:00.4590000" Value="#0C000000"/>
    <SplineColorKeyFrame KeyTime="00:00:00.6880000" Value="#00000000"/>
    <SplineColorKeyFrame KeyTime="00:00:00.9180000" Value="Black"/>
    <SplineColorKeyFrame KeyTime="00:00:01.1470000" Value="#EF000000"/>
    <SplineColorKeyFrame KeyTime="00:00:01.3760000" Value="#E2000000"/>
    <SplineColorKeyFrame KeyTime="00:00:01.6060000" Value="#D3000000"/>
    <SplineColorKeyFrame KeyTime="00:00:01.8350000" Value="#C6000000"/>
    <SplineColorKeyFrame KeyTime="00:00:02.0650000" Value="#B7000000"/>
    <SplineColorKeyFrame KeyTime="00:00:02.2940000" Value="#AA000000"/>
    <SplineColorKeyFrame KeyTime="00:00:02.5240000" Value="#9B000000"/>
    <SplineColorKeyFrame KeyTime="00:00:02.7530000" Value="#91000000"/>
    <SplineColorKeyFrame KeyTime="00:00:02.9820000" Value="#7F000000"/>
    <SplineColorKeyFrame KeyTime="00:00:03.2120000" Value="#72000000"/>
    <SplineColorKeyFrame KeyTime="00:00:03.4410000" Value="#63000000"/>
    <SplineColorKeyFrame KeyTime="00:00:03.6710000" Value="#56000000"/>
    <SplineColorKeyFrame KeyTime="00:00:03.9000000" Value="#3D000000"/>
</ColorAnimationUsingKeyFrames>  

God that looks ugly. Not to mention nightmarish to manage, maintain or update in the future. Let's simplify! XAML to the rescue! \o/

This is what the above XAML roughly does:

  • Duration is roughly 4 seconds.
  • Animates OpacityMask from full opacity to full transparent.
  • On full transparency, jumps back to no transparency.
  • There is a slight delay between full transparency to no transparency.

Judging from the values and everything else in that solution, it looks like a lot of the code was generated. Well we're gonna do this the right way.

First off, the author probably didn't know that there is <ColorAnimation> that works incredibly well with linear transformation (or that you can use linear transformation with Keyframes but details).

With that in mind, we can refactor the above keyframe nightmare into simple two-line <ColorAnimation> components:

<ColorAnimation BeginTime="00:00:00.000" Duration="00:00:00.888" From="#26000000" To="#00000000" Storyboard.TargetName="block4" Storyboard.TargetProperty="(UIElement.OpacityMask).(SolidColorBrush.Color)" />  
<ColorAnimation BeginTime="00:00:00.888" Duration="00:00:03.111" From="#FF000000" To="#26000000" Storyboard.TargetName="block4" Storyboard.TargetProperty="(UIElement.OpacityMask).(SolidColorBrush.Color)" />  

I also updated the time go full 4 seconds and removed the delay between full transparency to no transparency.

That's 20 lines compressed into 2 lines that's much easier to update, tweak and fix in the future. Final storyboard for everything looks like this:

<Storyboard x:Key="ProgressAnimation" RepeatBehavior="Forever">  
    <ColorAnimation BeginTime="00:00:00.000" Duration="00:00:03.777" From="#EE000000" To="#00000000" Storyboard.TargetName="block17" Storyboard.TargetProperty="(UIElement.OpacityMask).(SolidColorBrush.Color)" />
    <ColorAnimation BeginTime="00:00:03.777" Duration="00:00:00.222" From="#FF000000" To="#EE000000" Storyboard.TargetName="block17" Storyboard.TargetProperty="(UIElement.OpacityMask).(SolidColorBrush.Color)" />

    <ColorAnimation BeginTime="00:00:00.000" Duration="00:00:03.555" From="#E0000000" To="#00000000" Storyboard.TargetName="block16" Storyboard.TargetProperty="(UIElement.OpacityMask).(SolidColorBrush.Color)" />
    <ColorAnimation BeginTime="00:00:03.555" Duration="00:00:00.444" From="#FF000000" To="#E0000000" Storyboard.TargetName="block16" Storyboard.TargetProperty="(UIElement.OpacityMask).(SolidColorBrush.Color)" />

    <!-- skip block2 to block15 -->

    <ColorAnimation BeginTime="00:00:00.000" Duration="00:00:00.222" From="#0E000000" To="#00000000" Storyboard.TargetName="block1" Storyboard.TargetProperty="(UIElement.OpacityMask).(SolidColorBrush.Color)" />
    <ColorAnimation BeginTime="00:00:00.222" Duration="00:00:03.777" From="#FF000000" To="#0E000000" Storyboard.TargetName="block1" Storyboard.TargetProperty="(UIElement.OpacityMask).(SolidColorBrush.Color)" />

    <ColorAnimation BeginTime="00:00:00.000" Duration="00:00:04.000" From="#FF000000" To="#00000000" Storyboard.TargetName="block" Storyboard.TargetProperty="(UIElement.OpacityMask).(SolidColorBrush.Color)" />
</Storyboard>  

You can download my version below.

All in all, that was 552 + 8 XAML lines refactored into 202 XAML lines

Show Comments