TimePicker

This commit is contained in:
2026-01-16 05:14:14 +00:00
parent 870382097b
commit 675466a0f5
2 changed files with 190 additions and 106 deletions

View File

@@ -51,11 +51,11 @@ public partial class TimePickerHandler : ViewHandler<ITimePicker, SkiaTimePicker
// Apply dark theme colors if needed // Apply dark theme colors if needed
if (Application.Current?.UserAppTheme == AppTheme.Dark) if (Application.Current?.UserAppTheme == AppTheme.Dark)
{ {
platformView.ClockBackgroundColor = new SKColor(30, 30, 30); platformView.ClockBackgroundColor = Color.FromRgb(30, 30, 30);
platformView.ClockFaceColor = new SKColor(45, 45, 45); platformView.ClockFaceColor = Color.FromRgb(45, 45, 45);
platformView.TextColor = new SKColor(224, 224, 224); platformView.TextColor = Color.FromRgb(224, 224, 224);
platformView.BorderColor = new SKColor(97, 97, 97); platformView.BorderColor = Color.FromRgb(97, 97, 97);
platformView.BackgroundColor = new SKColor(45, 45, 45); platformView.BackgroundColor = Color.FromRgb(45, 45, 45).ToSKColor();
} }
} }
@@ -65,11 +65,11 @@ public partial class TimePickerHandler : ViewHandler<ITimePicker, SkiaTimePicker
base.DisconnectHandler(platformView); base.DisconnectHandler(platformView);
} }
private void OnTimeSelected(object? sender, EventArgs e) private void OnTimeSelected(object? sender, TimeChangedEventArgs e)
{ {
if (VirtualView is null || PlatformView is null) return; if (VirtualView is null || PlatformView is null) return;
VirtualView.Time = PlatformView.Time; VirtualView.Time = e.NewTime;
} }
public static void MapTime(TimePickerHandler handler, ITimePicker timePicker) public static void MapTime(TimePickerHandler handler, ITimePicker timePicker)
@@ -89,7 +89,7 @@ public partial class TimePickerHandler : ViewHandler<ITimePicker, SkiaTimePicker
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
if (timePicker.TextColor is not null) if (timePicker.TextColor is not null)
{ {
handler.PlatformView.TextColor = timePicker.TextColor.ToSKColor(); handler.PlatformView.TextColor = timePicker.TextColor;
} }
} }

View File

@@ -1,56 +1,73 @@
// Licensed to the .NET Foundation under one or more agreements. // Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license. // The .NET Foundation licenses this file to you under the MIT license.
using SkiaSharp; using Microsoft.Maui.Graphics;
using Microsoft.Maui.Platform.Linux; using Microsoft.Maui.Platform.Linux;
using SkiaSharp;
namespace Microsoft.Maui.Platform; namespace Microsoft.Maui.Platform;
/// <summary>
/// Event args for time picker time changes.
/// </summary>
public class TimeChangedEventArgs : EventArgs
{
public TimeSpan OldTime { get; }
public TimeSpan NewTime { get; }
public TimeChangedEventArgs(TimeSpan oldTime, TimeSpan newTime)
{
OldTime = oldTime;
NewTime = newTime;
}
}
/// <summary> /// <summary>
/// Skia-rendered time picker control with clock popup. /// Skia-rendered time picker control with clock popup.
/// Implements MAUI ITimePicker interface patterns.
/// </summary> /// </summary>
public class SkiaTimePicker : SkiaView public class SkiaTimePicker : SkiaView
{ {
#region BindableProperties #region BindableProperties
public static readonly BindableProperty TimeProperty = public static readonly BindableProperty TimeProperty =
BindableProperty.Create(nameof(Time), typeof(TimeSpan), typeof(SkiaTimePicker), DateTime.Now.TimeOfDay, BindingMode.OneWay, BindableProperty.Create(nameof(Time), typeof(TimeSpan), typeof(SkiaTimePicker), DateTime.Now.TimeOfDay, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaTimePicker)b).OnTimePropertyChanged()); propertyChanged: (b, o, n) => ((SkiaTimePicker)b).OnTimePropertyChanged((TimeSpan)o, (TimeSpan)n));
public static readonly BindableProperty FormatProperty = public static readonly BindableProperty FormatProperty =
BindableProperty.Create(nameof(Format), typeof(string), typeof(SkiaTimePicker), "t", BindingMode.TwoWay, BindableProperty.Create(nameof(Format), typeof(string), typeof(SkiaTimePicker), "t", BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate()); propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate());
public static readonly BindableProperty TextColorProperty = public static readonly BindableProperty TextColorProperty =
BindableProperty.Create(nameof(TextColor), typeof(SKColor), typeof(SkiaTimePicker), SKColors.Black, BindingMode.TwoWay, BindableProperty.Create(nameof(TextColor), typeof(Color), typeof(SkiaTimePicker), Colors.Black, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate()); propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate());
public static readonly BindableProperty BorderColorProperty = public static readonly BindableProperty BorderColorProperty =
BindableProperty.Create(nameof(BorderColor), typeof(SKColor), typeof(SkiaTimePicker), new SKColor(0xBD, 0xBD, 0xBD), BindingMode.TwoWay, BindableProperty.Create(nameof(BorderColor), typeof(Color), typeof(SkiaTimePicker), Color.FromRgb(189, 189, 189), BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate()); propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate());
public static readonly BindableProperty ClockBackgroundColorProperty = public static readonly BindableProperty ClockBackgroundColorProperty =
BindableProperty.Create(nameof(ClockBackgroundColor), typeof(SKColor), typeof(SkiaTimePicker), SKColors.White, BindingMode.TwoWay, BindableProperty.Create(nameof(ClockBackgroundColor), typeof(Color), typeof(SkiaTimePicker), Colors.White, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate()); propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate());
public static readonly BindableProperty ClockFaceColorProperty = public static readonly BindableProperty ClockFaceColorProperty =
BindableProperty.Create(nameof(ClockFaceColor), typeof(SKColor), typeof(SkiaTimePicker), new SKColor(0xF5, 0xF5, 0xF5), BindingMode.TwoWay, BindableProperty.Create(nameof(ClockFaceColor), typeof(Color), typeof(SkiaTimePicker), Color.FromRgb(245, 245, 245), BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate()); propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate());
public static readonly BindableProperty SelectedColorProperty = public static readonly BindableProperty SelectedColorProperty =
BindableProperty.Create(nameof(SelectedColor), typeof(SKColor), typeof(SkiaTimePicker), new SKColor(0x21, 0x96, 0xF3), BindingMode.TwoWay, BindableProperty.Create(nameof(SelectedColor), typeof(Color), typeof(SkiaTimePicker), Color.FromRgb(33, 150, 243), BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate()); propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate());
public static readonly BindableProperty HeaderColorProperty = public static readonly BindableProperty HeaderColorProperty =
BindableProperty.Create(nameof(HeaderColor), typeof(SKColor), typeof(SkiaTimePicker), new SKColor(0x21, 0x96, 0xF3), BindingMode.TwoWay, BindableProperty.Create(nameof(HeaderColor), typeof(Color), typeof(SkiaTimePicker), Color.FromRgb(33, 150, 243), BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate()); propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate());
public static readonly BindableProperty FontSizeProperty = public static readonly BindableProperty FontSizeProperty =
BindableProperty.Create(nameof(FontSize), typeof(float), typeof(SkiaTimePicker), 14f, BindingMode.TwoWay, BindableProperty.Create(nameof(FontSize), typeof(double), typeof(SkiaTimePicker), 14.0, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaTimePicker)b).InvalidateMeasure()); propertyChanged: (b, o, n) => ((SkiaTimePicker)b).InvalidateMeasure());
public static readonly BindableProperty CornerRadiusProperty = public static readonly BindableProperty CornerRadiusProperty =
BindableProperty.Create(nameof(CornerRadius), typeof(float), typeof(SkiaTimePicker), 4f, BindingMode.TwoWay, BindableProperty.Create(nameof(CornerRadius), typeof(double), typeof(SkiaTimePicker), 4.0, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate()); propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate());
#endregion #endregion
@@ -69,51 +86,51 @@ public class SkiaTimePicker : SkiaView
set => SetValue(FormatProperty, value); set => SetValue(FormatProperty, value);
} }
public SKColor TextColor public Color TextColor
{ {
get => (SKColor)GetValue(TextColorProperty); get => (Color)GetValue(TextColorProperty);
set => SetValue(TextColorProperty, value); set => SetValue(TextColorProperty, value);
} }
public SKColor BorderColor public Color BorderColor
{ {
get => (SKColor)GetValue(BorderColorProperty); get => (Color)GetValue(BorderColorProperty);
set => SetValue(BorderColorProperty, value); set => SetValue(BorderColorProperty, value);
} }
public SKColor ClockBackgroundColor public Color ClockBackgroundColor
{ {
get => (SKColor)GetValue(ClockBackgroundColorProperty); get => (Color)GetValue(ClockBackgroundColorProperty);
set => SetValue(ClockBackgroundColorProperty, value); set => SetValue(ClockBackgroundColorProperty, value);
} }
public SKColor ClockFaceColor public Color ClockFaceColor
{ {
get => (SKColor)GetValue(ClockFaceColorProperty); get => (Color)GetValue(ClockFaceColorProperty);
set => SetValue(ClockFaceColorProperty, value); set => SetValue(ClockFaceColorProperty, value);
} }
public SKColor SelectedColor public Color SelectedColor
{ {
get => (SKColor)GetValue(SelectedColorProperty); get => (Color)GetValue(SelectedColorProperty);
set => SetValue(SelectedColorProperty, value); set => SetValue(SelectedColorProperty, value);
} }
public SKColor HeaderColor public Color HeaderColor
{ {
get => (SKColor)GetValue(HeaderColorProperty); get => (Color)GetValue(HeaderColorProperty);
set => SetValue(HeaderColorProperty, value); set => SetValue(HeaderColorProperty, value);
} }
public float FontSize public double FontSize
{ {
get => (float)GetValue(FontSizeProperty); get => (double)GetValue(FontSizeProperty);
set => SetValue(FontSizeProperty, value); set => SetValue(FontSizeProperty, value);
} }
public float CornerRadius public double CornerRadius
{ {
get => (float)GetValue(CornerRadiusProperty); get => (double)GetValue(CornerRadiusProperty);
set => SetValue(CornerRadiusProperty, value); set => SetValue(CornerRadiusProperty, value);
} }
@@ -136,6 +153,8 @@ public class SkiaTimePicker : SkiaView
#endregion #endregion
#region Fields
private bool _isOpen; private bool _isOpen;
private int _selectedHour; private int _selectedHour;
private int _selectedMinute; private int _selectedMinute;
@@ -146,29 +165,71 @@ public class SkiaTimePicker : SkiaView
private const float HeaderHeight = 80; private const float HeaderHeight = 80;
private const float PopupHeight = ClockSize + HeaderHeight; private const float PopupHeight = ClockSize + HeaderHeight;
public event EventHandler? TimeSelected; #endregion
#region Events
public event EventHandler<TimeChangedEventArgs>? TimeSelected;
#endregion
#region Constructor
public SkiaTimePicker()
{
IsFocusable = true;
_selectedHour = DateTime.Now.Hour;
_selectedMinute = DateTime.Now.Minute;
}
#endregion
#region Helper Methods
/// <summary>
/// Converts a MAUI Color to SkiaSharp SKColor.
/// </summary>
private static SKColor ToSKColor(Color? color)
{
if (color == null) return SKColors.Transparent;
return new SKColor(
(byte)(color.Red * 255),
(byte)(color.Green * 255),
(byte)(color.Blue * 255),
(byte)(color.Alpha * 255));
}
/// <summary>
/// Converts a MAUI Color to SKColor with modified alpha.
/// </summary>
private static SKColor ToSKColorWithAlpha(Color? color, byte alpha)
{
if (color == null) return SKColors.Transparent;
return new SKColor(
(byte)(color.Red * 255),
(byte)(color.Green * 255),
(byte)(color.Blue * 255),
alpha);
}
/// <summary> /// <summary>
/// Gets the clock popup rectangle with edge detection applied. /// Gets the clock popup rectangle with edge detection applied.
/// </summary> /// </summary>
private SKRect GetPopupRect(SKRect pickerBounds) private SKRect GetPopupRect(SKRect pickerBounds)
{ {
// Get window dimensions for edge detection float cornerRadius = (float)CornerRadius;
var windowWidth = LinuxApplication.Current?.MainWindow?.Width ?? 800; var windowWidth = LinuxApplication.Current?.MainWindow?.Width ?? 800;
var windowHeight = LinuxApplication.Current?.MainWindow?.Height ?? 600; var windowHeight = LinuxApplication.Current?.MainWindow?.Height ?? 600;
// Calculate default position (below the picker)
var popupLeft = pickerBounds.Left; var popupLeft = pickerBounds.Left;
var popupTop = pickerBounds.Bottom + 4; var popupTop = pickerBounds.Bottom + 4;
// Edge detection: adjust horizontal position if popup would go off-screen
if (popupLeft + ClockSize > windowWidth) if (popupLeft + ClockSize > windowWidth)
{ {
popupLeft = windowWidth - ClockSize - 4; popupLeft = windowWidth - ClockSize - 4;
} }
if (popupLeft < 0) popupLeft = 4; if (popupLeft < 0) popupLeft = 4;
// Edge detection: show above if popup would go off-screen vertically
if (popupTop + PopupHeight > windowHeight) if (popupTop + PopupHeight > windowHeight)
{ {
popupTop = pickerBounds.Top - PopupHeight - 4; popupTop = pickerBounds.Top - PopupHeight - 4;
@@ -178,56 +239,53 @@ public class SkiaTimePicker : SkiaView
return new SKRect(popupLeft, popupTop, popupLeft + ClockSize, popupTop + PopupHeight); return new SKRect(popupLeft, popupTop, popupLeft + ClockSize, popupTop + PopupHeight);
} }
public SkiaTimePicker() #endregion
{
IsFocusable = true;
_selectedHour = DateTime.Now.Hour;
_selectedMinute = DateTime.Now.Minute;
}
private void OnTimePropertyChanged() #region Private Methods
private void OnTimePropertyChanged(TimeSpan oldValue, TimeSpan newValue)
{ {
_selectedHour = Time.Hours; _selectedHour = newValue.Hours;
_selectedMinute = Time.Minutes; _selectedMinute = newValue.Minutes;
TimeSelected?.Invoke(this, EventArgs.Empty); TimeSelected?.Invoke(this, new TimeChangedEventArgs(oldValue, newValue));
Invalidate(); Invalidate();
} }
private void DrawClockOverlay(SKCanvas canvas) private void DrawClockOverlay(SKCanvas canvas)
{ {
if (!_isOpen) return; if (!_isOpen) return;
// Use ScreenBounds for popup drawing (accounts for scroll offset)
DrawClockPopup(canvas, ScreenBounds); DrawClockPopup(canvas, ScreenBounds);
} }
protected override void OnDraw(SKCanvas canvas, SKRect bounds)
{
DrawPickerButton(canvas, bounds);
}
private void DrawPickerButton(SKCanvas canvas, SKRect bounds) private void DrawPickerButton(SKCanvas canvas, SKRect bounds)
{ {
float cornerRadius = (float)CornerRadius;
float fontSize = (float)FontSize;
SKColor textColor = ToSKColor(TextColor);
SKColor borderColor = ToSKColor(BorderColor);
SKColor selectedColor = ToSKColor(SelectedColor);
using var bgPaint = new SKPaint using var bgPaint = new SKPaint
{ {
Color = IsEnabled ? BackgroundColor : new SKColor(0xF5, 0xF5, 0xF5), Color = IsEnabled ? BackgroundColor : new SKColor(245, 245, 245),
Style = SKPaintStyle.Fill, Style = SKPaintStyle.Fill,
IsAntialias = true IsAntialias = true
}; };
canvas.DrawRoundRect(new SKRoundRect(bounds, CornerRadius), bgPaint); canvas.DrawRoundRect(new SKRoundRect(bounds, cornerRadius), bgPaint);
using var borderPaint = new SKPaint using var borderPaint = new SKPaint
{ {
Color = IsFocused ? SelectedColor : BorderColor, Color = IsFocused ? selectedColor : borderColor,
Style = SKPaintStyle.Stroke, Style = SKPaintStyle.Stroke,
StrokeWidth = IsFocused ? 2 : 1, StrokeWidth = IsFocused ? 2 : 1,
IsAntialias = true IsAntialias = true
}; };
canvas.DrawRoundRect(new SKRoundRect(bounds, CornerRadius), borderPaint); canvas.DrawRoundRect(new SKRoundRect(bounds, cornerRadius), borderPaint);
using var font = new SKFont(SKTypeface.Default, FontSize); using var font = new SKFont(SKTypeface.Default, fontSize);
using var textPaint = new SKPaint(font) using var textPaint = new SKPaint(font)
{ {
Color = IsEnabled ? TextColor : TextColor.WithAlpha(128), Color = IsEnabled ? textColor : textColor.WithAlpha(128),
IsAntialias = true IsAntialias = true
}; };
var timeText = DateTime.Today.Add(Time).ToString(Format); var timeText = DateTime.Today.Add(Time).ToString(Format);
@@ -240,9 +298,10 @@ public class SkiaTimePicker : SkiaView
private void DrawClockIcon(SKCanvas canvas, SKRect bounds) private void DrawClockIcon(SKCanvas canvas, SKRect bounds)
{ {
SKColor textColor = ToSKColor(TextColor);
using var paint = new SKPaint using var paint = new SKPaint
{ {
Color = IsEnabled ? TextColor : TextColor.WithAlpha(128), Color = IsEnabled ? textColor : textColor.WithAlpha(128),
Style = SKPaintStyle.Stroke, Style = SKPaintStyle.Stroke,
StrokeWidth = 1.5f, StrokeWidth = 1.5f,
IsAntialias = true IsAntialias = true
@@ -257,16 +316,17 @@ public class SkiaTimePicker : SkiaView
private void DrawClockPopup(SKCanvas canvas, SKRect bounds) private void DrawClockPopup(SKCanvas canvas, SKRect bounds)
{ {
float cornerRadius = (float)CornerRadius;
var popupRect = GetPopupRect(bounds); var popupRect = GetPopupRect(bounds);
using var shadowPaint = new SKPaint { Color = new SKColor(0, 0, 0, 40), MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 4), Style = SKPaintStyle.Fill }; using var shadowPaint = new SKPaint { Color = new SKColor(0, 0, 0, 40), MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 4), Style = SKPaintStyle.Fill };
canvas.DrawRoundRect(new SKRoundRect(new SKRect(popupRect.Left + 2, popupRect.Top + 2, popupRect.Right + 2, popupRect.Bottom + 2), CornerRadius), shadowPaint); canvas.DrawRoundRect(new SKRoundRect(new SKRect(popupRect.Left + 2, popupRect.Top + 2, popupRect.Right + 2, popupRect.Bottom + 2), cornerRadius), shadowPaint);
using var bgPaint = new SKPaint { Color = ClockBackgroundColor, Style = SKPaintStyle.Fill, IsAntialias = true }; using var bgPaint = new SKPaint { Color = ToSKColor(ClockBackgroundColor), Style = SKPaintStyle.Fill, IsAntialias = true };
canvas.DrawRoundRect(new SKRoundRect(popupRect, CornerRadius), bgPaint); canvas.DrawRoundRect(new SKRoundRect(popupRect, cornerRadius), bgPaint);
using var borderPaint = new SKPaint { Color = BorderColor, Style = SKPaintStyle.Stroke, StrokeWidth = 1, IsAntialias = true }; using var borderPaint = new SKPaint { Color = ToSKColor(BorderColor), Style = SKPaintStyle.Stroke, StrokeWidth = 1, IsAntialias = true };
canvas.DrawRoundRect(new SKRoundRect(popupRect, CornerRadius), borderPaint); canvas.DrawRoundRect(new SKRoundRect(popupRect, cornerRadius), borderPaint);
DrawTimeHeader(canvas, new SKRect(popupRect.Left, popupRect.Top, popupRect.Right, popupRect.Top + HeaderHeight)); DrawTimeHeader(canvas, new SKRect(popupRect.Left, popupRect.Top, popupRect.Right, popupRect.Top + HeaderHeight));
DrawClockFace(canvas, new SKRect(popupRect.Left, popupRect.Top + HeaderHeight, popupRect.Right, popupRect.Bottom)); DrawClockFace(canvas, new SKRect(popupRect.Left, popupRect.Top + HeaderHeight, popupRect.Right, popupRect.Bottom));
@@ -274,12 +334,13 @@ public class SkiaTimePicker : SkiaView
private void DrawTimeHeader(SKCanvas canvas, SKRect bounds) private void DrawTimeHeader(SKCanvas canvas, SKRect bounds)
{ {
using var headerPaint = new SKPaint { Color = HeaderColor, Style = SKPaintStyle.Fill }; float cornerRadius = (float)CornerRadius;
using var headerPaint = new SKPaint { Color = ToSKColor(HeaderColor), Style = SKPaintStyle.Fill };
canvas.Save(); canvas.Save();
canvas.ClipRoundRect(new SKRoundRect(new SKRect(bounds.Left, bounds.Top, bounds.Right, bounds.Top + CornerRadius * 2), CornerRadius)); canvas.ClipRoundRect(new SKRoundRect(new SKRect(bounds.Left, bounds.Top, bounds.Right, bounds.Top + cornerRadius * 2), cornerRadius));
canvas.DrawRect(bounds, headerPaint); canvas.DrawRect(bounds, headerPaint);
canvas.Restore(); canvas.Restore();
canvas.DrawRect(new SKRect(bounds.Left, bounds.Top + CornerRadius, bounds.Right, bounds.Bottom), headerPaint); canvas.DrawRect(new SKRect(bounds.Left, bounds.Top + cornerRadius, bounds.Right, bounds.Bottom), headerPaint);
using var font = new SKFont(SKTypeface.Default, 32); using var font = new SKFont(SKTypeface.Default, 32);
using var selectedPaint = new SKPaint(font) { Color = SKColors.White, IsAntialias = true }; using var selectedPaint = new SKPaint(font) { Color = SKColors.White, IsAntialias = true };
@@ -309,11 +370,15 @@ public class SkiaTimePicker : SkiaView
var centerX = bounds.MidX; var centerX = bounds.MidX;
var centerY = bounds.MidY; var centerY = bounds.MidY;
using var facePaint = new SKPaint { Color = ClockFaceColor, Style = SKPaintStyle.Fill, IsAntialias = true }; SKColor textColor = ToSKColor(TextColor);
SKColor clockFaceColor = ToSKColor(ClockFaceColor);
SKColor selectedColor = ToSKColor(SelectedColor);
using var facePaint = new SKPaint { Color = clockFaceColor, Style = SKPaintStyle.Fill, IsAntialias = true };
canvas.DrawCircle(centerX, centerY, ClockRadius + 20, facePaint); canvas.DrawCircle(centerX, centerY, ClockRadius + 20, facePaint);
using var font = new SKFont(SKTypeface.Default, 14); using var font = new SKFont(SKTypeface.Default, 14);
using var textPaint = new SKPaint(font) { Color = TextColor, IsAntialias = true }; using var textPaint = new SKPaint(font) { Color = textColor, IsAntialias = true };
if (_isSelectingHours) if (_isSelectingHours)
{ {
@@ -325,16 +390,16 @@ public class SkiaTimePicker : SkiaView
var isSelected = (_selectedHour % 12 == i % 12); var isSelected = (_selectedHour % 12 == i % 12);
if (isSelected) if (isSelected)
{ {
using var selBgPaint = new SKPaint { Color = SelectedColor, Style = SKPaintStyle.Fill, IsAntialias = true }; using var selBgPaint = new SKPaint { Color = selectedColor, Style = SKPaintStyle.Fill, IsAntialias = true };
canvas.DrawCircle(x, y, 18, selBgPaint); canvas.DrawCircle(x, y, 18, selBgPaint);
textPaint.Color = SKColors.White; textPaint.Color = SKColors.White;
} }
else textPaint.Color = TextColor; else textPaint.Color = textColor;
var textBounds = new SKRect(); var tBounds = new SKRect();
textPaint.MeasureText(i.ToString(), ref textBounds); textPaint.MeasureText(i.ToString(), ref tBounds);
canvas.DrawText(i.ToString(), x - textBounds.MidX, y - textBounds.MidY, textPaint); canvas.DrawText(i.ToString(), x - tBounds.MidX, y - tBounds.MidY, textPaint);
} }
DrawClockHand(canvas, centerX, centerY, (_selectedHour % 12) * 30 - 90, ClockRadius - 18); DrawClockHand(canvas, centerX, centerY, (_selectedHour % 12) * 30 - 90, ClockRadius - 18, selectedColor);
} }
else else
{ {
@@ -347,39 +412,46 @@ public class SkiaTimePicker : SkiaView
var isSelected = (_selectedMinute / 5 == i); var isSelected = (_selectedMinute / 5 == i);
if (isSelected) if (isSelected)
{ {
using var selBgPaint = new SKPaint { Color = SelectedColor, Style = SKPaintStyle.Fill, IsAntialias = true }; using var selBgPaint = new SKPaint { Color = selectedColor, Style = SKPaintStyle.Fill, IsAntialias = true };
canvas.DrawCircle(x, y, 18, selBgPaint); canvas.DrawCircle(x, y, 18, selBgPaint);
textPaint.Color = SKColors.White; textPaint.Color = SKColors.White;
} }
else textPaint.Color = TextColor; else textPaint.Color = textColor;
var textBounds = new SKRect(); var tBounds = new SKRect();
textPaint.MeasureText(minute.ToString("D2"), ref textBounds); textPaint.MeasureText(minute.ToString("D2"), ref tBounds);
canvas.DrawText(minute.ToString("D2"), x - textBounds.MidX, y - textBounds.MidY, textPaint); canvas.DrawText(minute.ToString("D2"), x - tBounds.MidX, y - tBounds.MidY, textPaint);
} }
DrawClockHand(canvas, centerX, centerY, _selectedMinute * 6 - 90, ClockRadius - 18); DrawClockHand(canvas, centerX, centerY, _selectedMinute * 6 - 90, ClockRadius - 18, selectedColor);
} }
} }
private void DrawClockHand(SKCanvas canvas, float centerX, float centerY, float angleDegrees, float length) private void DrawClockHand(SKCanvas canvas, float centerX, float centerY, float angleDegrees, float length, SKColor color)
{ {
var angle = angleDegrees * Math.PI / 180; var angle = angleDegrees * Math.PI / 180;
using var handPaint = new SKPaint { Color = SelectedColor, Style = SKPaintStyle.Stroke, StrokeWidth = 2, IsAntialias = true }; using var handPaint = new SKPaint { Color = color, Style = SKPaintStyle.Stroke, StrokeWidth = 2, IsAntialias = true };
canvas.DrawLine(centerX, centerY, centerX + (float)(length * Math.Cos(angle)), centerY + (float)(length * Math.Sin(angle)), handPaint); canvas.DrawLine(centerX, centerY, centerX + (float)(length * Math.Cos(angle)), centerY + (float)(length * Math.Sin(angle)), handPaint);
handPaint.Style = SKPaintStyle.Fill; handPaint.Style = SKPaintStyle.Fill;
canvas.DrawCircle(centerX, centerY, 6, handPaint); canvas.DrawCircle(centerX, centerY, 6, handPaint);
} }
#endregion
#region Overrides
protected override void OnDraw(SKCanvas canvas, SKRect bounds)
{
DrawPickerButton(canvas, bounds);
}
public override void OnPointerPressed(PointerEventArgs e) public override void OnPointerPressed(PointerEventArgs e)
{ {
if (!IsEnabled) return; if (!IsEnabled) return;
if (IsOpen) if (IsOpen)
{ {
// Use ScreenBounds for popup coordinate calculations (accounts for scroll offset)
var screenBounds = ScreenBounds; var screenBounds = ScreenBounds;
var popupRect = GetPopupRect(screenBounds); var popupRect = GetPopupRect(screenBounds);
// Check if click is in header area
var headerRect = new SKRect(popupRect.Left, popupRect.Top, popupRect.Right, popupRect.Top + HeaderHeight); var headerRect = new SKRect(popupRect.Left, popupRect.Top, popupRect.Right, popupRect.Top + HeaderHeight);
if (headerRect.Contains(e.X, e.Y)) if (headerRect.Contains(e.X, e.Y))
{ {
@@ -388,7 +460,6 @@ public class SkiaTimePicker : SkiaView
return; return;
} }
// Check if click is in clock face area
var clockCenterX = popupRect.Left + ClockSize / 2; var clockCenterX = popupRect.Left + ClockSize / 2;
var clockCenterY = popupRect.Top + HeaderHeight + ClockSize / 2; var clockCenterY = popupRect.Top + HeaderHeight + ClockSize / 2;
var dx = e.X - clockCenterX; var dx = e.X - clockCenterX;
@@ -418,7 +489,6 @@ public class SkiaTimePicker : SkiaView
return; return;
} }
// Click is outside clock - check if it's on the picker itself to toggle
if (screenBounds.Contains(e.X, e.Y)) if (screenBounds.Contains(e.X, e.Y))
{ {
IsOpen = false; IsOpen = false;
@@ -435,7 +505,6 @@ public class SkiaTimePicker : SkiaView
public override void OnFocusLost() public override void OnFocusLost()
{ {
base.OnFocusLost(); base.OnFocusLost();
// Close popup when focus is lost (clicking outside)
if (IsOpen) if (IsOpen)
{ {
IsOpen = false; IsOpen = false;
@@ -448,14 +517,34 @@ public class SkiaTimePicker : SkiaView
switch (e.Key) switch (e.Key)
{ {
case Key.Enter: case Key.Space: case Key.Enter:
if (IsOpen) { if (_isSelectingHours) _isSelectingHours = false; else { Time = new TimeSpan(_selectedHour, _selectedMinute, 0); IsOpen = false; } } case Key.Space:
if (IsOpen)
{
if (_isSelectingHours) _isSelectingHours = false;
else { Time = new TimeSpan(_selectedHour, _selectedMinute, 0); IsOpen = false; }
}
else { IsOpen = true; _isSelectingHours = true; } else { IsOpen = true; _isSelectingHours = true; }
e.Handled = true; break; e.Handled = true;
case Key.Escape: if (IsOpen) { IsOpen = false; e.Handled = true; } break; break;
case Key.Up: if (_isSelectingHours) _selectedHour = (_selectedHour + 1) % 24; else _selectedMinute = (_selectedMinute + 1) % 60; e.Handled = true; break; case Key.Escape:
case Key.Down: if (_isSelectingHours) _selectedHour = (_selectedHour - 1 + 24) % 24; else _selectedMinute = (_selectedMinute - 1 + 60) % 60; e.Handled = true; break; if (IsOpen) { IsOpen = false; e.Handled = true; }
case Key.Left: case Key.Right: _isSelectingHours = !_isSelectingHours; e.Handled = true; break; break;
case Key.Up:
if (_isSelectingHours) _selectedHour = (_selectedHour + 1) % 24;
else _selectedMinute = (_selectedMinute + 1) % 60;
e.Handled = true;
break;
case Key.Down:
if (_isSelectingHours) _selectedHour = (_selectedHour - 1 + 24) % 24;
else _selectedMinute = (_selectedMinute - 1 + 60) % 60;
e.Handled = true;
break;
case Key.Left:
case Key.Right:
_isSelectingHours = !_isSelectingHours;
e.Handled = true;
break;
} }
Invalidate(); Invalidate();
} }
@@ -465,19 +554,12 @@ public class SkiaTimePicker : SkiaView
return new SKSize(availableSize.Width < float.MaxValue ? Math.Min(availableSize.Width, 200) : 200, 40); return new SKSize(availableSize.Width < float.MaxValue ? Math.Min(availableSize.Width, 200) : 200, 40);
} }
/// <summary>
/// Override to include clock popup area in hit testing.
/// </summary>
protected override bool HitTestPopupArea(float x, float y) protected override bool HitTestPopupArea(float x, float y)
{ {
// Use ScreenBounds for hit testing (accounts for scroll offset)
var screenBounds = ScreenBounds; var screenBounds = ScreenBounds;
// Always include the picker button itself
if (screenBounds.Contains(x, y)) if (screenBounds.Contains(x, y))
return true; return true;
// When open, also include the clock popup area (with edge detection)
if (_isOpen) if (_isOpen)
{ {
var popupRect = GetPopupRect(screenBounds); var popupRect = GetPopupRect(screenBounds);
@@ -486,4 +568,6 @@ public class SkiaTimePicker : SkiaView
return false; return false;
} }
#endregion
} }