ProgressRing for Windows Phone 8
ProgressRing for Windows Phone 8
A Windows-8-style ProgressRing for Windows Phone 8
The ProgressRing control for Metro (Windows Store style) apps is a great way to show the familiar loading spinner while your app performs some long-running action. But what about Windows Phone 8? There is the ProgressBar control (which has been updated for WP8 to fix some performance issues), but I wanted the round spinny thing instead of the flat movey thing, so I created it.
The code below consists of a .cs
file that contains the guts of the control, as well as the <Style>
definition. A couple of fun things to note:
The
<Style>
is the exact style copied from the Windows 8 resources, so the control looks and behaves identicallyNote the check for
hasAppliedTemplate
- this is required to avoid some wonky behavior that I blogged about recently whereVisualStateManager
doesnt like to be called beforeOnApplyTemplate()
has been called.
Anyway, next time you need some spinning-dots goodness in your WP8 app, give it a try and let me know what you think.
ProgressRing.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
namespace Monsters.WindowsPhone.Controls
{
public class ProgressRing : Control
{
bool hasAppliedTemplate = false;
public ProgressRing()
{
this.DefaultStyleKey = typeof(ProgressRing);
TemplateSettings = new TemplateSettingValues(60);
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
hasAppliedTemplate = true;
UpdateState(this.IsActive);
}
void UpdateState(bool isActive)
{
if (hasAppliedTemplate)
{
string state = isActive ? "Active" : "Inactive";
System.Windows.VisualStateManager.GoToState(this, state, true);
}
}
protected override System.Windows.Size MeasureOverride(System.Windows.Size availableSize)
{
var width = 100D;
if(!System.ComponentModel.DesignerProperties.IsInDesignTool)
width = this.Width != double.NaN ? this.Width : availableSize.Width;
TemplateSettings = new TemplateSettingValues(width);
return base.MeasureOverride(availableSize);
}
public bool IsActive
{
get { return (bool)GetValue(IsActiveProperty); }
set { SetValue(IsActiveProperty, value); }
}
// Using a DependencyProperty as the backing store for IsActive. This enables animation, styling, binding, etc...
public static readonly DependencyProperty IsActiveProperty =
DependencyProperty.Register("IsActive", typeof(bool), typeof(ProgressRing), new PropertyMetadata(false, new PropertyChangedCallback(IsActiveChanged)));
private static void IsActiveChanged(DependencyObject d, DependencyPropertyChangedEventArgs args)
{
var pr = (ProgressRing) d;
var isActive = (bool)args.NewValue;
pr.UpdateState(isActive);
}
public TemplateSettingValues TemplateSettings
{
get { return (TemplateSettingValues)GetValue(TemplateSettingsProperty); }
set { SetValue(TemplateSettingsProperty, value); }
}
// Using a DependencyProperty as the backing store for TemplateSettings. This enables animation, styling, binding, etc...
public static readonly DependencyProperty TemplateSettingsProperty =
DependencyProperty.Register("TemplateSettings", typeof(TemplateSettingValues), typeof(ProgressRing), new PropertyMetadata(new TemplateSettingValues(100)));
public class TemplateSettingValues : System.Windows.DependencyObject
{
public TemplateSettingValues(double width)
{
MaxSideLength = 400;
EllipseDiameter = width/10;
EllipseOffset = new System.Windows.Thickness(EllipseDiameter);
}
public double MaxSideLength
{
get { return (double)GetValue(MaxSideLengthProperty); }
set { SetValue(MaxSideLengthProperty, value); }
}
// Using a DependencyProperty as the backing store for MaxSideLength. This enables animation, styling, binding, etc...
public static readonly DependencyProperty MaxSideLengthProperty =
DependencyProperty.Register("MaxSideLength", typeof(double), typeof(TemplateSettingValues), new PropertyMetadata(0D));
public double EllipseDiameter
{
get { return (double)GetValue(EllipseDiameterProperty); }
set { SetValue(EllipseDiameterProperty, value); }
}
// Using a DependencyProperty as the backing store for EllipseDiameter. This enables animation, styling, binding, etc...
public static readonly DependencyProperty EllipseDiameterProperty =
DependencyProperty.Register("EllipseDiameter", typeof(double), typeof(TemplateSettingValues), new PropertyMetadata(0D));
public Thickness EllipseOffset
{
get { return (Thickness)GetValue(EllipseOffsetProperty); }
set { SetValue(EllipseOffsetProperty, value); }
}
// Using a DependencyProperty as the backing store for EllipseOffset. This enables animation, styling, binding, etc...
public static readonly DependencyProperty EllipseOffsetProperty =
DependencyProperty.Register("EllipseOffset", typeof(Thickness), typeof(TemplateSettingValues), new PropertyMetadata(new Thickness()));
}
}
}
ProgressRing Style
<!-- Default style for Windows.UI.Xaml.Controls.ProgressRing -->
<Style TargetType="controls:ProgressRing">
<Setter Property="Foreground" Value="{StaticResource AccentBrush}" />
<Setter Property="IsHitTestVisible" Value="False" />
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="MinHeight" Value="20" />
<Setter Property="MinWidth" Value="20" />
<Setter Property="IsTabStop" Value="False" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="controls:ProgressRing">
<Border x:Name="ProgressRingRoot" Background="{TemplateBinding Background}"
BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}">
<Border.Resources>
<Style x:Key="ProgressRingEllipseStyle" TargetType="Ellipse">
<Setter Property="Opacity" Value="0" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Top" />
</Style>
</Border.Resources>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="SizeStates">
<VisualState x:Name="Large">
<Storyboard>
<ObjectAnimationUsingKeyFrames Duration="0"
Storyboard.TargetName="SixthCircle"
Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Small" />
</VisualStateGroup>
<VisualStateGroup x:Name="ActiveStates">
<VisualState x:Name="Inactive" />
<VisualState x:Name="Active">
<Storyboard RepeatBehavior="Forever">
<ObjectAnimationUsingKeyFrames Duration="0"
Storyboard.TargetName="Ring"
Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames
Storyboard.TargetName="E1"
Storyboard.TargetProperty="Opacity"
BeginTime="0">
<DiscreteDoubleKeyFrame KeyTime="0" Value="1" />
<DiscreteDoubleKeyFrame KeyTime="0:0:3.21" Value="1" />
<DiscreteDoubleKeyFrame KeyTime="0:0:3.22" Value="0" />
<DiscreteDoubleKeyFrame KeyTime="0:0:3.47" Value="0" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames
Storyboard.TargetName="E2"
Storyboard.TargetProperty="Opacity"
BeginTime="00:00:00.167">
<DiscreteDoubleKeyFrame KeyTime="0" Value="1" />
<DiscreteDoubleKeyFrame KeyTime="0:0:3.21" Value="1" />
<DiscreteDoubleKeyFrame KeyTime="0:0:3.22" Value="0" />
<DiscreteDoubleKeyFrame KeyTime="0:0:3.47" Value="0" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames
Storyboard.TargetName="E3"
Storyboard.TargetProperty="Opacity"
BeginTime="00:00:00.334">
<DiscreteDoubleKeyFrame KeyTime="0" Value="1" />
<DiscreteDoubleKeyFrame KeyTime="0:0:3.21" Value="1" />
<DiscreteDoubleKeyFrame KeyTime="0:0:3.22" Value="0" />
<DiscreteDoubleKeyFrame KeyTime="0:0:3.47" Value="0" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames
Storyboard.TargetName="E4"
Storyboard.TargetProperty="Opacity"
BeginTime="00:00:00.501">
<DiscreteDoubleKeyFrame KeyTime="0" Value="1" />
<DiscreteDoubleKeyFrame KeyTime="0:0:3.21" Value="1" />
<DiscreteDoubleKeyFrame KeyTime="0:0:3.22" Value="0" />
<DiscreteDoubleKeyFrame KeyTime="0:0:3.47" Value="0" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames
Storyboard.TargetName="E5"
Storyboard.TargetProperty="Opacity"
BeginTime="00:00:00.668">
<DiscreteDoubleKeyFrame KeyTime="0" Value="1" />
<DiscreteDoubleKeyFrame KeyTime="0:0:3.21" Value="1" />
<DiscreteDoubleKeyFrame KeyTime="0:0:3.22" Value="0" />
<DiscreteDoubleKeyFrame KeyTime="0:0:3.47" Value="0" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames
Storyboard.TargetName="E6"
Storyboard.TargetProperty="Opacity"
BeginTime="00:00:00.835">
<DiscreteDoubleKeyFrame KeyTime="0" Value="1" />
<DiscreteDoubleKeyFrame KeyTime="0:0:3.21" Value="1" />
<DiscreteDoubleKeyFrame KeyTime="0:0:3.22" Value="0" />
<DiscreteDoubleKeyFrame KeyTime="0:0:3.47" Value="0" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames
Storyboard.TargetName="E1R"
BeginTime="0"
Storyboard.TargetProperty="Angle">
<SplineDoubleKeyFrame KeyTime="0" Value="-110" KeySpline="0.13,0.21,0.1,0.7"/>
<SplineDoubleKeyFrame KeyTime="0:0:0.433" Value="10" KeySpline="0.02,0.33,0.38,0.77"/>
<SplineDoubleKeyFrame KeyTime="0:0:1.2" Value="93"/>
<SplineDoubleKeyFrame KeyTime="0:0:1.617" Value="205" KeySpline="0.57,0.17,0.95,0.75"/>
<SplineDoubleKeyFrame KeyTime="0:0:2.017" Value="357" KeySpline="0,0.19,0.07,0.72"/>
<SplineDoubleKeyFrame KeyTime="0:0:2.783" Value="439"/>
<SplineDoubleKeyFrame KeyTime="0:0:3.217" Value="585" KeySpline="0,0,0.95,0.37"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames
Storyboard.TargetName="E2R"
BeginTime="00:00:00.167"
Storyboard.TargetProperty="Angle">
<SplineDoubleKeyFrame KeyTime="0" Value="-116" KeySpline="0.13,0.21,0.1,0.7"/>
<SplineDoubleKeyFrame KeyTime="0:0:0.433" Value="4" KeySpline="0.02,0.33,0.38,0.77"/>
<SplineDoubleKeyFrame KeyTime="0:0:1.2" Value="87"/>
<SplineDoubleKeyFrame KeyTime="0:0:1.617" Value="199" KeySpline="0.57,0.17,0.95,0.75"/>
<SplineDoubleKeyFrame KeyTime="0:0:2.017" Value="351" KeySpline="0,0.19,0.07,0.72"/>
<SplineDoubleKeyFrame KeyTime="0:0:2.783" Value="433"/>
<SplineDoubleKeyFrame KeyTime="0:0:3.217" Value="579" KeySpline="0,0,0.95,0.37"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames
Storyboard.TargetName="E3R"
BeginTime="00:00:00.334"
Storyboard.TargetProperty="Angle">
<SplineDoubleKeyFrame KeyTime="0" Value="-122" KeySpline="0.13,0.21,0.1,0.7"/>
<SplineDoubleKeyFrame KeyTime="0:0:0.433" Value="-2" KeySpline="0.02,0.33,0.38,0.77"/>
<SplineDoubleKeyFrame KeyTime="0:0:1.2" Value="81"/>
<SplineDoubleKeyFrame KeyTime="0:0:1.617" Value="193" KeySpline="0.57,0.17,0.95,0.75"/>
<SplineDoubleKeyFrame KeyTime="0:0:2.017" Value="345" KeySpline="0,0.19,0.07,0.72"/>
<SplineDoubleKeyFrame KeyTime="0:0:2.783" Value="427"/>
<SplineDoubleKeyFrame KeyTime="0:0:3.217" Value="573" KeySpline="0,0,0.95,0.37"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames
Storyboard.TargetName="E4R"
BeginTime="00:00:00.501"
Storyboard.TargetProperty="Angle">
<SplineDoubleKeyFrame KeyTime="0" Value="-128" KeySpline="0.13,0.21,0.1,0.7"/>
<SplineDoubleKeyFrame KeyTime="0:0:0.433" Value="-8" KeySpline="0.02,0.33,0.38,0.77"/>
<SplineDoubleKeyFrame KeyTime="0:0:1.2" Value="75"/>
<SplineDoubleKeyFrame KeyTime="0:0:1.617" Value="187" KeySpline="0.57,0.17,0.95,0.75"/>
<SplineDoubleKeyFrame KeyTime="0:0:2.017" Value="339" KeySpline="0,0.19,0.07,0.72"/>
<SplineDoubleKeyFrame KeyTime="0:0:2.783" Value="421"/>
<SplineDoubleKeyFrame KeyTime="0:0:3.217" Value="567" KeySpline="0,0,0.95,0.37"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames
Storyboard.TargetName="E5R"
BeginTime="00:00:00.668"
Storyboard.TargetProperty="Angle">
<SplineDoubleKeyFrame KeyTime="0" Value="-134" KeySpline="0.13,0.21,0.1,0.7"/>
<SplineDoubleKeyFrame KeyTime="0:0:0.433" Value="-14" KeySpline="0.02,0.33,0.38,0.77"/>
<SplineDoubleKeyFrame KeyTime="0:0:1.2" Value="69"/>
<SplineDoubleKeyFrame KeyTime="0:0:1.617" Value="181" KeySpline="0.57,0.17,0.95,0.75"/>
<SplineDoubleKeyFrame KeyTime="0:0:2.017" Value="331" KeySpline="0,0.19,0.07,0.72"/>
<SplineDoubleKeyFrame KeyTime="0:0:2.783" Value="415"/>
<SplineDoubleKeyFrame KeyTime="0:0:3.217" Value="561" KeySpline="0,0,0.95,0.37"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames
Storyboard.TargetName="E6R"
BeginTime="00:00:00.835"
Storyboard.TargetProperty="Angle">
<SplineDoubleKeyFrame KeyTime="0" Value="-140" KeySpline="0.13,0.21,0.1,0.7"/>
<SplineDoubleKeyFrame KeyTime="0:0:0.433" Value="-20" KeySpline="0.02,0.33,0.38,0.77"/>
<SplineDoubleKeyFrame KeyTime="0:0:1.2" Value="63"/>
<SplineDoubleKeyFrame KeyTime="0:0:1.617" Value="175" KeySpline="0.57,0.17,0.95,0.75"/>
<SplineDoubleKeyFrame KeyTime="0:0:2.017" Value="325" KeySpline="0,0.19,0.07,0.72"/>
<SplineDoubleKeyFrame KeyTime="0:0:2.783" Value="409"/>
<SplineDoubleKeyFrame KeyTime="0:0:3.217" Value="555" KeySpline="0,0,0.95,0.37"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid x:Name="Ring"
Margin="{TemplateBinding Padding}"
MaxWidth="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.MaxSideLength}"
MaxHeight="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.MaxSideLength}"
Visibility="Collapsed"
RenderTransformOrigin=".5,.5"
FlowDirection="LeftToRight">
<Canvas RenderTransformOrigin=".5,.5">
<Canvas.RenderTransform>
<RotateTransform x:Name="E1R" />
</Canvas.RenderTransform>
<Ellipse
x:Name="E1"
Style="{StaticResource ProgressRingEllipseStyle}"
Width="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseDiameter}"
Height="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseDiameter}"
Margin="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseOffset}"
Fill="{TemplateBinding Foreground}"/>
</Canvas>
<Canvas RenderTransformOrigin=".5,.5">
<Canvas.RenderTransform>
<RotateTransform x:Name="E2R" />
</Canvas.RenderTransform>
<Ellipse
x:Name="E2"
Style="{StaticResource ProgressRingEllipseStyle}"
Width="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseDiameter}"
Height="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseDiameter}"
Margin="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseOffset}"
Fill="{TemplateBinding Foreground}"/>
</Canvas>
<Canvas RenderTransformOrigin=".5,.5">
<Canvas.RenderTransform>
<RotateTransform x:Name="E3R" />
</Canvas.RenderTransform>
<Ellipse
x:Name="E3"
Style="{StaticResource ProgressRingEllipseStyle}"
Width="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseDiameter}"
Height="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseDiameter}"
Margin="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseOffset}"
Fill="{TemplateBinding Foreground}"/>
</Canvas>
<Canvas RenderTransformOrigin=".5,.5">
<Canvas.RenderTransform>
<RotateTransform x:Name="E4R" />
</Canvas.RenderTransform>
<Ellipse
x:Name="E4"
Style="{StaticResource ProgressRingEllipseStyle}"
Width="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseDiameter}"
Height="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseDiameter}"
Margin="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseOffset}"
Fill="{TemplateBinding Foreground}"/>
</Canvas>
<Canvas RenderTransformOrigin=".5,.5">
<Canvas.RenderTransform>
<RotateTransform x:Name="E5R" />
</Canvas.RenderTransform>
<Ellipse
x:Name="E5"
Style="{StaticResource ProgressRingEllipseStyle}"
Width="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseDiameter}"
Height="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseDiameter}"
Margin="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseOffset}"
Fill="{TemplateBinding Foreground}"/>
</Canvas>
<Canvas RenderTransformOrigin=".5,.5"
Visibility="Collapsed"
x:Name="SixthCircle">
<Canvas.RenderTransform>
<RotateTransform x:Name="E6R" />
</Canvas.RenderTransform>
<Ellipse
x:Name="E6"
Style="{StaticResource ProgressRingEllipseStyle}"
Width="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseDiameter}"
Height="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseDiameter}"
Margin="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.EllipseOffset}"
Fill="{TemplateBinding Foreground}"/>
</Canvas>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>