Я реализовал элемент управления для визуализации обратного отсчёта с анимацией дуги, который выглядит вот так:
Код рабочий, но требуется обратная по поводу качества реализации данного элемента управления.
Заметки по реализации:
Для визуализации дуги я создал класс
Arc, унаследовавшись отShape(код основан на этом посте).Я создал элемент управления
Countdown(наследуетUserControl). Для установки таймаута я добавил свойтсво зависимости (dependency property)Seconds. I'm using bindingContent="{Binding Seconds}"to display seconds. Анимация выполняется в code behindAnimation.Duration = new Duration(TimeSpan.FromSeconds(Seconds));потому что я не уверен, возможно ли это сделать в XAML без написания специального конвертера. Я полагаю, что написание здесь своего конвертера ради одной строчки кода здесь не оправдано.
Для масштабирования элемента управления, его содержимое обёртнуто в элемент управления
Viewbox.Я анимации секунд я использую
DispatcherTimer, ничего особенного. Нет ли способа это сделать лучше?
Код
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();
}
}
Элемент управления помещается на окно с помощью подобного кода
<local:Countdown Width="300" Height="300" Seconds="25" />
Такой же вопрос на Code Review SE.

DefiningGeometry- я бы содержимое вынес в отдельную функцию, и кешировал бы результат, чтобы по 100500 раз не вычислять одно и то же. – tym32167 Jun 25 '18 at 10:00