AvaloniaCountdownTimer.cs 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. using Avalonia;
  2. using Avalonia.Controls;
  3. using Avalonia.Media;
  4. using Avalonia.Threading;
  5. using System.Windows.Input;
  6. using InABox.Core;
  7. namespace InABox.Avalonia.Components;
  8. public class CircularCountdownTimer : Control
  9. {
  10. private readonly DispatcherTimer _timer;
  11. private DateTime? _startTime;
  12. public static readonly StyledProperty<IImage?> ImageProperty =
  13. AvaloniaProperty.Register<CircularCountdownTimer, IImage?>(nameof(Image));
  14. public IImage? Image
  15. {
  16. get => GetValue(ImageProperty);
  17. set => SetValue(ImageProperty, value);
  18. }
  19. public static readonly StyledProperty<bool> IsActiveProperty =
  20. AvaloniaProperty.Register<CircularCountdownTimer, bool>(nameof(IsActive));
  21. public bool IsActive
  22. {
  23. get => GetValue(IsActiveProperty);
  24. set => SetValue(IsActiveProperty, value);
  25. }
  26. public static readonly StyledProperty<ICommand?> StartedProperty =
  27. AvaloniaProperty.Register<CircularCountdownTimer, ICommand?>(nameof(Started));
  28. public ICommand? Started
  29. {
  30. get => GetValue(StartedProperty);
  31. set => SetValue(StartedProperty, value);
  32. }
  33. public static readonly StyledProperty<ICommand?> StoppedProperty =
  34. AvaloniaProperty.Register<CircularCountdownTimer, ICommand?>(nameof(Stopped));
  35. public ICommand? Stopped
  36. {
  37. get => GetValue(StoppedProperty);
  38. set => SetValue(StoppedProperty, value);
  39. }
  40. public static readonly StyledProperty<IBrush> BackgroundProperty =
  41. AvaloniaProperty.Register<CircularCountdownTimer, IBrush>(nameof(Background), Brushes.Transparent);
  42. public IBrush Background
  43. {
  44. get => GetValue(BackgroundProperty);
  45. set => SetValue(BackgroundProperty, value);
  46. }
  47. public static readonly StyledProperty<double> StrokeThicknessProperty =
  48. AvaloniaProperty.Register<CircularCountdownTimer, double>(nameof(StrokeThickness), 10.0);
  49. public double StrokeThickness
  50. {
  51. get => GetValue(StrokeThicknessProperty);
  52. set => SetValue(StrokeThicknessProperty, value);
  53. }
  54. public static readonly StyledProperty<IBrush> ProgressBackgroundProperty =
  55. AvaloniaProperty.Register<CircularCountdownTimer, IBrush>(nameof(ProgressBackground), Brushes.Gray);
  56. public IBrush ProgressBackground
  57. {
  58. get => GetValue(ProgressBackgroundProperty);
  59. set => SetValue(ProgressBackgroundProperty, value);
  60. }
  61. public static readonly StyledProperty<IBrush> ProgressForegroundProperty =
  62. AvaloniaProperty.Register<CircularCountdownTimer, IBrush>(nameof(ProgressForeground), Brushes.WhiteSmoke);
  63. public IBrush ProgressForeground
  64. {
  65. get => GetValue(ProgressForegroundProperty);
  66. set => SetValue(ProgressForegroundProperty, value);
  67. }
  68. protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
  69. {
  70. base.OnPropertyChanged(change);
  71. if (change.Property == IsActiveProperty)
  72. {
  73. if (Equals(change.NewValue,true))
  74. Start();
  75. else
  76. Stop();
  77. }
  78. }
  79. public static readonly StyledProperty<double> DurationProperty =
  80. AvaloniaProperty.Register<CircularCountdownTimer, double>(nameof(Duration), 15.0);
  81. public double Duration
  82. {
  83. get => GetValue(DurationProperty);
  84. set
  85. {
  86. SetValue(DurationProperty, value);
  87. InvalidateVisual();
  88. }
  89. }
  90. public CircularCountdownTimer()
  91. {
  92. _timer = new DispatcherTimer(DispatcherPriority.MaxValue)
  93. {
  94. Interval = TimeSpan.FromMilliseconds(200),
  95. };
  96. _timer.Tick += TimerTick;
  97. }
  98. private void Start()
  99. {
  100. _startTime = DateTime.Now;
  101. _timer.Start();
  102. Started?.Execute(DataContext);
  103. InvalidateVisual();
  104. }
  105. private void Stop()
  106. {
  107. _timer.Stop();
  108. _startTime = null;
  109. Stopped?.Execute(DataContext);
  110. InvalidateVisual();
  111. }
  112. private TimeSpan RemainingTime()
  113. {
  114. if (!_startTime.HasValue)
  115. return TimeSpan.Zero;
  116. var _nowTime = DateTime.Now;
  117. var _endTime = _startTime.Value.AddSeconds(Duration);
  118. return _endTime > _nowTime
  119. ? _endTime - _nowTime
  120. : TimeSpan.Zero;
  121. }
  122. private void TimerTick(object? sender, EventArgs e)
  123. {
  124. if (RemainingTime() <= TimeSpan.Zero)
  125. IsActive = false;
  126. else
  127. InvalidateVisual();
  128. }
  129. public override void Render(DrawingContext context)
  130. {
  131. var boundsRect = new Rect(Bounds.Left+Margin.Left, Bounds.Top+Margin.Top, Bounds.Width-(Margin.Left+Margin.Right), Bounds.Height-(Margin.Top+Margin.Bottom));
  132. double centerX = boundsRect.Width / 2;
  133. double centerY = boundsRect.Height / 2;
  134. double radius = Math.Min(centerX, centerY) - StrokeThickness;
  135. var backgroundPen = new Pen(ProgressBackground, StrokeThickness);
  136. var progressPen = new Pen(ProgressForeground, StrokeThickness)
  137. {
  138. LineCap = PenLineCap.Flat // Smooth arc edges
  139. };
  140. context.FillRectangle(Background, boundsRect);
  141. // Draw background circle
  142. context.DrawEllipse(null, backgroundPen, new Point(centerX, centerY), radius, radius);
  143. if (Image != null)
  144. {
  145. var boxwidth = Math.Sqrt(2) * radius;
  146. Rect rect = new Rect(centerX - (boxwidth/2), centerY - (boxwidth/2), boxwidth, boxwidth);
  147. context.DrawImage(Image, rect);
  148. }
  149. double _remainingTime = RemainingTime().TotalSeconds;
  150. if (_remainingTime.IsEffectivelyGreaterThan(0.0))
  151. {
  152. // Calculate the sweep angle
  153. double sweepAngle = (_remainingTime / Duration) * 360;
  154. double startAngle = -90;
  155. double endAngle = startAngle + sweepAngle;
  156. // Convert angles to radians
  157. double startRad = Math.PI * startAngle / 180.0;
  158. double endRad = Math.PI * endAngle / 180.0;
  159. // Calculate points on the circle
  160. var startPoint = new Point(
  161. centerX + radius * Math.Cos(startRad),
  162. centerY + radius * Math.Sin(startRad)
  163. );
  164. var endPoint = new Point(
  165. centerX + radius * Math.Cos(endRad),
  166. centerY + radius * Math.Sin(endRad)
  167. );
  168. // Create an arc geometry
  169. var geometry = new StreamGeometry();
  170. using (var ctx = geometry.Open())
  171. {
  172. ctx.BeginFigure(startPoint, false);
  173. ctx.ArcTo(endPoint, new Size(radius, radius), 0, sweepAngle > 180, SweepDirection.Clockwise);
  174. }
  175. // Draw the progress arc
  176. context.DrawGeometry(null, progressPen, geometry);
  177. }
  178. }
  179. }