From c48a63422024ae860685c18f754d9c7683107e76 Mon Sep 17 00:00:00 2001 From: NaBian <836904362@qq.com> Date: Sat, 20 Jun 2026 16:21:22 +0800 Subject: [PATCH] feat: Add ProgressButton control and demo. --- .../Data/DemoInfo.json | 1 + .../Controls/ProgressButtonDemo.axaml | 81 +++++++++ .../Controls/ProgressButtonDemo.axaml.cs | 114 +++++++++++++ .../Controls/Button/ProgressButton.cs | 26 +++ .../Controls/Loading/LoadingCircle.cs | 40 ++--- .../Themes/Styles/MergedStyles.axaml | 1 + .../Themes/Styles/ProgressButton.axaml | 158 ++++++++++++++++++ .../Themes/Styles/ToggleButton.axaml | 125 ++++++++++++++ 8 files changed, 526 insertions(+), 20 deletions(-) create mode 100644 src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/ProgressButtonDemo.axaml create mode 100644 src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/ProgressButtonDemo.axaml.cs create mode 100644 src/Avalonia/HandyControl_Avalonia/Controls/Button/ProgressButton.cs create mode 100644 src/Avalonia/HandyControl_Avalonia/Themes/Styles/ProgressButton.axaml diff --git a/src/Avalonia/HandyControlDemo_Avalonia/Data/DemoInfo.json b/src/Avalonia/HandyControlDemo_Avalonia/Data/DemoInfo.json index 85486a72..91de8154 100644 --- a/src/Avalonia/HandyControlDemo_Avalonia/Data/DemoInfo.json +++ b/src/Avalonia/HandyControlDemo_Avalonia/Data/DemoInfo.json @@ -44,6 +44,7 @@ [ "ConfettiCannon", "ConfettiCannonDemo", "Brush.Favorite", "", "Interacting" ], [ "Gravatar", "GravatarDemo", "Brush.User", "", "DataDisplay" ], [ "ButtonGroup", "ButtonGroupDemo", "Brush.ButtonGroup", "", "Button" ], + [ "ProgressButton", "ProgressButtonDemo", "Brush.ProgressBarStatusBar8", "", "Button" ], [ "Card", "CardDemo", "Brush.ContactCard", "", "DataDisplay" ], [ "ElementGroup", "ElementGroupDemo", "Brush.ButtonGroup", "", "Layout" ], [ "CirclePanel", "CirclePanelDemo", "Brush.ColorWheel", "", "Layout" ], diff --git a/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/ProgressButtonDemo.axaml b/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/ProgressButtonDemo.axaml new file mode 100644 index 00000000..97515047 --- /dev/null +++ b/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/ProgressButtonDemo.axaml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/ProgressButtonDemo.axaml.cs b/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/ProgressButtonDemo.axaml.cs new file mode 100644 index 00000000..d48770e4 --- /dev/null +++ b/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/ProgressButtonDemo.axaml.cs @@ -0,0 +1,114 @@ +using System; +using Avalonia; +using Avalonia.Threading; +using HandyControlDemo.Properties.Langs; + +namespace HandyControlDemo.UserControl; + +public partial class ProgressButtonDemo : Avalonia.Controls.UserControl +{ + private readonly DispatcherTimer _timer; + + private int _progress; + private bool _isUploading; + + public static readonly DirectProperty ProgressProperty = + AvaloniaProperty.RegisterDirect(nameof(Progress), o => o.Progress, (o, v) => o.Progress = v); + + public static readonly DirectProperty IsUploadingProperty = + AvaloniaProperty.RegisterDirect(nameof(IsUploading), o => o.IsUploading, (o, v) => o.IsUploading = v); + + public static readonly DirectProperty UploadContentProperty = + AvaloniaProperty.RegisterDirect(nameof(UploadContent), o => o.UploadContent); + + public ProgressButtonDemo() + { + InitializeComponent(); + + DataContext = this; + + _timer = new DispatcherTimer + { + Interval = TimeSpan.FromMilliseconds(200) + }; + _timer.Tick += Timer_Tick; + } + + public int Progress + { + get => _progress; + set + { + if (_progress == value) + { + return; + } + + var oldValue = _progress; + _progress = value; + RaisePropertyChanged(ProgressProperty, oldValue, value); + } + } + + public bool IsUploading + { + get => _isUploading; + set + { + if (_isUploading == value) + { + return; + } + + var oldValue = _isUploading; + var oldUploadContent = UploadContent; + _isUploading = value; + RaisePropertyChanged(IsUploadingProperty, oldValue, value); + RaisePropertyChanged(UploadContentProperty, oldUploadContent, UploadContent); + + if (_isUploading) + { + if (!_timer.IsEnabled) + { + _timer.Start(); + } + } + else + { + if (_timer.IsEnabled) + { + _timer.Stop(); + } + + if (Progress != 0) + { + Progress = 0; + } + } + } + } + + public string UploadContent + { + get + { + var values = Lang.UploadStr.Split(';'); + if (values.Length < 2) + { + return Lang.UploadStr; + } + + return (IsUploading ? values[1] : values[0]).Trim(); + } + } + + private void Timer_Tick(object? sender, EventArgs e) + { + Progress++; + if (Progress >= 100) + { + Progress = 0; + IsUploading = false; + } + } +} diff --git a/src/Avalonia/HandyControl_Avalonia/Controls/Button/ProgressButton.cs b/src/Avalonia/HandyControl_Avalonia/Controls/Button/ProgressButton.cs new file mode 100644 index 00000000..072b00f8 --- /dev/null +++ b/src/Avalonia/HandyControl_Avalonia/Controls/Button/ProgressButton.cs @@ -0,0 +1,26 @@ +using Avalonia; +using Avalonia.Controls.Primitives; +using Avalonia.Styling; + +namespace HandyControl.Controls; + +public class ProgressButton : ToggleButton +{ + public static readonly StyledProperty ProgressThemeProperty = + AvaloniaProperty.Register(nameof(ProgressTheme)); + + public static readonly StyledProperty ProgressProperty = + AvaloniaProperty.Register(nameof(Progress)); + + public ControlTheme? ProgressTheme + { + get => GetValue(ProgressThemeProperty); + set => SetValue(ProgressThemeProperty, value); + } + + public double Progress + { + get => GetValue(ProgressProperty); + set => SetValue(ProgressProperty, value); + } +} \ No newline at end of file diff --git a/src/Avalonia/HandyControl_Avalonia/Controls/Loading/LoadingCircle.cs b/src/Avalonia/HandyControl_Avalonia/Controls/Loading/LoadingCircle.cs index 42570e16..2a3e4a9b 100644 --- a/src/Avalonia/HandyControl_Avalonia/Controls/Loading/LoadingCircle.cs +++ b/src/Avalonia/HandyControl_Avalonia/Controls/Loading/LoadingCircle.cs @@ -58,6 +58,8 @@ public class LoadingCircle : LoadingBase var dotDelayTime = DotDelayTime; var needHidden = NeedHidden; var dotOffset = DotOffSet; + var totalSeconds = dotSpeed + 0.4d; + var visibleCue = dotSpeed / totalSeconds; if (dotCount < 1) return; PrivateCanvas.Children.Clear(); @@ -73,20 +75,21 @@ public class LoadingCircle : LoadingBase var rotateAnimation = new Animation { - Duration = TimeSpan.FromSeconds(dotSpeed), + Duration = TimeSpan.FromSeconds(totalSeconds), Delay = TimeSpan.FromMilliseconds(dotDelayTime * i), IterationCount = IterationCount.Infinite, FillMode = FillMode.Forward, Easing = new LinearEasing(), Children = { - BuildAngleFrame(0d / 7d, 0 + subAngle, null), - BuildAngleFrame(0.75d / 7d, 180 + subAngle, EaseOutSpline), - BuildAngleFrame(2.75d / 7d, 180 + dotOffset + subAngle, null), - BuildAngleFrame(3.5d / 7d, 360 + subAngle, EaseInSpline), - BuildAngleFrame(4.25d / 7d, 540 + subAngle, EaseOutSpline), - BuildAngleFrame(6.25d / 7d, 540 + dotOffset + subAngle, null), - BuildAngleFrame(1d, 720 + subAngle, EaseInSpline), + BuildAngleFrame(0d, 0 + subAngle, null), + BuildAngleFrame((dotSpeed * (0.75d / 7d)) / totalSeconds, 180 + subAngle, EaseOutSpline), + BuildAngleFrame((dotSpeed * (2.75d / 7d)) / totalSeconds, 180 + dotOffset + subAngle, null), + BuildAngleFrame((dotSpeed * (3.5d / 7d)) / totalSeconds, 360 + subAngle, EaseInSpline), + BuildAngleFrame((dotSpeed * (4.25d / 7d)) / totalSeconds, 540 + subAngle, EaseOutSpline), + BuildAngleFrame((dotSpeed * (6.25d / 7d)) / totalSeconds, 540 + dotOffset + subAngle, null), + BuildAngleFrame(visibleCue, 720 + subAngle, EaseInSpline), + BuildAngleFrame(1d, 720 + subAngle, null), } }; @@ -96,10 +99,7 @@ public class LoadingCircle : LoadingBase if (needHidden) { - var totalSeconds = dotSpeed + 0.4d; - var visibleCue = dotSpeed / totalSeconds; - - var opacityAnimation = new Animation + var visibleAnimation = new Animation { Duration = TimeSpan.FromSeconds(totalSeconds), Delay = TimeSpan.FromMilliseconds(dotDelayTime * i), @@ -111,29 +111,29 @@ public class LoadingCircle : LoadingBase new KeyFrame { Cue = new Cue(0d), - Setters = { new Setter(Visual.OpacityProperty, 1d) } + Setters = { new Setter(Visual.IsVisibleProperty, true) } }, new KeyFrame { Cue = new Cue(Math.Max(0d, visibleCue - 0.0001d)), - Setters = { new Setter(Visual.OpacityProperty, 1d) } + Setters = { new Setter(Visual.IsVisibleProperty, true) } }, new KeyFrame { Cue = new Cue(visibleCue), - Setters = { new Setter(Visual.OpacityProperty, 0d) } + Setters = { new Setter(Visual.IsVisibleProperty, false) } }, new KeyFrame { Cue = new Cue(1d), - Setters = { new Setter(Visual.OpacityProperty, 0d) } + Setters = { new Setter(Visual.IsVisibleProperty, false) } } } }; - var opacityCts = new CancellationTokenSource(); - AnimationTokens.Add(opacityCts); - _ = opacityAnimation.RunAsync(border, opacityCts.Token); + var visibleCts = new CancellationTokenSource(); + AnimationTokens.Add(visibleCts); + _ = visibleAnimation.RunAsync(border, visibleCts.Token); } } } @@ -166,7 +166,7 @@ public class LoadingCircle : LoadingBase Child = ellipse, RenderTransformOrigin = RelativePoint.Center, RenderTransform = rotate, - Opacity = needHidden ? 0d : 1d + IsVisible = !needHidden }; border.Bind(WidthProperty, new Binding(nameof(Width)) { Source = this }); border.Bind(HeightProperty, new Binding(nameof(Height)) { Source = this }); diff --git a/src/Avalonia/HandyControl_Avalonia/Themes/Styles/MergedStyles.axaml b/src/Avalonia/HandyControl_Avalonia/Themes/Styles/MergedStyles.axaml index dab03312..16d30524 100644 --- a/src/Avalonia/HandyControl_Avalonia/Themes/Styles/MergedStyles.axaml +++ b/src/Avalonia/HandyControl_Avalonia/Themes/Styles/MergedStyles.axaml @@ -34,6 +34,7 @@ + diff --git a/src/Avalonia/HandyControl_Avalonia/Themes/Styles/ProgressButton.axaml b/src/Avalonia/HandyControl_Avalonia/Themes/Styles/ProgressButton.axaml new file mode 100644 index 00000000..c2871968 --- /dev/null +++ b/src/Avalonia/HandyControl_Avalonia/Themes/Styles/ProgressButton.axaml @@ -0,0 +1,158 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/HandyControl_Avalonia/Themes/Styles/ToggleButton.axaml b/src/Avalonia/HandyControl_Avalonia/Themes/Styles/ToggleButton.axaml index 8bf5021b..d6c8d5b0 100644 --- a/src/Avalonia/HandyControl_Avalonia/Themes/Styles/ToggleButton.axaml +++ b/src/Avalonia/HandyControl_Avalonia/Themes/Styles/ToggleButton.axaml @@ -895,4 +895,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +