How to create a custom indeterminate progress bar for Windows Phone 8

[ This article is for Windows Phone 8 developers. If you’re developing for Windows 10, see the latest documentation. ]

 

Microsoft recommends that you use a ProgressBar control to indicate progress for a lengthy operation in your app. However, if you plan to add an indeterminate progress bar, using the ProgressBar class can decrease the performance of your Windows Phone OS 7.1 app. This is because the Windows Phone OS 7.1 implementation of ProgressBar runs an indeterminate progress bar on the UI thread, rather than on the compositor thread.

Instead, you can use the CustomIndeterminateProgressBar sample to add an indeterminate progress bar that runs on the compositor thread for better performance. This topic describes how the sample works, and how you can use it to add an indeterminate progress bar to your app.

Note

In Windows Phone 8, the ProgressBar control has been updated and doesn’t have the same performance issues when being used as an indeterminate progress bar. If you are creating an app that targets Windows Phone 8, you should use ProgressBar.

This topic contains the following sections.

 

How it works

This section describes how the sample works, but you don’t need to read this section to use the sample. If you want to use the sample as-is, skip to the next section and follow the instructions there.

The CustomIndeterminateProgressBar sample was created to solve two problems:

  • The need for an indeterminate progress bar that runs on the compositor thread for better performance.

  • The need for a progress bar that can handle scaling that is the result of orientation changes.

The solution is implemented in the XAML code, with an additional .cs file.

In the XAML code

  1. A Style in App.xaml for CustomIndeterminateProgressBar contains a custom VisualState for the Indeterminate state. The DoubleAnimationUsingKeyFrames object is used to animate the indeterminate progress bar.

  2. The Value of each key frame is a number that ends with ".1" or “.2”, which is used as a kind of naming convention to identify the values. When the orientation of the page changes, a custom processor looks for values with this suffix and converts them to a percentage of the control’s width or height, respectively.

  3. The CustomIndeterminateProgressBar is instantiated in MainPage.xaml. In the sample, a button controls whether the progress bar is visible or not, and toggles both Visibility and the IsIndeterminateProperty flag. For better performance, when you are building an app, you would want to ensure that the progress bar is both not visible and not running when it is not being displayed on the screen.

In the code-behind file

The RelativeAnimatingContentControl.cs file contains the logic that scales the indeterminate progress bar when orientation changes occur. Upon an orientation change, the following steps are taken:

  1. For any VisualState that contains a Storyboard, each DoubleAnimation or DoubleAnimationUsingKeyFrames value is sent to one of two processing methods.

  2. Any double animation key frame value that ends in ".1" is stripped of the ".1" and updated to a percentage of the width of the control. For example, Value="33.1" is used for a percentage of 33 percent. If the orientation changes and the control’s new width is 480, this key frame value would be changed to approximately 160.

  3. Any double animation key frame value that ends in ".2" is stripped of the ".2" and updated to a percentage of the height of the control.

Adding a custom indeterminate progress bar

To get the sample and create the project

  1. Download the CustomIndeterminateProgressBar sample.

  2. In Visual Studio, create a new **Windows Phone App ** project.

  3. In Solution Explorer, right-click the project name, then add the RelativeAnimatingContentControl.cs file from the sample to the root directory, using Add an Existing Item.

To add the CustomIndeterminateProgressBar style

  1. Open the App.xaml file. Inside the <Application> tag, add the following XMLNS declaration with the other namespace declarations:

    xmlns:unsupported="clr-namespace:Microsoft.Phone.Controls.Unsupported"
    
  2. Add the following XAML code inside of the <Application.Resources> tag:

    <Style x:Key="CustomIndeterminateProgressBar" TargetType="ProgressBar"> 
        <Setter Property="Foreground" Value="{StaticResource PhoneAccentBrush}"/> 
        <Setter Property="Background" Value="{StaticResource PhoneAccentBrush}"/> 
        <Setter Property="Maximum" Value="100"/> 
        <Setter Property="IsHitTestVisible" Value="False"/> 
        <Setter Property="Padding" Value="{StaticResource PhoneHorizontalMargin}"/> 
        <Setter Property="Template"> 
            <Setter.Value> 
                <ControlTemplate TargetType="ProgressBar"> 
                    <unsupported:RelativeAnimatingContentControl HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch"> 
                        <unsupported:RelativeAnimatingContentControl.Resources> 
                            <ExponentialEase EasingMode="EaseOut" Exponent="1" x:Key="ProgressBarEaseOut"/> 
                            <ExponentialEase EasingMode="EaseOut" Exponent="1" x:Key="ProgressBarEaseIn"/> 
                        </unsupported:RelativeAnimatingContentControl.Resources> 
                        <VisualStateManager.VisualStateGroups> 
                            <VisualStateGroup x:Name="CommonStates"> 
                                <VisualState x:Name="Determinate"/> 
                                <VisualState x:Name="Indeterminate"> 
                                    <Storyboard RepeatBehavior="Forever" Duration="00:00:04.4"> 
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="IndeterminateRoot"> 
                                            <DiscreteObjectKeyFrame KeyTime="0"> 
                                                <DiscreteObjectKeyFrame.Value> 
                                                    <Visibility>Visible</Visibility> 
                                                </DiscreteObjectKeyFrame.Value> 
                                            </DiscreteObjectKeyFrame> 
                                        </ObjectAnimationUsingKeyFrames> 
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="DeterminateRoot"> 
                                            <DiscreteObjectKeyFrame KeyTime="0"> 
                                                <DiscreteObjectKeyFrame.Value> 
                                                    <Visibility>Collapsed</Visibility> 
                                                </DiscreteObjectKeyFrame.Value> 
                                            </DiscreteObjectKeyFrame> 
                                        </ObjectAnimationUsingKeyFrames> 
                                        <DoubleAnimationUsingKeyFrames BeginTime="00:00:00.0" Storyboard.TargetProperty="X" Storyboard.TargetName="R1TT"> 
                                            <LinearDoubleKeyFrame KeyTime="00:00:00.0" Value="0.1"/> 
                                            <EasingDoubleKeyFrame KeyTime="00:00:00.5" Value="33.1" EasingFunction="{StaticResource ProgressBarEaseOut}"/> 
                                            <LinearDoubleKeyFrame KeyTime="00:00:02.0" Value="66.1"/> 
                                            <EasingDoubleKeyFrame KeyTime="00:00:02.5" Value="100.1" EasingFunction="{StaticResource ProgressBarEaseIn}"/> 
                                        </DoubleAnimationUsingKeyFrames> 
                                        <DoubleAnimationUsingKeyFrames BeginTime="00:00:00.2" Storyboard.TargetProperty="X" Storyboard.TargetName="R2TT"> 
                                            <LinearDoubleKeyFrame KeyTime="00:00:00.0" Value="0.1"/> 
                                            <EasingDoubleKeyFrame KeyTime="00:00:00.5" Value="33.1" EasingFunction="{StaticResource ProgressBarEaseOut}"/> 
                                            <LinearDoubleKeyFrame KeyTime="00:00:02.0" Value="66.1"/> 
                                            <EasingDoubleKeyFrame KeyTime="00:00:02.5" Value="100.1" EasingFunction="{StaticResource ProgressBarEaseIn}"/> 
                                        </DoubleAnimationUsingKeyFrames> 
                                        <DoubleAnimationUsingKeyFrames BeginTime="00:00:00.4" Storyboard.TargetProperty="X" Storyboard.TargetName="R3TT"> 
                                            <LinearDoubleKeyFrame KeyTime="00:00:00.0" Value="0.1"/> 
                                            <EasingDoubleKeyFrame KeyTime="00:00:00.5" Value="33.1" EasingFunction="{StaticResource ProgressBarEaseOut}"/> 
                                            <LinearDoubleKeyFrame KeyTime="00:00:02.0" Value="66.1"/> 
                                            <EasingDoubleKeyFrame KeyTime="00:00:02.5" Value="100.1" EasingFunction="{StaticResource ProgressBarEaseIn}"/> 
                                        </DoubleAnimationUsingKeyFrames> 
                                        <DoubleAnimationUsingKeyFrames BeginTime="00:00:00.6" Storyboard.TargetProperty="X" Storyboard.TargetName="R4TT"> 
                                            <LinearDoubleKeyFrame KeyTime="00:00:00.0" Value="0.1"/> 
                                            <EasingDoubleKeyFrame KeyTime="00:00:00.5" Value="33.1" EasingFunction="{StaticResource ProgressBarEaseOut}"/> 
                                            <LinearDoubleKeyFrame KeyTime="00:00:02.0" Value="66.1"/> 
                                            <EasingDoubleKeyFrame KeyTime="00:00:02.5" Value="100.1" EasingFunction="{StaticResource ProgressBarEaseIn}"/> 
                                        </DoubleAnimationUsingKeyFrames> 
                                        <DoubleAnimationUsingKeyFrames BeginTime="00:00:00.8" Storyboard.TargetProperty="X" Storyboard.TargetName="R5TT"> 
                                            <LinearDoubleKeyFrame KeyTime="00:00:00.0" Value="0.1"/> 
                                            <EasingDoubleKeyFrame KeyTime="00:00:00.5" Value="33.1" EasingFunction="{StaticResource ProgressBarEaseOut}"/> 
                                            <LinearDoubleKeyFrame KeyTime="00:00:02.0" Value="66.1"/> 
                                            <EasingDoubleKeyFrame KeyTime="00:00:02.5" Value="100.1" EasingFunction="{StaticResource ProgressBarEaseIn}"/> 
                                        </DoubleAnimationUsingKeyFrames> 
                                        <DoubleAnimationUsingKeyFrames BeginTime="00:00:00.0" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="R1"> 
                                            <DiscreteDoubleKeyFrame KeyTime="0" Value="1"/> 
                                            <DiscreteDoubleKeyFrame KeyTime="00:00:02.5" Value="0"/> 
                                        </DoubleAnimationUsingKeyFrames> 
                                        <DoubleAnimationUsingKeyFrames BeginTime="00:00:00.2" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="R2"> 
                                            <DiscreteDoubleKeyFrame KeyTime="0" Value="1"/> 
                                            <DiscreteDoubleKeyFrame KeyTime="00:00:02.5" Value="0"/> 
                                        </DoubleAnimationUsingKeyFrames> 
                                        <DoubleAnimationUsingKeyFrames BeginTime="00:00:00.4" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="R3"> 
                                            <DiscreteDoubleKeyFrame KeyTime="0" Value="1"/> 
                                            <DiscreteDoubleKeyFrame KeyTime="00:00:02.5" Value="0"/> 
                                        </DoubleAnimationUsingKeyFrames> 
                                        <DoubleAnimationUsingKeyFrames BeginTime="00:00:00.6" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="R4"> 
                                            <DiscreteDoubleKeyFrame KeyTime="0" Value="1"/> 
                                            <DiscreteDoubleKeyFrame KeyTime="00:00:02.5" Value="0"/> 
                                        </DoubleAnimationUsingKeyFrames> 
                                        <DoubleAnimationUsingKeyFrames BeginTime="00:00:00.8" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="R5"> 
                                            <DiscreteDoubleKeyFrame KeyTime="0" Value="1"/> 
                                            <DiscreteDoubleKeyFrame KeyTime="00:00:02.5" Value="0"/> 
                                        </DoubleAnimationUsingKeyFrames> 
                                    </Storyboard> 
                                </VisualState> 
                            </VisualStateGroup> 
                        </VisualStateManager.VisualStateGroups> 
                        <Grid> 
                            <Grid x:Name="DeterminateRoot" Margin="{TemplateBinding Padding}" Visibility="Visible"> 
                                <Rectangle x:Name="ProgressBarTrack" Fill="{TemplateBinding Background}" Height="4" Opacity="0.1"/> 
                                <Rectangle x:Name="ProgressBarIndicator" Fill="{TemplateBinding Foreground}" HorizontalAlignment="Left" Height="4"/> 
                            </Grid> 
                            <Border x:Name="IndeterminateRoot" Margin="{TemplateBinding Padding}" Visibility="Collapsed"> 
                                <Grid HorizontalAlignment="Left"> 
                                    <Rectangle Fill="{TemplateBinding Foreground}" Height="4" IsHitTestVisible="False" Width="4" x:Name="R1" Opacity="0" CacheMode="BitmapCache"> 
                                        <Rectangle.RenderTransform> 
                                            <TranslateTransform x:Name="R1TT"/> 
                                        </Rectangle.RenderTransform> 
                                    </Rectangle> 
                                    <Rectangle Fill="{TemplateBinding Foreground}" Height="4" IsHitTestVisible="False" Width="4" x:Name="R2" Opacity="0" CacheMode="BitmapCache"> 
                                        <Rectangle.RenderTransform> 
                                            <TranslateTransform x:Name="R2TT"/> 
                                        </Rectangle.RenderTransform> 
                                    </Rectangle> 
                                    <Rectangle Fill="{TemplateBinding Foreground}" Height="4" IsHitTestVisible="False" Width="4" x:Name="R3" Opacity="0" CacheMode="BitmapCache"> 
                                        <Rectangle.RenderTransform> 
                                            <TranslateTransform x:Name="R3TT"/> 
                                        </Rectangle.RenderTransform> 
                                    </Rectangle> 
                                    <Rectangle Fill="{TemplateBinding Foreground}" Height="4" IsHitTestVisible="False" Width="4" x:Name="R4" Opacity="0" CacheMode="BitmapCache"> 
                                        <Rectangle.RenderTransform> 
                                            <TranslateTransform x:Name="R4TT"/> 
                                        </Rectangle.RenderTransform> 
                                    </Rectangle> 
                                    <Rectangle Fill="{TemplateBinding Foreground}" Height="4" IsHitTestVisible="False" Width="4" x:Name="R5" Opacity="0" CacheMode="BitmapCache"> 
                                        <Rectangle.RenderTransform> 
                                            <TranslateTransform x:Name="R5TT"/> 
                                        </Rectangle.RenderTransform> 
                                    </Rectangle> 
                                </Grid> 
                            </Border> 
                        </Grid> 
                    </unsupported:RelativeAnimatingContentControl> 
                </ControlTemplate> 
            </Setter.Value> 
        </Setter> 
    </Style>
    

To add the CustomInderminateProgressBar

  1. The following code sets the IsIndeterminate flag to true, so you can see the progress bar immediately when you run the sample. To add the progress bar in XAML, use the following code:

    <ProgressBar 
       IsIndeterminate="true"
       x:Name="customIndeterminateProgressBar"
       Style="{StaticResource CustomIndeterminateProgressBar}"
    />
    

    Add the above code to MainPage.xaml, inside the <Grid> tag that has an x:Name of ContentPanel.

  2. When using the progress bar in your app, ensure that the progress bar is both set to IsIndeterminate = false and the Visibility property is set to Collapsed when the progress bar is not being used. For demonstration purposes, the sample accomplishes this by setting up a button that toggles those values in a click handler. Alternatively, if you do not want to set and toggle the values, you could set use data binding on IsIndeterminate.

    For the purpose of this sample, which uses a button to toggle the values, add the following code to MainPage.xaml, directly below the ProgressBar code in the previous step:

    <Button Content="Toggle ProgressBar" Height="72" HorizontalAlignment="Left" Margin="81,450,0,0" Name="toggleButton" VerticalAlignment="Top" Width="300" Click="toggleButton_Click" />
    

    The following code is used in the click handler to toggle the values:

    customIndeterminateProgressBar.IsIndeterminate = !(customIndeterminateProgressBar.IsIndeterminate);
    
    if (customIndeterminateProgressBar.Visibility == Visibility.Collapsed)
    {
       customIndeterminateProgressBar.Visibility = Visibility.Visible;
    }
    else
    {
       customIndeterminateProgressBar.Visibility = Visibility.Collapsed;
    }
    

See Also

Other Resources

ProgressBar control design guidelines for Windows Phone

App performance considerations for Windows Phone 8