feat: Add ProgressButton control and demo.

This commit is contained in:
NaBian committed 2026-06-20 16:35:26 +08:00
1 parent d50c08c629
commit c48a634220
8 files changed
+526 -20

No files matched your search

@@ -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" ],
@@ -0,0 +1,81 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:hc="https://handyorg.github.io/handycontrol"
x:Class="HandyControlDemo.UserControl.ProgressButtonDemo">
<ScrollViewer>
<WrapPanel Margin="16"
VerticalAlignment="Center">
<StackPanel Margin="16"
Spacing="0">
<hc:ProgressButton Width="180"
Margin="5"
Progress="{Binding Progress}"
IsChecked="{Binding IsUploading, Mode=TwoWay}"
Content="{Binding UploadContent}" />
<hc:ProgressButton Width="180"
Margin="5"
Theme="{StaticResource ProgressButtonPrimary}"
Progress="{Binding Progress}"
IsChecked="{Binding IsUploading, Mode=TwoWay}"
Content="{Binding UploadContent}" />
<hc:ProgressButton Width="180"
Margin="5"
Theme="{StaticResource ProgressButtonSuccess}"
Progress="{Binding Progress}"
IsChecked="{Binding IsUploading, Mode=TwoWay}"
Content="{Binding UploadContent}" />
<hc:ProgressButton Width="180"
Margin="5"
Theme="{StaticResource ProgressButtonInfo}"
Progress="{Binding Progress}"
IsChecked="{Binding IsUploading, Mode=TwoWay}"
Content="{Binding UploadContent}" />
<hc:ProgressButton Width="180"
Margin="5"
Theme="{StaticResource ProgressButtonWarning}"
Progress="{Binding Progress}"
IsChecked="{Binding IsUploading, Mode=TwoWay}"
Content="{Binding UploadContent}" />
<hc:ProgressButton Width="180"
Margin="5"
Theme="{StaticResource ProgressButtonDanger}"
Progress="{Binding Progress}"
IsChecked="{Binding IsUploading, Mode=TwoWay}"
Content="{Binding UploadContent}" />
</StackPanel>
<StackPanel Margin="16"
Spacing="0">
<ToggleButton Width="180"
Margin="5"
Theme="{StaticResource ToggleButtonLoading}"
IsChecked="{Binding IsUploading, Mode=TwoWay}"
Content="{Binding UploadContent}" />
<ToggleButton Width="180"
Margin="5"
Theme="{StaticResource ToggleButtonLoadingPrimary}"
IsChecked="{Binding IsUploading, Mode=TwoWay}"
Content="{Binding UploadContent}" />
<ToggleButton Width="180"
Margin="5"
Theme="{StaticResource ToggleButtonLoadingSuccess}"
IsChecked="{Binding IsUploading, Mode=TwoWay}"
Content="{Binding UploadContent}" />
<ToggleButton Width="180"
Margin="5"
Theme="{StaticResource ToggleButtonLoadingInfo}"
IsChecked="{Binding IsUploading, Mode=TwoWay}"
Content="{Binding UploadContent}" />
<ToggleButton Width="180"
Margin="5"
Theme="{StaticResource ToggleButtonLoadingWarning}"
IsChecked="{Binding IsUploading, Mode=TwoWay}"
Content="{Binding UploadContent}" />
<ToggleButton Width="180"
Margin="5"
Theme="{StaticResource ToggleButtonLoadingDanger}"
IsChecked="{Binding IsUploading, Mode=TwoWay}"
Content="{Binding UploadContent}" />
</StackPanel>
</WrapPanel>
</ScrollViewer>
</UserControl>
@@ -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<ProgressButtonDemo, int> ProgressProperty =
AvaloniaProperty.RegisterDirect<ProgressButtonDemo, int>(nameof(Progress), o => o.Progress, (o, v) => o.Progress = v);
public static readonly DirectProperty<ProgressButtonDemo, bool> IsUploadingProperty =
AvaloniaProperty.RegisterDirect<ProgressButtonDemo, bool>(nameof(IsUploading), o => o.IsUploading, (o, v) => o.IsUploading = v);
public static readonly DirectProperty<ProgressButtonDemo, string> UploadContentProperty =
AvaloniaProperty.RegisterDirect<ProgressButtonDemo, string>(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;
}
}
}
@@ -0,0 +1,26 @@
using Avalonia;
using Avalonia.Controls.Primitives;
using Avalonia.Styling;
namespace HandyControl.Controls;
public class ProgressButton : ToggleButton
{
public static readonly StyledProperty<ControlTheme?> ProgressThemeProperty =
AvaloniaProperty.Register<ProgressButton, ControlTheme?>(nameof(ProgressTheme));
public static readonly StyledProperty<double> ProgressProperty =
AvaloniaProperty.Register<ProgressButton, double>(nameof(Progress));
public ControlTheme? ProgressTheme
{
get => GetValue(ProgressThemeProperty);
set => SetValue(ProgressThemeProperty, value);
}
public double Progress
{
get => GetValue(ProgressProperty);
set => SetValue(ProgressProperty, value);
}
}
@@ -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 });
@@ -34,6 +34,7 @@
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/Button.axaml" />
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/SplitButton.axaml" />
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/DropDownButton.axaml" />
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/ProgressButton.axaml" />
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/HyperlinkButton.axaml" />
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/RepeatButton.axaml" />
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/ToggleButton.axaml" />
@@ -0,0 +1,158 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:ClassModifier="internal"
xmlns:hc="clr-namespace:HandyControl.Controls">
<ControlTheme x:Key="ProgressBarFlat4ProgressButton"
TargetType="ProgressBar">
<Setter Property="Background"
Value="Transparent" />
<Setter Property="hc:BorderElement.CornerRadius"
Value="{StaticResource DefaultCornerRadius}" />
<Setter Property="Foreground"
Value="{DynamicResource DarkMaskBrush}" />
<Setter Property="ShowProgressText"
Value="False" />
<Setter Property="Template">
<ControlTemplate>
<Border x:Name="PART_Track">
<Border x:Name="PART_Indicator"
HorizontalAlignment="Left">
<Border Background="{TemplateBinding Foreground}"
CornerRadius="{Binding Path=(hc:BorderElement.CornerRadius), RelativeSource={RelativeSource TemplatedParent}}" />
</Border>
</Border>
</ControlTemplate>
</Setter>
</ControlTheme>
<ControlTheme x:Key="ProgressButtonBaseStyle"
BasedOn="{StaticResource ToggleButtonBaseStyle}"
TargetType="hc:ProgressButton">
<Setter Property="Background"
Value="{DynamicResource RegionBrush}" />
<Setter Property="ProgressTheme"
Value="{StaticResource ProgressBarFlat4ProgressButton}" />
<Setter Property="Template">
<ControlTemplate>
<Border x:Name="Chrome"
CornerRadius="{TemplateBinding CornerRadius}"
BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}"
Background="{TemplateBinding Background}">
<Panel>
<ProgressBar x:Name="ProgressBar"
Maximum="100"
Value="{TemplateBinding Progress}"
Theme="{TemplateBinding ProgressTheme}"
IsVisible="False" />
<ContentPresenter x:Name="UnCheckedElement"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
RecognizesAccessKey="True"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}" />
<ContentPresenter x:Name="CheckedElement"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
RecognizesAccessKey="True"
Content="{TemplateBinding hc:StatusSwitchElement.CheckedElement}"
ContentTemplate="{TemplateBinding ContentTemplate}"
IsVisible="False" />
</Panel>
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^:pointerover">
<Setter Property="Opacity"
Value="0.9" />
</Style>
<Style Selector="^:pressed">
<Setter Property="Opacity"
Value="0.6" />
</Style>
<Style Selector="^:checked /template/ ProgressBar#ProgressBar">
<Setter Property="IsVisible"
Value="True" />
</Style>
<Style Selector="^:checked /template/ ContentPresenter#CheckedElement">
<Setter Property="IsVisible"
Value="True" />
</Style>
<Style Selector="^[(hc|StatusSwitchElement.HideUncheckedElement)=True]:checked /template/ ContentPresenter#UnCheckedElement">
<Setter Property="IsVisible"
Value="False" />
</Style>
<Style Selector="^:disabled">
<Setter Property="Opacity"
Value="0.4" />
</Style>
</ControlTheme>
<ControlTheme x:Key="ProgressButtonDefault"
BasedOn="{StaticResource ProgressButtonBaseStyle}"
TargetType="hc:ProgressButton">
<Setter Property="Foreground"
Value="{DynamicResource PrimaryTextBrush}" />
<Setter Property="BorderThickness"
Value="1" />
<Style Selector="^:pointerover">
<Setter Property="Background"
Value="{DynamicResource SecondaryRegionBrush}" />
</Style>
<Style Selector="^:pressed">
<Setter Property="Background"
Value="{DynamicResource BorderBrush}" />
</Style>
</ControlTheme>
<ControlTheme x:Key="{x:Type hc:ProgressButton}"
BasedOn="{StaticResource ProgressButtonDefault}"
TargetType="hc:ProgressButton" />
<ControlTheme x:Key="ProgressButtonPrimary"
BasedOn="{StaticResource ProgressButtonBaseStyle}"
TargetType="hc:ProgressButton">
<Setter Property="Background"
Value="{DynamicResource PrimaryBrush}" />
</ControlTheme>
<ControlTheme x:Key="ProgressButtonSuccess"
BasedOn="{StaticResource ProgressButtonBaseStyle}"
TargetType="hc:ProgressButton">
<Setter Property="Background"
Value="{DynamicResource SuccessBrush}" />
</ControlTheme>
<ControlTheme x:Key="ProgressButtonInfo"
BasedOn="{StaticResource ProgressButtonBaseStyle}"
TargetType="hc:ProgressButton">
<Setter Property="Background"
Value="{DynamicResource InfoBrush}" />
</ControlTheme>
<ControlTheme x:Key="ProgressButtonWarning"
BasedOn="{StaticResource ProgressButtonBaseStyle}"
TargetType="hc:ProgressButton">
<Setter Property="Background"
Value="{DynamicResource WarningBrush}" />
</ControlTheme>
<ControlTheme x:Key="ProgressButtonDanger"
BasedOn="{StaticResource ProgressButtonBaseStyle}"
TargetType="hc:ProgressButton">
<Setter Property="Background"
Value="{DynamicResource DangerBrush}" />
</ControlTheme>
</ResourceDictionary>
@@ -895,4 +895,129 @@
<Setter Property="hc:BorderElement.CornerRadius"
Value="0,0,4,4" />
</ControlTheme>
<ControlTheme x:Key="ToggleButtonLoadingPrimary"
BasedOn="{StaticResource ToggleButtonBaseStyle}"
TargetType="ToggleButton">
<Setter Property="Background"
Value="{DynamicResource PrimaryBrush}" />
<Setter Property="Template">
<ControlTemplate>
<Border x:Name="Chrome"
CornerRadius="{TemplateBinding CornerRadius}"
BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}"
Background="{TemplateBinding Background}">
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center">
<Panel>
<ContentPresenter x:Name="UnCheckedElement"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
RecognizesAccessKey="True"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}" />
<ContentPresenter x:Name="CheckedElement"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
RecognizesAccessKey="True"
IsVisible="False"
Content="{TemplateBinding hc:StatusSwitchElement.CheckedElement}"
ContentTemplate="{TemplateBinding ContentTemplate}" />
</Panel>
<Viewbox Margin="0,4">
<hc:LoadingCircle x:Name="Loading"
Width="22"
Height="22"
Foreground="{TemplateBinding Foreground}"
DotDiameter="3"
IsRunning="{TemplateBinding IsChecked}"
IsVisible="False" />
</Viewbox>
</StackPanel>
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^:pointerover">
<Setter Property="Opacity"
Value="0.9" />
</Style>
<Style Selector="^:pressed">
<Setter Property="Opacity"
Value="0.6" />
</Style>
<Style Selector="^:checked /template/ hc|LoadingCircle#Loading">
<Setter Property="IsVisible"
Value="True" />
</Style>
<Style Selector="^:checked /template/ ContentPresenter#CheckedElement">
<Setter Property="IsVisible"
Value="True" />
</Style>
<Style Selector="^[(hc|StatusSwitchElement.HideUncheckedElement)=True]:checked /template/ ContentPresenter#UnCheckedElement">
<Setter Property="IsVisible"
Value="False" />
</Style>
<Style Selector="^:disabled">
<Setter Property="Opacity"
Value="0.4" />
</Style>
</ControlTheme>
<ControlTheme x:Key="ToggleButtonLoading"
BasedOn="{StaticResource ToggleButtonLoadingPrimary}"
TargetType="ToggleButton">
<Setter Property="Background"
Value="{DynamicResource RegionBrush}" />
<Setter Property="Foreground"
Value="{DynamicResource PrimaryTextBrush}" />
<Setter Property="BorderThickness"
Value="1" />
<Style Selector="^:pointerover">
<Setter Property="Background"
Value="{DynamicResource SecondaryRegionBrush}" />
</Style>
<Style Selector="^:pressed">
<Setter Property="Background"
Value="{DynamicResource BorderBrush}" />
</Style>
</ControlTheme>
<ControlTheme x:Key="ToggleButtonLoadingSuccess"
BasedOn="{StaticResource ToggleButtonLoadingPrimary}"
TargetType="ToggleButton">
<Setter Property="Background"
Value="{DynamicResource SuccessBrush}" />
</ControlTheme>
<ControlTheme x:Key="ToggleButtonLoadingInfo"
BasedOn="{StaticResource ToggleButtonLoadingPrimary}"
TargetType="ToggleButton">
<Setter Property="Background"
Value="{DynamicResource InfoBrush}" />
</ControlTheme>
<ControlTheme x:Key="ToggleButtonLoadingWarning"
BasedOn="{StaticResource ToggleButtonLoadingPrimary}"
TargetType="ToggleButton">
<Setter Property="Background"
Value="{DynamicResource WarningBrush}" />
</ControlTheme>
<ControlTheme x:Key="ToggleButtonLoadingDanger"
BasedOn="{StaticResource ToggleButtonLoadingPrimary}"
TargetType="ToggleButton">
<Setter Property="Background"
Value="{DynamicResource DangerBrush}" />
</ControlTheme>
</ResourceDictionary>