I've implemented countdown timer control with arc animation that looks like this
Implementation notes:
For arc visualization I've created class
Arc
derived fromShape
(code is based on this post).I've created
Countdown
control (derived fromUserControl
). For setting timeout I've addedSeconds
dependency property. I'm using bindingContent="{Binding Seconds}"
to display seconds. Animation duration is set in code behindAnimation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));
because I'm not sure if it's possible to do it in XAML without writing custom converter. I think that creating custom converter is not justified here.
For control's scaling content is wrapped in
Viewbox
сontrol.For seconds animation I'm using
DispatcherTimer
, nothing special. Is it the best to go here?
Code
Arc.cs
public class Arc : Shape
{
public Point Center
{
get => (Point)GetValue(CenterProperty);
set => SetValue(CenterProperty, value);
}
// Using a DependencyProperty as the backing store for Center. This enables animation, styling, binding, etc...
public static readonly DependencyProperty CenterProperty =
DependencyProperty.Register("Center", typeof(Point), typeof(Arc),
new FrameworkPropertyMetadata(new Point(), FrameworkPropertyMetadataOptions.AffectsRender));
// Start angle in degrees
public double StartAngle
{
get => (double)GetValue(StartAngleProperty);
set => SetValue(StartAngleProperty, value);
}
// Using a DependencyProperty as the backing store for StartAngle. This enables animation, styling, binding, etc...
public static readonly DependencyProperty StartAngleProperty =
DependencyProperty.Register("StartAngle", typeof(double), typeof(Arc),
new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender));
// End angle in degrees
public double EndAngle
{
get => (double)GetValue(EndAngleProperty);
set => SetValue(EndAngleProperty, value);
}
// Using a DependencyProperty as the backing store for EndAngle. This enables animation, styling, binding, etc...
public static readonly DependencyProperty EndAngleProperty =
DependencyProperty.Register("EndAngle", typeof(double), typeof(Arc),
new FrameworkPropertyMetadata(90.0, FrameworkPropertyMetadataOptions.AffectsRender));
public double Radius
{
get => (double)GetValue(RadiusProperty);
set => SetValue(RadiusProperty, value);
}
// Using a DependencyProperty as the backing store for Radius. This enables animation, styling, binding, etc...
public static readonly DependencyProperty RadiusProperty =
DependencyProperty.Register("Radius", typeof(double), typeof(Arc),
new FrameworkPropertyMetadata(10.0, FrameworkPropertyMetadataOptions.AffectsRender));
public bool SmallAngle
{
get => (bool)GetValue(SmallAngleProperty);
set => SetValue(SmallAngleProperty, value);
}
// Using a DependencyProperty as the backing store for SmallAngle. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SmallAngleProperty =
DependencyProperty.Register("SmallAngle", typeof(bool), typeof(Arc),
new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender));
static Arc() => DefaultStyleKeyProperty.OverrideMetadata(typeof(Arc), new FrameworkPropertyMetadata(typeof(Arc)));
protected override Geometry DefiningGeometry
{
get
{
double startAngleRadians = StartAngle * Math.PI / 180;
double endAngleRadians = EndAngle * Math.PI / 180;
double a0 = StartAngle < 0 ? startAngleRadians + 2 * Math.PI : startAngleRadians;
double a1 = EndAngle < 0 ? endAngleRadians + 2 * Math.PI : endAngleRadians;
if (a1 < a0)
a1 += Math.PI * 2;
SweepDirection d = SweepDirection.Counterclockwise;
bool large;
if (SmallAngle)
{
large = false;
double t = a1;
d = (a1 - a0) > Math.PI ? SweepDirection.Counterclockwise : SweepDirection.Clockwise;
}
else
large = (Math.Abs(a1 - a0) < Math.PI);
Point p0 = Center + new Vector(Math.Cos(a0), Math.Sin(a0)) * Radius;
Point p1 = Center + new Vector(Math.Cos(a1), Math.Sin(a1)) * Radius;
List<PathSegment> segments = new List<PathSegment>
{
new ArcSegment(p1, new Size(Radius, Radius), 0.0, large, d, true)
};
List<PathFigure> figures = new List<PathFigure>
{
new PathFigure(p0, segments, true)
{
IsClosed = false
}
};
return new PathGeometry(figures, FillRule.EvenOdd, null);
}
}
}
Countdown.xaml
<UserControl x:Class="WpfApp3.Countdown"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfApp"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="450" Loaded="UserControl_Loaded">
<UserControl.Triggers>
<EventTrigger RoutedEvent="UserControl.Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Name="Animation"
Storyboard.TargetName="Arc"
Storyboard.TargetProperty="EndAngle"
From="-90"
To="270" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</UserControl.Triggers>
<Viewbox>
<Grid Width="100" Height="100">
<Border Background="#222" Margin="5" CornerRadius="50">
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<Label Foreground="#fff" Content="{Binding Seconds}" FontSize="50" Margin="0, -10, 0, 0" />
<Label Foreground="#fff" Content="sec" HorizontalAlignment="Center" Margin="0, -20, 0, 0" />
</StackPanel>
</Border>
<local:Arc
x:Name="Arc"
Center="50, 50"
StartAngle="-90"
EndAngle="-90"
Stroke="#45d3be"
StrokeThickness="5"
Radius="45" />
</Grid>
</Viewbox>
</UserControl>
Countdown.xaml.cs
public partial class Countdown : UserControl
{
public int Seconds
{
get => (int)GetValue(SecondsProperty);
set => SetValue(SecondsProperty, value);
}
public static readonly DependencyProperty SecondsProperty =
DependencyProperty.Register(nameof(Seconds), typeof(int), typeof(Countdown), new PropertyMetadata(0));
private readonly DispatcherTimer _timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
public Countdown()
{
InitializeComponent();
DataContext = this;
}
private void UserControl_Loaded(object sender, EventArgs e)
{
Animation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));
if (Seconds > 0)
{
_timer.Start();
_timer.Tick += Timer_Tick;
}
}
private void Timer_Tick(object sender, EventArgs e)
{
Seconds--;
if (Seconds == 0) _timer.Stop();
}
}
Control is placed on Window
like this
<local:Countdown Width="300" Height="300" Seconds="25" />