Add TimerStatistic And Statistic Controls

This commit is contained in:
chinboy
2025-12-08 14:49:25 +08:00
parent c426e111da
commit b59b026c98
7 changed files with 275 additions and 50 deletions
@@ -11,6 +11,31 @@ public class StatisticViewModel : ReactiveObject, IRoutableViewModel
public string UrlPathSegment { get; } = ID.ToString();
private DateTime _deadline;
public DateTime Deadline
{
get => _deadline;
set => this.RaiseAndSetIfChanged(ref _deadline, value);
}
private DateTime _tenSecondsLater;
public DateTime TenSecondsLater
{
get => _tenSecondsLater;
set => this.RaiseAndSetIfChanged(ref _tenSecondsLater, value);
}
private DateTime _before;
public DateTime Before
{
get => _before;
set => this.RaiseAndSetIfChanged(ref _before, value);
}
public StatisticViewModel(IScreen screen)
{
HostScreen = screen;
@@ -50,7 +50,7 @@
<gallery:ShowCaseItem Title="Animated number" Description="Animated number with StatisticCountUp." IsOccupyEntireRow="True">
<UniformGrid Columns="2" Rows="2">
<atom:Statistic Header="Active Users" Value="100">
<atom:Statistic Header="Active Users" Value="112893">
<atom:Statistic.Content>
<atom:StatisticCountUp EndValue="{Binding Value}"
Precision="2"/>
@@ -64,5 +64,16 @@
</atom:Statistic>
</UniformGrid>
</gallery:ShowCaseItem>
<gallery:ShowCaseItem Title="Timer" Description="Timer component." IsOccupyEntireRow="True">
<UniformGrid Columns="2" Rows="3" RowSpacing="10">
<atom:TimerStatistic Value="{Binding Deadline}"/>
<atom:TimerStatistic Header="Million Seconds" Value="{Binding Deadline}" Format="hh\:mm\:ss\.fff"/>
<atom:TimerStatistic Header="Countdown" Value="{Binding TenSecondsLater}"/>
<atom:TimerStatistic Header="Countup" Value="{Binding Before}"/>
<atom:TimerStatistic Header="Day Level (Countdown)" Value="{Binding Deadline}" Format="d\ \天\ h\ \时\ m\ \分\ s\ \秒"/>
<atom:TimerStatistic Header="Day Level (Countup)" Value="{Binding Before}" Format="d\ \天\ h\ \时\ m\ \分\ s\ \秒"/>
</UniformGrid>
</gallery:ShowCaseItem>
</gallery:ShowCasePanel>
</UserControl>
@@ -8,7 +8,15 @@ public partial class StatisticShowCase : ReactiveUserControl<StatisticViewModel>
{
public StatisticShowCase()
{
this.WhenActivated(disposables => { });
this.WhenActivated(disposables =>
{
if (DataContext is StatisticViewModel viewModel)
{
viewModel.Deadline = DateTime.Now.Add(TimeSpan.FromSeconds(60 * 60 * 24 * 2 + 30));
viewModel.Before = DateTime.Now.Subtract(TimeSpan.FromSeconds(60 * 60 * 24 * 2 + 30));
viewModel.TenSecondsLater = DateTime.Now.AddSeconds(10);
}
});
InitializeComponent();
}
}
@@ -4,21 +4,56 @@
xmlns:atom="https://atomui.net"
x:Class="AtomUI.Desktop.Controls.Themes.AbstractStatisticTheme"
x:ClassModifier="internal">
<Setter Property="Template">
<ControlTemplate TargetType="atom:AbstractStatistic">
<Border Name="Frame"
Background="{TemplateBinding Background}"
CornerRadius="{TemplateBinding CornerRadius}">
<StackPanel Name="RootLayout">
<ContentPresenter Name="HeaderPresenter"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
IsVisible="{TemplateBinding Header, Converter={x:Static ObjectConverters.IsNotNull}}" />
<atom:Skeleton IsLoading="{TemplateBinding IsLoading}" IsActive="True" IsShowParagraph="False">
<StackPanel Orientation="Horizontal" Name="ContentLayout">
<ContentPresenter Name="ValuePrefixAddOn"
Foreground="{TemplateBinding ContentForeground}"
FontSize="{TemplateBinding ContentFontSize}"
Content="{TemplateBinding ValuePrefixAddOn}"
ContentTemplate="{TemplateBinding ValuePrefixAddOnTemplate}"
IsVisible="{TemplateBinding ValuePrefixAddOn, Converter={x:Static ObjectConverters.IsNotNull}}" />
<ContentPresenter Name="ContentPresenter"
Foreground="{TemplateBinding ContentForeground}"
FontSize="{TemplateBinding ContentFontSize}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}" />
<ContentPresenter Name="ValueSuffixAddOn"
Foreground="{TemplateBinding ContentForeground}"
FontSize="{TemplateBinding ContentFontSize}"
Content="{TemplateBinding ValueSuffixAddOn}"
ContentTemplate="{TemplateBinding ValueSuffixAddOnTemplate}"
IsVisible="{TemplateBinding ValueSuffixAddOn, Converter={x:Static ObjectConverters.IsNotNull}}" />
</StackPanel>
</atom:Skeleton>
</StackPanel>
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^:is(atom|AbstractStatistic)">
<Style Selector="^ /template/ ContentPresenter#HeaderPresenter">
<Setter Property="Foreground" Value="{DynamicResource {x:Static atom:SharedTokenKey.ColorTextDescription}}"/>
<Setter Property="FontSize" Value="{DynamicResource {x:Static atom:StatisticTokenKey.TitleFontSize}}"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static atom:SharedTokenKey.ColorTextDescription}}" />
<Setter Property="FontSize" Value="{DynamicResource {x:Static atom:StatisticTokenKey.TitleFontSize}}" />
</Style>
<Setter Property="ContentForeground" Value="{DynamicResource {x:Static atom:SharedTokenKey.ColorTextHeading}}"/>
<Setter Property="ContentFontSize" Value="{DynamicResource {x:Static atom:StatisticTokenKey.ContentFontSize}}"/>
<Setter Property="ContentForeground" Value="{DynamicResource {x:Static atom:SharedTokenKey.ColorTextHeading}}" />
<Setter Property="ContentFontSize" Value="{DynamicResource {x:Static atom:StatisticTokenKey.ContentFontSize}}" />
<Style Selector="^ /template/ atom|Icon">
<Setter Property="FillBrush" Value="{TemplateBinding ContentForeground}"/>
<Setter Property="StrokeBrush" Value="{TemplateBinding ContentForeground}"/>
<Setter Property="Width" Value="{DynamicResource {x:Static atom:StatisticTokenKey.ContentFontSize}}"/>
<Setter Property="Height" Value="{DynamicResource {x:Static atom:StatisticTokenKey.ContentFontSize}}"/>
<Setter Property="FillBrush" Value="{TemplateBinding ContentForeground}" />
<Setter Property="StrokeBrush" Value="{TemplateBinding ContentForeground}" />
<Setter Property="Width" Value="{DynamicResource {x:Static atom:StatisticTokenKey.ContentFontSize}}" />
<Setter Property="Height" Value="{DynamicResource {x:Static atom:StatisticTokenKey.ContentFontSize}}" />
</Style>
</Style>
</ControlTheme>
@@ -7,40 +7,6 @@
<ControlTheme.BasedOn>
<atom:AbstractStatisticTheme TargetType="atom:Statistic"/>
</ControlTheme.BasedOn>
<Setter Property="Template">
<ControlTemplate>
<Border Name="Frame"
Background="{TemplateBinding Background}"
CornerRadius="{TemplateBinding CornerRadius}">
<StackPanel Name="RootLayout">
<ContentPresenter Name="HeaderPresenter"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"/>
<atom:Skeleton IsLoading="{TemplateBinding IsLoading}" IsActive="True" IsShowParagraph="False">
<StackPanel Orientation="Horizontal" Name="ContentLayout">
<ContentPresenter Name="ValuePrefixAddOn"
Foreground="{TemplateBinding ContentForeground}"
FontSize="{TemplateBinding ContentFontSize}"
Content="{TemplateBinding ValuePrefixAddOn}"
ContentTemplate="{TemplateBinding ValuePrefixAddOnTemplate}"
IsVisible="{TemplateBinding ValuePrefixAddOn, Converter={x:Static ObjectConverters.IsNotNull}}"/>
<ContentPresenter Name="ContentPresenter"
Foreground="{TemplateBinding ContentForeground}"
FontSize="{TemplateBinding ContentFontSize}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"/>
<ContentPresenter Name="ValueSuffixAddOn"
Foreground="{TemplateBinding ContentForeground}"
FontSize="{TemplateBinding ContentFontSize}"
Content="{TemplateBinding ValueSuffixAddOn}"
ContentTemplate="{TemplateBinding ValueSuffixAddOnTemplate}"
IsVisible="{TemplateBinding ValueSuffixAddOn, Converter={x:Static ObjectConverters.IsNotNull}}"/>
</StackPanel>
</atom:Skeleton>
</StackPanel>
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^ /template/ StackPanel#RootLayout">
<Setter Property="Spacing" Value="{DynamicResource {x:Static atom:SharedTokenKey.SpacingXXS}}"/>
@@ -9,6 +9,36 @@
</ControlTheme.BasedOn>
<Setter Property="Template">
<ControlTemplate>
<Border Name="Frame"
Background="{TemplateBinding Background}"
CornerRadius="{TemplateBinding CornerRadius}">
<StackPanel Name="RootLayout">
<ContentPresenter Name="HeaderPresenter"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
IsVisible="{TemplateBinding Header, Converter={x:Static ObjectConverters.IsNotNull}}"/>
<atom:Skeleton IsLoading="{TemplateBinding IsLoading}" IsActive="True" IsShowParagraph="False">
<StackPanel Orientation="Horizontal" Name="ContentLayout">
<ContentPresenter Name="ValuePrefixAddOn"
Foreground="{TemplateBinding ContentForeground}"
FontSize="{TemplateBinding ContentFontSize}"
Content="{TemplateBinding ValuePrefixAddOn}"
ContentTemplate="{TemplateBinding ValuePrefixAddOnTemplate}"
IsVisible="{TemplateBinding ValuePrefixAddOn, Converter={x:Static ObjectConverters.IsNotNull}}"/>
<ContentPresenter Name="ContentPresenter"
Foreground="{TemplateBinding ContentForeground}"
FontSize="{TemplateBinding ContentFontSize}"
Content="{TemplateBinding RemainingTimeText}"/>
<ContentPresenter Name="ValueSuffixAddOn"
Foreground="{TemplateBinding ContentForeground}"
FontSize="{TemplateBinding ContentFontSize}"
Content="{TemplateBinding ValueSuffixAddOn}"
ContentTemplate="{TemplateBinding ValueSuffixAddOnTemplate}"
IsVisible="{TemplateBinding ValueSuffixAddOn, Converter={x:Static ObjectConverters.IsNotNull}}"/>
</StackPanel>
</atom:Skeleton>
</StackPanel>
</Border>
</ControlTemplate>
</Setter>
</ControlTheme>
@@ -1,4 +1,5 @@
using Avalonia;
using Avalonia.Threading;
namespace AtomUI.Desktop.Controls;
@@ -6,14 +7,163 @@ public class TimerStatistic : AbstractStatistic
{
#region 公共属性定义
public static readonly StyledProperty<double> ValueProperty =
AvaloniaProperty.Register<AbstractStatistic, double>(nameof(Value));
public static readonly StyledProperty<DateTime> ValueProperty =
AvaloniaProperty.Register<AbstractStatistic, DateTime>(nameof(Value));
public double Value
public static readonly StyledProperty<string?> FormatProperty =
AvaloniaProperty.Register<AbstractStatistic, string?>(nameof(Format));
public static readonly StyledProperty<TimeSpan> RefreshDurationProperty =
AvaloniaProperty.Register<AbstractStatistic, TimeSpan>(nameof(RefreshDuration), TimeSpan.FromMilliseconds(10));
public DateTime Value
{
get => GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
public string? Format
{
get => GetValue(FormatProperty);
set => SetValue(FormatProperty, value);
}
public TimeSpan RefreshDuration
{
get => GetValue(RefreshDurationProperty);
set => SetValue(RefreshDurationProperty, value);
}
public Func<TimeSpan, string>? Formatter;
#endregion
#region 公共事件定义
public event EventHandler? CountdownFinished;
#endregion
#region 内部属性定义
internal static readonly DirectProperty<TimerStatistic, TimeSpan> RemainingTimeProperty =
AvaloniaProperty.RegisterDirect<TimerStatistic, TimeSpan>(nameof(RemainingTime),
o => o.RemainingTime,
(o, v) => o.RemainingTime = v);
internal static readonly DirectProperty<TimerStatistic, string?> RemainingTimeTextProperty =
AvaloniaProperty.RegisterDirect<TimerStatistic, string?>(nameof(RemainingTimeText),
o => o.RemainingTimeText,
(o, v) => o.RemainingTimeText = v);
private TimeSpan _remainingTime;
internal TimeSpan RemainingTime
{
get => _remainingTime;
set => SetAndRaise(RemainingTimeProperty, ref _remainingTime, value);
}
private string? _remainingTimeText;
internal string? RemainingTimeText
{
get => _remainingTimeText;
set => SetAndRaise(RemainingTimeTextProperty, ref _remainingTimeText, value);
}
#endregion
private DispatcherTimer? _timer;
static TimerStatistic()
{
AffectsMeasure<TimerStatistic>(ValueProperty, FormatProperty, RemainingTimeTextProperty);
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
_timer?.Start();
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
_timer?.Stop();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == RemainingTimeProperty ||
change.Property == FormatProperty)
{
GenerateRemainingTimeText();
}
if (change.Property == ValueProperty ||
change.Property == RefreshDurationProperty)
{
BuildTimer(true);
}
}
private void GenerateRemainingTimeText()
{
string? formattedText = null;
if (Formatter != null)
{
formattedText = Formatter(RemainingTime);
}
else
{
if (Format != null)
{
formattedText = RemainingTime.ToString(Format);
}
else if (Format == @"hh\:mm\:ss")
{
formattedText = $"{(int)RemainingTime.TotalHours:00}:{RemainingTime.Minutes:00}:{RemainingTime.Seconds:00}";
}
else if (Format == @"hh\:mm\:ss\.fff")
{
formattedText = $"{(int)RemainingTime.TotalHours:00}:{RemainingTime.Minutes:00}:{RemainingTime.Seconds:00}.{RemainingTime.Milliseconds:000}";
}
else
{
formattedText = $"{(int)RemainingTime.TotalHours:00}:{RemainingTime.Minutes:00}:{RemainingTime.Seconds:00}";
}
}
SetCurrentValue(RemainingTimeTextProperty, formattedText);
}
private void BuildTimer(bool start)
{
_timer = new DispatcherTimer();
_timer.Interval = RefreshDuration;
_timer.Tick += HandleTickElapsed;
if (start)
{
_timer.Start();
}
}
private void HandleTickElapsed(object? sender, EventArgs args)
{
if (Value > DateTime.Now)
{
RemainingTime = Value - DateTime.Now;
}
else
{
RemainingTime = DateTime.Now - Value;
}
if (RemainingTime <= TimeSpan.Zero)
{
RemainingTime = TimeSpan.Zero;
_timer?.Stop();
CountdownFinished?.Invoke(this, EventArgs.Empty);
}
}
}