Merge pull request #259 from irihitech/clock

Clock: Enable smooth second hand movement.
This commit is contained in:
Dong Bin
2024-06-15 19:59:34 +08:00
committed by GitHub
2 changed files with 105 additions and 43 deletions

View File

@@ -10,9 +10,9 @@
x:CompileBindings="True" x:CompileBindings="True"
d:DesignHeight="450" d:DesignHeight="450"
d:DesignWidth="800" d:DesignWidth="800"
mc:Ignorable="d"> mc:Ignorable="d">
<Grid> <Grid RowDefinitions="Auto, *">
<u:Clock HorizontalAlignment="Left" Time="{Binding Time}"></u:Clock> <ToggleSwitch Content="Smooth" Name="smooth"></ToggleSwitch>
<u:Clock Grid.Row="1" HorizontalAlignment="Left" IsSmooth="{Binding #smooth.IsChecked}" Time="{Binding Time}"></u:Clock>
</Grid> </Grid>
</UserControl> </UserControl>

View File

@@ -1,131 +1,193 @@
using Avalonia; using Avalonia;
using Avalonia.Animation;
using Avalonia.Controls.Metadata; using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Styling;
namespace Ursa.Controls; namespace Ursa.Controls;
[TemplatePart(PART_ClockTicks, typeof(ClockTicks))] [TemplatePart(PART_ClockTicks, typeof(ClockTicks))]
public class Clock: TemplatedControl public class Clock : TemplatedControl
{ {
public const string PART_ClockTicks = "PART_ClockTicks"; public const string PART_ClockTicks = "PART_ClockTicks";
public static readonly StyledProperty<DateTime> TimeProperty = AvaloniaProperty.Register<Clock, DateTime>( public static readonly StyledProperty<DateTime> TimeProperty = AvaloniaProperty.Register<Clock, DateTime>(
nameof(Time), defaultBindingMode: BindingMode.TwoWay); nameof(Time), defaultBindingMode: BindingMode.TwoWay);
public static readonly StyledProperty<bool> ShowHourTicksProperty =
ClockTicks.ShowHourTicksProperty.AddOwner<Clock>();
public static readonly StyledProperty<bool> ShowMinuteTicksProperty =
ClockTicks.ShowMinuteTicksProperty.AddOwner<Clock>();
public static readonly StyledProperty<IBrush?> HandBrushProperty = AvaloniaProperty.Register<Clock, IBrush?>(
nameof(HandBrush));
public static readonly StyledProperty<bool> ShowHourHandProperty = AvaloniaProperty.Register<Clock, bool>(
nameof(ShowHourHand), true);
public static readonly StyledProperty<bool> ShowMinuteHandProperty = AvaloniaProperty.Register<Clock, bool>(
nameof(ShowMinuteHand), true);
public static readonly StyledProperty<bool> ShowSecondHandProperty = AvaloniaProperty.Register<Clock, bool>(
nameof(ShowSecondHand), true);
public static readonly StyledProperty<bool> IsSmoothProperty = AvaloniaProperty.Register<Clock, bool>(
nameof(IsSmooth));
public static readonly DirectProperty<Clock, double> HourAngleProperty =
AvaloniaProperty.RegisterDirect<Clock, double>(
nameof(HourAngle), o => o.HourAngle);
public static readonly DirectProperty<Clock, double> MinuteAngleProperty =
AvaloniaProperty.RegisterDirect<Clock, double>(
nameof(MinuteAngle), o => o.MinuteAngle);
public static readonly DirectProperty<Clock, double> SecondAngleProperty =
AvaloniaProperty.RegisterDirect<Clock, double>(
nameof(SecondAngle), o => o.SecondAngle, (o, v) => o.SecondAngle = v);
private double _hourAngle;
private double _minuteAngle;
private double _secondAngle;
static Clock()
{
TimeProperty.Changed.AddClassHandler<Clock, DateTime>((clock, args) => clock.OnTimeChanged(args));
IsSmoothProperty.Changed.AddClassHandler<Clock, bool>((clock, args) => clock.OnIsSmoothChanged(args));
}
private void OnIsSmoothChanged(AvaloniaPropertyChangedEventArgs<bool> args)
{
if (args.NewValue.Value && !_cts.IsCancellationRequested )
{
_cts.Cancel();
}
}
public DateTime Time public DateTime Time
{ {
get => GetValue(TimeProperty); get => GetValue(TimeProperty);
set => SetValue(TimeProperty, value); set => SetValue(TimeProperty, value);
} }
public static readonly StyledProperty<bool> ShowHourTicksProperty =
ClockTicks.ShowHourTicksProperty.AddOwner<Clock>();
public bool ShowHourTicks public bool ShowHourTicks
{ {
get => GetValue(ShowHourTicksProperty); get => GetValue(ShowHourTicksProperty);
set => SetValue(ShowHourTicksProperty, value); set => SetValue(ShowHourTicksProperty, value);
} }
public static readonly StyledProperty<bool> ShowMinuteTicksProperty =
ClockTicks.ShowMinuteTicksProperty.AddOwner<Clock>();
public bool ShowMinuteTicks public bool ShowMinuteTicks
{ {
get => GetValue(ShowMinuteTicksProperty); get => GetValue(ShowMinuteTicksProperty);
set => SetValue(ShowMinuteTicksProperty, value); set => SetValue(ShowMinuteTicksProperty, value);
} }
public static readonly StyledProperty<IBrush?> HandBrushProperty = AvaloniaProperty.Register<Clock, IBrush?>(
nameof(HandBrush));
public IBrush? HandBrush public IBrush? HandBrush
{ {
get => GetValue(HandBrushProperty); get => GetValue(HandBrushProperty);
set => SetValue(HandBrushProperty, value); set => SetValue(HandBrushProperty, value);
} }
public static readonly StyledProperty<bool> ShowHourHandProperty = AvaloniaProperty.Register<Clock, bool>(
nameof(ShowHourHand), defaultValue: true);
public bool ShowHourHand public bool ShowHourHand
{ {
get => GetValue(ShowHourHandProperty); get => GetValue(ShowHourHandProperty);
set => SetValue(ShowHourHandProperty, value); set => SetValue(ShowHourHandProperty, value);
} }
public static readonly StyledProperty<bool> ShowMinuteHandProperty = AvaloniaProperty.Register<Clock, bool>(
nameof(ShowMinuteHand), defaultValue: true);
public bool ShowMinuteHand public bool ShowMinuteHand
{ {
get => GetValue(ShowMinuteHandProperty); get => GetValue(ShowMinuteHandProperty);
set => SetValue(ShowMinuteHandProperty, value); set => SetValue(ShowMinuteHandProperty, value);
} }
public static readonly StyledProperty<bool> ShowSecondHandProperty = AvaloniaProperty.Register<Clock, bool>(
nameof(ShowSecondHand), defaultValue: true);
public bool ShowSecondHand public bool ShowSecondHand
{ {
get => GetValue(ShowSecondHandProperty); get => GetValue(ShowSecondHandProperty);
set => SetValue(ShowSecondHandProperty, value); set => SetValue(ShowSecondHandProperty, value);
} }
public bool IsSmooth
{
get => GetValue(IsSmoothProperty);
set => SetValue(IsSmoothProperty, value);
}
public static readonly DirectProperty<Clock, double> HourAngleProperty = AvaloniaProperty.RegisterDirect<Clock, double>(
nameof(HourAngle), o => o.HourAngle);
private double _hourAngle;
public double HourAngle public double HourAngle
{ {
get => _hourAngle; get => _hourAngle;
private set => SetAndRaise(HourAngleProperty, ref _hourAngle, value); private set => SetAndRaise(HourAngleProperty, ref _hourAngle, value);
} }
public static readonly DirectProperty<Clock, double> MinuteAngleProperty = AvaloniaProperty.RegisterDirect<Clock, double>(
nameof(MinuteAngle), o => o.MinuteAngle);
private double _minuteAngle;
public double MinuteAngle public double MinuteAngle
{ {
get => _minuteAngle; get => _minuteAngle;
private set => SetAndRaise(MinuteAngleProperty, ref _minuteAngle, value); private set => SetAndRaise(MinuteAngleProperty, ref _minuteAngle, value);
} }
public static readonly DirectProperty<Clock, double> SecondAngleProperty = AvaloniaProperty.RegisterDirect<Clock, double>(
nameof(SecondAngle), o => o.SecondAngle);
private double _secondAngle;
public double SecondAngle public double SecondAngle
{ {
get => _secondAngle; get => _secondAngle;
private set => SetAndRaise(SecondAngleProperty, ref _secondAngle, value); private set => SetAndRaise(SecondAngleProperty, ref _secondAngle, value);
} }
static Clock() private Animation _secondsAnimation = new Animation()
{ {
TimeProperty.Changed.AddClassHandler<Clock, DateTime>((clock, args)=>clock.OnTimeChanged(args)); FillMode = FillMode.Forward,
Duration = TimeSpan.FromSeconds(1),
Children =
{
new KeyFrame
{
Cue = new Cue(0.0),
Setters = { new Setter { Property = SecondAngleProperty } }
},
new KeyFrame
{
Cue = new Cue(1.0),
Setters = { new Setter { Property = SecondAngleProperty } }
} }
}
};
private void OnTimeChanged(AvaloniaPropertyChangedEventArgs<DateTime> args) private CancellationTokenSource _cts = new CancellationTokenSource();
private async void OnTimeChanged(AvaloniaPropertyChangedEventArgs<DateTime> args)
{ {
DateTime time = args.NewValue.Value; var oldSeconds = args.OldValue.Value.Second;
var time = args.NewValue.Value;
var hour = time.Hour; var hour = time.Hour;
var minute = time.Minute; var minute = time.Minute;
var second = time.Second; var second = time.Second;
var hourAngle = 360.0 / 12 * hour + 360.0 / 12 / 60 * minute; var hourAngle = 360.0 / 12 * hour + 360.0 / 12 / 60 * minute;
var minuteAngle = 360.0 / 60 * minute + 360.0 / 60 / 60 * second; var minuteAngle = 360.0 / 60 * minute + 360.0 / 60 / 60 * second;
if (second == 0) second = 60;
var oldSecondAngle = 360.0 / 60 * oldSeconds;
var secondAngle = 360.0 / 60 * second; var secondAngle = 360.0 / 60 * second;
HourAngle = hourAngle; HourAngle = hourAngle;
MinuteAngle = minuteAngle; MinuteAngle = minuteAngle;
if (!IsLoaded || !IsSmooth)
{
SecondAngle = secondAngle; SecondAngle = secondAngle;
} }
else
{
_cts.Cancel();
_cts.Dispose();
_cts = new CancellationTokenSource();
(_secondsAnimation.Children[0].Setters[0] as Setter).Value = oldSecondAngle;
(_secondsAnimation.Children[1].Setters[0] as Setter).Value = secondAngle;
_secondsAnimation.RunAsync(this, _cts.Token);
}
}
protected override Size MeasureOverride(Size availableSize) protected override Size MeasureOverride(Size availableSize)
{ {
double min = Math.Min(availableSize.Height, availableSize.Width); var min = Math.Min(availableSize.Height, availableSize.Width);
var newSize = new Size(min, min); var newSize = new Size(min, min);
var size = base.MeasureOverride(newSize); var size = base.MeasureOverride(newSize);
return size; return size;
@@ -133,7 +195,7 @@ public class Clock: TemplatedControl
protected override Size ArrangeOverride(Size finalSize) protected override Size ArrangeOverride(Size finalSize)
{ {
double min = Math.Min(finalSize.Height, finalSize.Width); var min = Math.Min(finalSize.Height, finalSize.Width);
var newSize = new Size(min, min); var newSize = new Size(min, min);
var size = base.ArrangeOverride(newSize); var size = base.ArrangeOverride(newSize);
return size; return size;