mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-21 16:14:28 +08:00
0.2.2
时钟组件的完善。
This commit is contained in:
330
LanMontainDesktop/Views/Components/AnalogClockWidget.axaml.cs
Normal file
330
LanMontainDesktop/Views/Components/AnalogClockWidget.axaml.cs
Normal file
@@ -0,0 +1,330 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Shapes;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Styling;
|
||||
using Avalonia.Threading;
|
||||
using LanMontainDesktop.Services;
|
||||
|
||||
namespace LanMontainDesktop.Views.Components;
|
||||
|
||||
public partial class AnalogClockWidget : UserControl
|
||||
{
|
||||
private readonly DispatcherTimer _timer = new()
|
||||
{
|
||||
Interval = TimeSpan.FromSeconds(1)
|
||||
};
|
||||
|
||||
private const double DialSize = 258;
|
||||
private const double Center = DialSize / 2;
|
||||
|
||||
private TimeZoneService? _timeZoneService;
|
||||
private double _currentCellSize = 48;
|
||||
private bool _dialInitialized;
|
||||
private bool _handsInitialized;
|
||||
private bool? _isNightModeApplied;
|
||||
private readonly Line _hourHandLine = CreateHandLine("#1A2A46", 12);
|
||||
private readonly Line _minuteHandLine = CreateHandLine("#29406B", 8);
|
||||
private readonly Line _secondHandLine = CreateHandLine("#1A74F2", 4);
|
||||
|
||||
public AnalogClockWidget()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_timer.Tick += OnTimerTick;
|
||||
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||
SizeChanged += OnSizeChanged;
|
||||
|
||||
InitializeDialIfNeeded();
|
||||
InitializeHandsIfNeeded();
|
||||
UpdateClock();
|
||||
}
|
||||
|
||||
public void SetTimeZoneService(TimeZoneService timeZoneService)
|
||||
{
|
||||
if (_timeZoneService is not null)
|
||||
{
|
||||
_timeZoneService.TimeZoneChanged -= OnTimeZoneChanged;
|
||||
}
|
||||
|
||||
_timeZoneService = timeZoneService;
|
||||
_timeZoneService.TimeZoneChanged += OnTimeZoneChanged;
|
||||
UpdateClock();
|
||||
}
|
||||
|
||||
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
InitializeDialIfNeeded();
|
||||
InitializeHandsIfNeeded();
|
||||
UpdateClock();
|
||||
_timer.Start();
|
||||
}
|
||||
|
||||
private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
_timer.Stop();
|
||||
}
|
||||
|
||||
private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
|
||||
{
|
||||
ApplyCellSize(_currentCellSize);
|
||||
}
|
||||
|
||||
private void OnTimerTick(object? sender, EventArgs e)
|
||||
{
|
||||
UpdateClock();
|
||||
}
|
||||
|
||||
private void OnTimeZoneChanged(object? sender, EventArgs e)
|
||||
{
|
||||
UpdateClock();
|
||||
}
|
||||
|
||||
private void InitializeDialIfNeeded()
|
||||
{
|
||||
if (_dialInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
BuildTicks(isNightMode: true);
|
||||
BuildNumbers(isNightMode: true);
|
||||
_dialInitialized = true;
|
||||
}
|
||||
|
||||
private void InitializeHandsIfNeeded()
|
||||
{
|
||||
if (_handsInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
HandsCanvas.Children.Clear();
|
||||
HandsCanvas.Children.Add(_hourHandLine);
|
||||
HandsCanvas.Children.Add(_minuteHandLine);
|
||||
HandsCanvas.Children.Add(_secondHandLine);
|
||||
_handsInitialized = true;
|
||||
}
|
||||
|
||||
private void BuildTicks(bool isNightMode)
|
||||
{
|
||||
TickCanvas.Children.Clear();
|
||||
var majorBrush = CreateBrush(isNightMode ? "#1A1A1A" : "#1E2430");
|
||||
var minorBrush = CreateBrush(isNightMode ? "#D0D0D0" : "#D7DCE5");
|
||||
var majorThickness = isNightMode ? 3.0 : 2.8;
|
||||
var minorThickness = isNightMode ? 1.4 : 1.2;
|
||||
|
||||
for (var i = 0; i < 60; i++)
|
||||
{
|
||||
var angle = (i * 6 - 90) * Math.PI / 180d;
|
||||
var isHourTick = i % 5 == 0;
|
||||
var outerRadius = Center - 7;
|
||||
var innerRadius = outerRadius - (isHourTick ? 16 : 8);
|
||||
|
||||
var x1 = Center + Math.Cos(angle) * innerRadius;
|
||||
var y1 = Center + Math.Sin(angle) * innerRadius;
|
||||
var x2 = Center + Math.Cos(angle) * outerRadius;
|
||||
var y2 = Center + Math.Sin(angle) * outerRadius;
|
||||
|
||||
var tick = new Line
|
||||
{
|
||||
StartPoint = new Point(x1, y1),
|
||||
EndPoint = new Point(x2, y2),
|
||||
Stroke = isHourTick ? majorBrush : minorBrush,
|
||||
StrokeThickness = isHourTick ? majorThickness : minorThickness,
|
||||
StrokeLineCap = PenLineCap.Round
|
||||
};
|
||||
|
||||
TickCanvas.Children.Add(tick);
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildNumbers(bool isNightMode)
|
||||
{
|
||||
NumberCanvas.Children.Clear();
|
||||
var foreground = CreateBrush(isNightMode ? "#101010" : "#0F131A");
|
||||
var fontWeight = isNightMode ? FontWeight.Bold : FontWeight.SemiBold;
|
||||
|
||||
for (var number = 1; number <= 12; number++)
|
||||
{
|
||||
var angle = (number * 30 - 90) * Math.PI / 180d;
|
||||
var radius = 88;
|
||||
var x = Center + Math.Cos(angle) * radius;
|
||||
var y = Center + Math.Sin(angle) * radius;
|
||||
var isDoubleDigit = number >= 10;
|
||||
var width = isDoubleDigit ? 44 : 28;
|
||||
var height = 34;
|
||||
|
||||
var text = new TextBlock
|
||||
{
|
||||
Text = number.ToString(CultureInfo.InvariantCulture),
|
||||
Width = width,
|
||||
Height = height,
|
||||
TextAlignment = TextAlignment.Center,
|
||||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
|
||||
FontSize = 18,
|
||||
FontWeight = fontWeight,
|
||||
Foreground = foreground
|
||||
};
|
||||
|
||||
Canvas.SetLeft(text, x - width / 2d);
|
||||
Canvas.SetTop(text, y - height / 2d);
|
||||
NumberCanvas.Children.Add(text);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateClock()
|
||||
{
|
||||
ApplyModeVisualIfNeeded();
|
||||
|
||||
var now = _timeZoneService?.GetCurrentTime() ?? DateTime.Now;
|
||||
var hourAngle = (now.Hour % 12 + now.Minute / 60d + now.Second / 3600d) * 30d;
|
||||
var minuteAngle = (now.Minute + now.Second / 60d) * 6d;
|
||||
var secondAngle = (now.Second + now.Millisecond / 1000d) * 6d;
|
||||
|
||||
SetHandGeometry(_hourHandLine, hourAngle, forwardLength: 52, backwardLength: 6);
|
||||
SetHandGeometry(_minuteHandLine, minuteAngle, forwardLength: 76, backwardLength: 8);
|
||||
SetHandGeometry(_secondHandLine, secondAngle, forwardLength: 94, backwardLength: 18);
|
||||
|
||||
var isZh = CultureInfo.CurrentCulture.TwoLetterISOLanguageName.Equals("zh", StringComparison.OrdinalIgnoreCase);
|
||||
CityTextBlock.Text = isZh ? "\u5317\u4eac" : "Beijing";
|
||||
}
|
||||
|
||||
private void ApplyModeVisualIfNeeded()
|
||||
{
|
||||
var isNightMode = ResolveIsNightMode();
|
||||
if (_isNightModeApplied.HasValue && _isNightModeApplied.Value == isNightMode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isNightModeApplied = isNightMode;
|
||||
ApplyModeVisual(isNightMode);
|
||||
}
|
||||
|
||||
private void ApplyModeVisual(bool isNightMode)
|
||||
{
|
||||
RootBorder.Background = isNightMode
|
||||
? CreateLinearGradientBrush("#1F2C4B", "#131B33")
|
||||
: CreateLinearGradientBrush("#EEF2FA", "#E7ECF6");
|
||||
|
||||
DialBorder.Background = CreateBrush(isNightMode ? "#F4F4F4" : "#FEFEFF");
|
||||
DialBorder.BorderBrush = CreateBrush(isNightMode ? "#E5E5E5" : "#DCE2EB");
|
||||
|
||||
CityTextBlock.Foreground = CreateBrush(isNightMode ? "#757575" : "#7E8593");
|
||||
CenterDotOuter.Fill = CreateBrush(isNightMode ? "#1E3C6A" : "#30486E");
|
||||
CenterDotInner.Fill = CreateBrush("#1A74F2");
|
||||
|
||||
_hourHandLine.Stroke = CreateBrush(isNightMode ? "#1A2A46" : "#2E3F5F");
|
||||
_minuteHandLine.Stroke = CreateBrush(isNightMode ? "#29406B" : "#3E557E");
|
||||
_secondHandLine.Stroke = CreateBrush("#1A74F2");
|
||||
|
||||
BuildTicks(isNightMode);
|
||||
BuildNumbers(isNightMode);
|
||||
}
|
||||
|
||||
private static void SetHandGeometry(Line hand, double angleDeg, double forwardLength, double backwardLength)
|
||||
{
|
||||
var radians = (angleDeg - 90) * Math.PI / 180d;
|
||||
var cos = Math.Cos(radians);
|
||||
var sin = Math.Sin(radians);
|
||||
|
||||
var start = new Point(
|
||||
Center - cos * backwardLength,
|
||||
Center - sin * backwardLength);
|
||||
var end = new Point(
|
||||
Center + cos * forwardLength,
|
||||
Center + sin * forwardLength);
|
||||
|
||||
hand.StartPoint = start;
|
||||
hand.EndPoint = end;
|
||||
}
|
||||
|
||||
public void ApplyCellSize(double cellSize)
|
||||
{
|
||||
_currentCellSize = Math.Max(1, cellSize);
|
||||
var scale = ResolveScale();
|
||||
|
||||
RootBorder.CornerRadius = new CornerRadius(Math.Clamp(42 * scale, 16, 56));
|
||||
RootBorder.Padding = new Thickness(Math.Clamp(14 * scale, 8, 26));
|
||||
ApplyModeVisualIfNeeded();
|
||||
}
|
||||
|
||||
private double ResolveScale()
|
||||
{
|
||||
var cellScale = Math.Clamp(_currentCellSize / 44d, 0.60, 1.90);
|
||||
var heightScale = Bounds.Height > 1 ? Math.Clamp(Bounds.Height / 300d, 0.58, 2.0) : 1;
|
||||
var widthScale = Bounds.Width > 1 ? Math.Clamp(Bounds.Width / 300d, 0.58, 2.0) : 1;
|
||||
return Math.Clamp(Math.Min(cellScale, Math.Min(heightScale, widthScale) * 1.05), 0.58, 1.95);
|
||||
}
|
||||
|
||||
private static IBrush CreateBrush(string colorHex)
|
||||
{
|
||||
return new SolidColorBrush(Color.Parse(colorHex));
|
||||
}
|
||||
|
||||
private static IBrush CreateLinearGradientBrush(string fromColorHex, string toColorHex)
|
||||
{
|
||||
return new LinearGradientBrush
|
||||
{
|
||||
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
|
||||
EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative),
|
||||
GradientStops = new GradientStops
|
||||
{
|
||||
new GradientStop(Color.Parse(fromColorHex), 0),
|
||||
new GradientStop(Color.Parse(toColorHex), 1)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static Line CreateHandLine(string strokeHex, double thickness)
|
||||
{
|
||||
return new Line
|
||||
{
|
||||
StartPoint = new Point(Center, Center),
|
||||
EndPoint = new Point(Center, Center - 40),
|
||||
Stroke = CreateBrush(strokeHex),
|
||||
StrokeThickness = thickness,
|
||||
StrokeLineCap = PenLineCap.Round
|
||||
};
|
||||
}
|
||||
|
||||
private bool ResolveIsNightMode()
|
||||
{
|
||||
if (ActualThemeVariant == ThemeVariant.Dark)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ActualThemeVariant == ThemeVariant.Light)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.TryFindResource("AdaptiveSurfaceBaseBrush", out var value) &&
|
||||
value is ISolidColorBrush solidBrush)
|
||||
{
|
||||
return CalculateRelativeLuminance(solidBrush.Color) < 0.45;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static double CalculateRelativeLuminance(Color color)
|
||||
{
|
||||
static double ToLinear(double channel)
|
||||
{
|
||||
return channel <= 0.03928
|
||||
? channel / 12.92
|
||||
: Math.Pow((channel + 0.055) / 1.055, 2.4);
|
||||
}
|
||||
|
||||
var r = ToLinear(color.R / 255d);
|
||||
var g = ToLinear(color.G / 255d);
|
||||
var b = ToLinear(color.B / 255d);
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user