feat: Add Circle and Wave progress bars and demo.

This commit is contained in:
NaBian committed 2026-06-14 13:17:45 +08:00
1 parent 7198c9c2ae
commit 090d17a1c8
6 files changed
+641 -2

No files matched your search

@@ -47,7 +47,8 @@
[ "FlexPanel", "FlexPanelDemo", "Brush.FlowLayoutPanel", "", "Layout" ],
[ "WaterfallPanel", "WaterfallPanelDemo", "Brush.Panel", "", "Layout" ],
[ "HoneycombPanel", "HoneycombPanelDemo", "Brush.PolygonHexagon", "", "Layout" ],
[ "UniformSpacingPanel", "UniformSpacingPanelDemo", "Brush.IncreaseHorizontalSpacing", "", "Layout" ]
[ "UniformSpacingPanel", "UniformSpacingPanelDemo", "Brush.IncreaseHorizontalSpacing", "", "Layout" ],
[ "ProgressBar", "ProgressBarDemo", "Brush.ProgressBar", "", "Feedback" ]
]
}
]
@@ -0,0 +1,88 @@
<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.ProgressBarDemo">
<ScrollViewer>
<StackPanel Margin="32">
<Slider Value="50"
Name="SliderDemo"
Maximum="100" />
<hc:UniformSpacingPanel Spacing="16"
Margin="0,32,0,0">
<hc:CircleProgressBar Value="{Binding #SliderDemo.Value}" />
<hc:CircleProgressBar Value="{Binding #SliderDemo.Value}"
FontSize="30" />
<hc:CircleProgressBar Value="{Binding #SliderDemo.Value}"
ShowText="False"
Width="20"
Height="20"
ArcThickness="2"
Theme="{StaticResource ProgressBarSuccessCircle}" />
<hc:CircleProgressBar Value="{Binding #SliderDemo.Value}"
ShowText="False"
Width="30"
Height="30"
ArcThickness="6"
Theme="{StaticResource ProgressBarInfoCircle}" />
<hc:CircleProgressBar Value="{Binding #SliderDemo.Value}"
ShowText="False"
Width="40"
Height="40"
ArcThickness="10"
Theme="{StaticResource ProgressBarWarningCircle}" />
<hc:CircleProgressBar Value="{Binding #SliderDemo.Value}"
Width="50"
Height="50"
Theme="{StaticResource ProgressBarDangerCircle}" />
</hc:UniformSpacingPanel>
<hc:UniformSpacingPanel Spacing="16"
Margin="0,32,0,0">
<hc:CircleProgressBar IsIndeterminate="True" />
<hc:CircleProgressBar IsIndeterminate="True"
Background="Transparent" />
<hc:CircleProgressBar IsIndeterminate="True"
Width="20"
Height="20"
ArcThickness="2"
Theme="{StaticResource ProgressBarSuccessCircle}" />
<hc:CircleProgressBar IsIndeterminate="True"
Width="30"
Height="30"
ArcThickness="6"
Theme="{StaticResource ProgressBarInfoCircle}" />
<hc:CircleProgressBar IsIndeterminate="True"
Width="40"
Height="40"
ArcThickness="10"
Theme="{StaticResource ProgressBarWarningCircle}" />
<hc:CircleProgressBar IsIndeterminate="True"
Width="50"
Height="50"
Theme="{StaticResource ProgressBarDangerCircle}" />
</hc:UniformSpacingPanel>
<hc:UniformSpacingPanel Spacing="16"
Margin="0,32,0,0">
<hc:WaveProgressBar Value="{Binding #SliderDemo.Value}" />
<hc:WaveProgressBar Value="{Binding #SliderDemo.Value}"
FontSize="20"
WaveThickness="4"
WaveStroke="#FFFF0080">
<hc:WaveProgressBar.WaveFill>
<LinearGradientBrush EndPoint="0,1"
StartPoint="0,0">
<GradientStop Color="#66FF0080"
Offset="0" />
<GradientStop Color="#FFFF0080"
Offset="1" />
</LinearGradientBrush>
</hc:WaveProgressBar.WaveFill>
</hc:WaveProgressBar>
<hc:WaveProgressBar Value="{Binding #SliderDemo.Value}"
ShowText="False"
Width="50"
Height="50"
Theme="{StaticResource ProgressBarWarningWave}" />
</hc:UniformSpacingPanel>
</StackPanel>
</ScrollViewer>
</UserControl>
@@ -0,0 +1,9 @@
namespace HandyControlDemo.UserControl;
public partial class ProgressBarDemo : Avalonia.Controls.UserControl
{
public ProgressBarDemo()
{
InitializeComponent();
}
}
@@ -0,0 +1,87 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
namespace HandyControl.Controls;
[TemplatePart(IndicatorTemplateName, typeof(Arc))]
public class CircleProgressBar : RangeBase
{
private const string IndicatorTemplateName = "PART_Indicator";
public static readonly StyledProperty<double> ArcThicknessProperty =
AvaloniaProperty.Register<CircleProgressBar, double>(nameof(ArcThickness));
public static readonly StyledProperty<bool> ShowTextProperty =
AvaloniaProperty.Register<CircleProgressBar, bool>(nameof(ShowText), true);
public static readonly StyledProperty<string?> TextProperty =
AvaloniaProperty.Register<CircleProgressBar, string?>(nameof(Text));
public static readonly StyledProperty<bool> IsIndeterminateProperty =
AvaloniaProperty.Register<CircleProgressBar, bool>(nameof(IsIndeterminate));
private Arc? _indicator;
static CircleProgressBar()
{
FocusableProperty.OverrideDefaultValue<CircleProgressBar>(false);
MaximumProperty.OverrideDefaultValue<CircleProgressBar>(100d);
}
public string? Text
{
get => GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
public bool ShowText
{
get => GetValue(ShowTextProperty);
set => SetValue(ShowTextProperty, value);
}
public double ArcThickness
{
get => GetValue(ArcThicknessProperty);
set => SetValue(ArcThicknessProperty, value);
}
public bool IsIndeterminate
{
get => GetValue(IsIndeterminateProperty);
set => SetValue(IsIndeterminateProperty, value);
}
private void SetProgressBarIndicatorAngle()
{
if (_indicator == null) return;
var minimum = Minimum;
var maximum = Maximum;
var num = Value;
_indicator.StartAngle = 0;
_indicator.SweepAngle = (maximum <= minimum ? 0 : (num - minimum) / (maximum - minimum)) * 360;
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_indicator = e.NameScope.Find<Arc>(IndicatorTemplateName);
SetProgressBarIndicatorAngle();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == MinimumProperty
|| change.Property == MaximumProperty
|| change.Property == ValueProperty)
{
SetProgressBarIndicatorAngle();
}
}
}
@@ -0,0 +1,120 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Media;
using System;
namespace HandyControl.Controls;
/// <summary>
/// 波浪进度条
/// </summary>
[TemplatePart(ElementWave, typeof(Control))]
[TemplatePart(ElementClip, typeof(Control))]
public class WaveProgressBar : RangeBase
{
private const string ElementWave = "PART_Wave";
private const string ElementClip = "PART_Clip";
private const double TranslateTransformMinY = -20;
private Control? _waveElement;
private double _translateTransformYRange;
private TranslateTransform? _yTransform;
static WaveProgressBar()
{
FocusableProperty.OverrideDefaultValue<WaveProgressBar>(false);
MaximumProperty.OverrideDefaultValue<WaveProgressBar>(100d);
}
public static readonly StyledProperty<string?> TextProperty =
AvaloniaProperty.Register<WaveProgressBar, string?>(nameof(Text));
public string? Text
{
get => GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
public static readonly StyledProperty<bool> ShowTextProperty =
AvaloniaProperty.Register<WaveProgressBar, bool>(nameof(ShowText), true);
public bool ShowText
{
get => GetValue(ShowTextProperty);
set => SetValue(ShowTextProperty, value);
}
public static readonly StyledProperty<IBrush?> WaveFillProperty =
AvaloniaProperty.Register<WaveProgressBar, IBrush?>(nameof(WaveFill));
public IBrush? WaveFill
{
get => GetValue(WaveFillProperty);
set => SetValue(WaveFillProperty, value);
}
public static readonly StyledProperty<double> WaveThicknessProperty =
AvaloniaProperty.Register<WaveProgressBar, double>(nameof(WaveThickness));
public double WaveThickness
{
get => GetValue(WaveThicknessProperty);
set => SetValue(WaveThicknessProperty, value);
}
public static readonly StyledProperty<IBrush?> WaveStrokeProperty =
AvaloniaProperty.Register<WaveProgressBar, IBrush?>(nameof(WaveStroke));
public IBrush? WaveStroke
{
get => GetValue(WaveStrokeProperty);
set => SetValue(WaveStrokeProperty, value);
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_waveElement = e.NameScope.Find<Control>(ElementWave);
var clipElement = e.NameScope.Find<Control>(ElementClip);
if (_waveElement != null && clipElement != null)
{
var clipElementHeight = clipElement.Height;
_yTransform = new TranslateTransform { Y = clipElementHeight };
_translateTransformYRange = clipElementHeight - TranslateTransformMinY;
_waveElement.RenderTransform = _yTransform;
UpdateWave(Value);
}
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == ValueProperty
|| change.Property == MinimumProperty
|| change.Property == MaximumProperty)
{
UpdateWave(Value);
}
}
private void UpdateWave(double value)
{
if (_yTransform == null) return;
var maximum = Maximum;
if (Math.Abs(maximum) < 1e-6) return;
var scale = 1 - value / maximum;
var y = _translateTransformYRange * scale + TranslateTransformMinY;
_yTransform.Y = y;
}
}
@@ -1,7 +1,8 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:ClassModifier="internal"
xmlns:hc="clr-namespace:HandyControl.Controls">
xmlns:hc="clr-namespace:HandyControl.Controls"
xmlns:shapes="clr-namespace:Avalonia.Controls.Shapes;assembly=Avalonia.Controls">
<ControlTheme x:Key="ProgressBarBaseStyle"
TargetType="ProgressBar">
<Setter Property="hc:BorderElement.CornerRadius"
@@ -647,4 +648,337 @@
</Style>
</Style>
</ControlTheme>
<!-- CircleProgressBar -->
<ControlTheme x:Key="CircleProgressBarTemplate"
TargetType="hc:CircleProgressBar">
<Setter Property="Background"
Value="{DynamicResource SecondaryRegionBrush}" />
<Setter Property="BorderBrush"
Value="{DynamicResource BorderBrush}" />
<Setter Property="Height"
Value="100" />
<Setter Property="Width"
Value="100" />
<Setter Property="ArcThickness"
Value="4" />
<Setter Property="Text"
Value="{Binding $self.Value, StringFormat={}{0:F0} %}" />
<Setter Property="Template">
<ControlTemplate TargetType="hc:CircleProgressBar">
<Panel Name="TemplateRoot">
<shapes:Arc StartAngle="-90"
SweepAngle="360"
Stretch="None"
Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}"
Stroke="{TemplateBinding Background}"
StrokeThickness="{TemplateBinding ArcThickness}"
StrokeLineCap="Flat" />
<shapes:Arc Name="PART_Indicator"
StartAngle="-90"
SweepAngle="0"
Stretch="None"
Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}"
Stroke="{TemplateBinding Foreground}"
StrokeThickness="{TemplateBinding ArcThickness}"
StrokeLineCap="Flat" />
<TextBlock IsVisible="{TemplateBinding ShowText}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="{TemplateBinding Foreground}"
Text="{TemplateBinding Text}" />
</Panel>
</ControlTemplate>
</Setter>
<Style Selector="^[IsIndeterminate=True]">
<Setter Property="Template">
<ControlTemplate TargetType="hc:CircleProgressBar">
<Panel Name="TemplateRoot"
RenderTransformOrigin="50%,50%">
<Panel.RenderTransform>
<RotateTransform />
</Panel.RenderTransform>
<shapes:Arc StartAngle="-90"
SweepAngle="360"
Stretch="None"
Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}"
Stroke="{TemplateBinding Background}"
StrokeThickness="{TemplateBinding ArcThickness}"
StrokeLineCap="Flat" />
<shapes:Arc Name="arc"
StartAngle="-5"
SweepAngle="10"
Stretch="None"
Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}"
Stroke="{TemplateBinding Foreground}"
StrokeThickness="{TemplateBinding ArcThickness}"
StrokeLineCap="Flat"
RenderTransformOrigin="50%,50%">
<shapes:Arc.RenderTransform>
<RotateTransform />
</shapes:Arc.RenderTransform>
</shapes:Arc>
</Panel>
</ControlTemplate>
</Setter>
<!-- TemplateRoot 整体旋转:1.568s 转 360° -->
<Style Selector="^ /template/ Panel#TemplateRoot">
<Style.Animations>
<Animation IterationCount="Infinite"
Easing="LinearEasing"
Duration="0:0:1.568">
<KeyFrame Cue="0%">
<Setter Property="RotateTransform.Angle"
Value="0" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="RotateTransform.Angle"
Value="360" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<!-- arc 自身额外旋转:5.332s 转 1080°,按 WPF 分 8 段 (每段 0.666s,+135°),每段独立 0.4,0,0.2,1 缓动 -->
<Style Selector="^ /template/ shapes|Arc#arc">
<Style.Animations>
<Animation IterationCount="Infinite"
Duration="0:0:5.332">
<KeyFrame Cue="0%"
KeySpline="0.4,0,0.2,1">
<Setter Property="RotateTransform.Angle"
Value="0" />
</KeyFrame>
<KeyFrame Cue="12.5%"
KeySpline="0.4,0,0.2,1">
<Setter Property="RotateTransform.Angle"
Value="135" />
</KeyFrame>
<KeyFrame Cue="25%"
KeySpline="0.4,0,0.2,1">
<Setter Property="RotateTransform.Angle"
Value="270" />
</KeyFrame>
<KeyFrame Cue="37.5%"
KeySpline="0.4,0,0.2,1">
<Setter Property="RotateTransform.Angle"
Value="405" />
</KeyFrame>
<KeyFrame Cue="50%"
KeySpline="0.4,0,0.2,1">
<Setter Property="RotateTransform.Angle"
Value="540" />
</KeyFrame>
<KeyFrame Cue="62.5%"
KeySpline="0.4,0,0.2,1">
<Setter Property="RotateTransform.Angle"
Value="675" />
</KeyFrame>
<KeyFrame Cue="75%"
KeySpline="0.4,0,0.2,1">
<Setter Property="RotateTransform.Angle"
Value="810" />
</KeyFrame>
<KeyFrame Cue="87.5%"
KeySpline="0.4,0,0.2,1">
<Setter Property="RotateTransform.Angle"
Value="945" />
</KeyFrame>
<KeyFrame Cue="100%"
KeySpline="0.4,0,0.2,1">
<Setter Property="RotateTransform.Angle"
Value="1080" />
</KeyFrame>
</Animation>
<!-- StartAngle: -5 → -130 → -5(每 1.333s 一个周期) -->
<Animation IterationCount="Infinite"
Duration="0:0:1.333">
<KeyFrame Cue="0%"
KeySpline="0.4,0,0.2,1">
<Setter Property="shapes:Arc.StartAngle"
Value="-5" />
</KeyFrame>
<KeyFrame Cue="50%"
KeySpline="0.4,0,0.2,1">
<Setter Property="shapes:Arc.StartAngle"
Value="-130" />
</KeyFrame>
<KeyFrame Cue="100%"
KeySpline="0.4,0,0.2,1">
<Setter Property="shapes:Arc.StartAngle"
Value="-5" />
</KeyFrame>
</Animation>
<!-- SweepAngle: 10 → 260 → 10(弧长伸缩) -->
<Animation IterationCount="Infinite"
Duration="0:0:1.333">
<KeyFrame Cue="0%"
KeySpline="0.4,0,0.2,1">
<Setter Property="shapes:Arc.SweepAngle"
Value="10" />
</KeyFrame>
<KeyFrame Cue="50%"
KeySpline="0.4,0,0.2,1">
<Setter Property="shapes:Arc.SweepAngle"
Value="260" />
</KeyFrame>
<KeyFrame Cue="100%"
KeySpline="0.4,0,0.2,1">
<Setter Property="shapes:Arc.SweepAngle"
Value="10" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</Style>
</ControlTheme>
<ControlTheme x:Key="{x:Type hc:CircleProgressBar}"
BasedOn="{StaticResource CircleProgressBarTemplate}"
TargetType="hc:CircleProgressBar">
<Setter Property="Foreground"
Value="{DynamicResource PrimaryBrush}" />
</ControlTheme>
<ControlTheme x:Key="ProgressBarSuccessCircle"
BasedOn="{StaticResource CircleProgressBarTemplate}"
TargetType="hc:CircleProgressBar">
<Setter Property="Foreground"
Value="{DynamicResource SuccessBrush}" />
</ControlTheme>
<ControlTheme x:Key="ProgressBarInfoCircle"
BasedOn="{StaticResource CircleProgressBarTemplate}"
TargetType="hc:CircleProgressBar">
<Setter Property="Foreground"
Value="{DynamicResource InfoBrush}" />
</ControlTheme>
<ControlTheme x:Key="ProgressBarWarningCircle"
BasedOn="{StaticResource CircleProgressBarTemplate}"
TargetType="hc:CircleProgressBar">
<Setter Property="Foreground"
Value="{DynamicResource WarningBrush}" />
</ControlTheme>
<ControlTheme x:Key="ProgressBarDangerCircle"
BasedOn="{StaticResource CircleProgressBarTemplate}"
TargetType="hc:CircleProgressBar">
<Setter Property="Foreground"
Value="{DynamicResource DangerBrush}" />
</ControlTheme>
<!-- WaveProgressBar -->
<ControlTheme x:Key="ProgressBarWaveBaseStyle"
TargetType="hc:WaveProgressBar">
<Setter Property="Background"
Value="{DynamicResource SecondaryRegionBrush}" />
<Setter Property="Width"
Value="100" />
<Setter Property="BorderThickness"
Value="1" />
<Setter Property="BorderBrush"
Value="{DynamicResource BorderBrush}" />
<Setter Property="Text"
Value="{Binding $self.Value, StringFormat={}{0:F0} %}" />
<Setter Property="Template">
<ControlTemplate TargetType="hc:WaveProgressBar">
<StackPanel>
<Border Theme="{StaticResource BorderCircular}"
Background="{TemplateBinding Background}"
BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}">
<Viewbox>
<Border Name="PART_Clip"
Theme="{StaticResource BorderCircularClip}"
CornerRadius="100"
Width="200"
Height="200">
<Panel Name="PART_Wave"
HorizontalAlignment="Left"
VerticalAlignment="Top">
<Path Name="PART_WaveAnim"
Stroke="{TemplateBinding WaveStroke}"
StrokeThickness="{TemplateBinding WaveThickness}"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Width="600"
Height="250"
Fill="{TemplateBinding WaveFill}"
Stretch="Fill"
UseLayoutRounding="False"
Margin="0,0,-400,-20"
Data="M 0,1 C 0.5,1 0.5,0 1,0 C 1.5,0 1.5,1 2,1 C 2.5,1 2.5,0 3,0 L 3,10 0,10 0,1 Z" />
</Panel>
</Border>
</Viewbox>
</Border>
<TextBlock Margin="0,10,0,0"
IsVisible="{TemplateBinding ShowText}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="{TemplateBinding Foreground}"
Text="{TemplateBinding Text}" />
</StackPanel>
</ControlTemplate>
</Setter>
<Style Selector="^ /template/ Path#PART_WaveAnim">
<Style.Animations>
<Animation IterationCount="Infinite"
Easing="LinearEasing"
Duration="0:0:2">
<KeyFrame Cue="0%">
<Setter Property="TranslateTransform.X"
Value="0" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="TranslateTransform.X"
Value="-400" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</ControlTheme>
<ControlTheme x:Key="{x:Type hc:WaveProgressBar}"
BasedOn="{StaticResource ProgressBarWaveBaseStyle}"
TargetType="hc:WaveProgressBar">
<Setter Property="WaveFill"
Value="{DynamicResource DarkPrimaryBrush}" />
</ControlTheme>
<ControlTheme x:Key="ProgressBarSuccessWave"
BasedOn="{StaticResource ProgressBarWaveBaseStyle}"
TargetType="hc:WaveProgressBar">
<Setter Property="WaveFill"
Value="{DynamicResource DarkSuccessBrush}" />
</ControlTheme>
<ControlTheme x:Key="ProgressBarInfoWave"
BasedOn="{StaticResource ProgressBarWaveBaseStyle}"
TargetType="hc:WaveProgressBar">
<Setter Property="WaveFill"
Value="{DynamicResource DarkInfoBrush}" />
</ControlTheme>
<ControlTheme x:Key="ProgressBarWarningWave"
BasedOn="{StaticResource ProgressBarWaveBaseStyle}"
TargetType="hc:WaveProgressBar">
<Setter Property="WaveFill"
Value="{DynamicResource DarkWarningBrush}" />
</ControlTheme>
<ControlTheme x:Key="ProgressBarDangerWave"
BasedOn="{StaticResource ProgressBarWaveBaseStyle}"
TargetType="hc:WaveProgressBar">
<Setter Property="WaveFill"
Value="{DynamicResource DarkDangerBrush}" />
</ControlTheme>
</ResourceDictionary>