完成 LoadingIndicator

This commit is contained in:
polarboy 2024-07-13 14:07:15 +08:00
parent e95ba39dc5
commit 90e288a33c
16 changed files with 677 additions and 135 deletions

View File

@ -0,0 +1,43 @@
<UserControl
x:Class="AtomUI.Demo.Desktop.ShowCase.LoadingIndicatorShowCase"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:atom="https://atomui.net"
xmlns:showcase="clr-namespace:AtomUI.Demo.Desktop.ShowCase"
mc:Ignorable="d">
<showcase:ShowCasePanel>
<showcase:ShowCaseItem
Title="Basic Usage"
Description="A simple loading status.">
<atom:LoadingIndicator/>
</showcase:ShowCaseItem>
<showcase:ShowCaseItem
Title="Size"
Description="A small LoadingIndicator is used for loading text, default sized LoadingIndicator for loading a card-level block, and large LoadingIndicator used for loading a page.">
<StackPanel Orientation="Horizontal">
<atom:LoadingIndicator SizeType="Small" VerticalAlignment="Center"/>
<atom:LoadingIndicator SizeType="Middle" VerticalAlignment="Center"/>
<atom:LoadingIndicator SizeType="Large" VerticalAlignment="Center"/>
</StackPanel>
</showcase:ShowCaseItem>
<showcase:ShowCaseItem
Title="Custom spinning indicator"
Description="Use custom loading indicator.">
<StackPanel Orientation="Horizontal">
<atom:LoadingIndicator SizeType="Small"
VerticalAlignment="Center"
CustomIndicatorIcon="{atom:IconProvider Kind=LoadingOutlined,NormalFilledColor=#1677ff}"/>
<atom:LoadingIndicator SizeType="Middle"
VerticalAlignment="Center"
CustomIndicatorIcon="{atom:IconProvider Kind=LoadingOutlined,NormalFilledColor=#1677ff}"/>
<atom:LoadingIndicator SizeType="Large"
VerticalAlignment="Center"
CustomIndicatorIcon="{atom:IconProvider Kind=LoadingOutlined,NormalFilledColor=#1677ff}"/>
</StackPanel>
</showcase:ShowCaseItem>
</showcase:ShowCasePanel>
</UserControl>

View File

@ -2,9 +2,9 @@ using Avalonia.Controls;
namespace AtomUI.Demo.Desktop.ShowCase;
public partial class ProgressIndicatorShowCase : UserControl
public partial class LoadingIndicatorShowCase : UserControl
{
public ProgressIndicatorShowCase()
public LoadingIndicatorShowCase()
{
InitializeComponent();
}

View File

@ -1,13 +0,0 @@
<UserControl
x:Class="AtomUI.Demo.Desktop.ShowCase.ProgressIndicatorShowCase"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:atom="https://atomui.net"
xmlns:showcase="clr-namespace:AtomUI.Demo.Desktop.ShowCase"
mc:Ignorable="d">
<showcase:ShowCasePanel>
</showcase:ShowCasePanel>
</UserControl>

View File

@ -1,13 +0,0 @@
<UserControl
x:Class="AtomUI.Demo.Desktop.ShowCase.SpinIndicatorShowCase"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:atom="https://atomui.net"
xmlns:showcase="clr-namespace:AtomUI.Demo.Desktop.ShowCase"
mc:Ignorable="d">
<showcase:ShowCasePanel>
</showcase:ShowCasePanel>
</UserControl>

View File

@ -1,13 +0,0 @@
using Avalonia.Controls;
using Button = AtomUI.Controls.Button;
using ToggleSwitch = AtomUI.Controls.ToggleSwitch;
namespace AtomUI.Demo.Desktop.ShowCase;
public partial class SpinIndicatorShowCase : UserControl
{
public SpinIndicatorShowCase()
{
InitializeComponent();
}
}

View File

@ -81,8 +81,8 @@
<TabItem Header="ProgressBar">
<showcases:ProgressBarShowCase />
</TabItem>
<TabItem Header="ProgressIndicator">
<showcases:ProgressIndicatorShowCase />
<TabItem Header="LoadingIndicator">
<showcases:LoadingIndicatorShowCase />
</TabItem>
<TabItem Header="Segmented">
<showcases:SegmentedShowCase />
@ -111,9 +111,6 @@
<TabItem Header="InputNumber">
<showcases:InputNumberShowCase />
</TabItem>
<TabItem Header="SpinIndicator">
<showcases:SpinIndicatorShowCase />
</TabItem>
<TabItem Header="TitleBar">
<showcases:TitleBarShowCase />
</TabItem>

View File

@ -19,21 +19,7 @@
<ItemGroup>
<ProjectReference Include="..\AtomUI.Generator\AtomUI.Generator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
<ItemGroup>
<Folder Include="Avatar\" />
<Folder Include="Collapse\" />
<Folder Include="ComboBox\" />
<Folder Include="EmptyIndicator\" />
<Folder Include="Expander\" />
<Folder Include="Input\" />
<Folder Include="Menu\" />
<Folder Include="Pagination\" />
<Folder Include="ProgressIndicator\" />
<Folder Include="Slider\" />
<Folder Include="TabBar\" />
<Folder Include="TabControl\" />
</ItemGroup>
<PropertyGroup>
<!-- enable private APIs -->
<AvaloniaAccessUnstablePrivateApis>true</AvaloniaAccessUnstablePrivateApis>

View File

@ -5,7 +5,7 @@ using Avalonia.Media;
namespace AtomUI.Controls;
[ControlDesignToken]
public class BadgeToken : AbstractControlDesignToken
internal class BadgeToken : AbstractControlDesignToken
{
public const string ID = "Badge";

View File

@ -25,110 +25,78 @@ public enum ButtonShape
public partial class Button : AvaloniaButton, ISizeTypeAware
{
// 需要改造
public static readonly DirectProperty<Button, ButtonType> ButtonTypeProperty =
AvaloniaProperty.RegisterDirect<Button, ButtonType>(nameof(ButtonType),
o => o.ButtonType,
(o, v) => o.ButtonType = v,
ButtonType.Default);
#region
public static readonly StyledProperty<ButtonType> ButtonTypeProperty =
AvaloniaProperty.Register<Button, ButtonType>(nameof(ButtonType), ButtonType.Default);
public static readonly DirectProperty<Button, ButtonShape> ButtonShapeProperty =
AvaloniaProperty.RegisterDirect<Button, ButtonShape>(nameof(Shape),
o => o.Shape,
(o, v) => o.Shape = v,
ButtonShape.Default);
public static readonly StyledProperty<ButtonShape> ButtonShapeProperty =
AvaloniaProperty.Register<Button, ButtonShape>(nameof(Shape), ButtonShape.Default);
public static readonly DirectProperty<Button, bool> IsDangerProperty =
AvaloniaProperty.RegisterDirect<Button, bool>(nameof(IsDanger),
o => o.IsDanger,
(o, v) => o.IsDanger = v,
false);
public static readonly StyledProperty<bool> IsDangerProperty =
AvaloniaProperty.Register<Button, bool>(nameof(IsDanger), false);
public static readonly DirectProperty<Button, bool> IsGhostProperty =
AvaloniaProperty.RegisterDirect<Button, bool>(nameof(IsGhost),
o => o.IsGhost,
(o, v) => o.IsGhost = v,
false);
public static readonly StyledProperty<bool> IsGhostProperty =
AvaloniaProperty.Register<Button, bool>(nameof(IsGhost), false);
public static readonly DirectProperty<Button, ButtonSizeType> SizeTypeProperty =
AvaloniaProperty.RegisterDirect<Button, ButtonSizeType>(nameof(SizeType),
o => o.SizeType,
(o, v) => o.SizeType = v,
ButtonSizeType.Middle);
public static readonly StyledProperty<ButtonSizeType> SizeTypeProperty =
AvaloniaProperty.Register<Button, ButtonSizeType>(nameof(SizeType), ButtonSizeType.Middle);
public static readonly DirectProperty<Button, PathIcon?> IconProperty
= AvaloniaProperty.RegisterDirect<Button, PathIcon?>(nameof(Icon),
o => o.Icon,
(o, v) => o.Icon = v);
public static readonly StyledProperty<PathIcon?> IconProperty
= AvaloniaProperty.Register<Button, PathIcon?>(nameof(Icon));
public static readonly DirectProperty<Button, string> TextProperty
= AvaloniaProperty.RegisterDirect<Button, string>(nameof(Text),
o => o.Text,
(o, v) => o.Text = v,
string.Empty);
private ButtonType _buttonType = ButtonType.Default;
public static readonly StyledProperty<string?> TextProperty
= AvaloniaProperty.Register<Button, string?>(nameof(Text));
public ButtonType ButtonType
{
get => _buttonType;
set => SetAndRaise(ButtonTypeProperty, ref _buttonType, value);
get => GetValue(ButtonTypeProperty);
set => SetValue(ButtonTypeProperty, value);
}
private ButtonShape _shape = ButtonShape.Default;
public ButtonShape Shape
{
get => _shape;
set => SetAndRaise(ButtonShapeProperty, ref _shape, value);
get => GetValue(ButtonShapeProperty);
set => SetValue(ButtonShapeProperty, value);
}
private bool _isDanger = false;
public bool IsDanger
{
get => _isDanger;
set => SetAndRaise(IsDangerProperty, ref _isDanger, value);
get => GetValue(IsDangerProperty);
set => SetValue(IsDangerProperty, value);
}
private bool _isGhost = false;
public bool IsGhost
{
get => _isGhost;
set => SetAndRaise(IsGhostProperty, ref _isGhost, value);
get => GetValue(IsGhostProperty);
set => SetValue(IsGhostProperty, value);
}
private ButtonSizeType _sizeType = ButtonSizeType.Middle;
public ButtonSizeType SizeType
{
get => _sizeType;
set => SetAndRaise(SizeTypeProperty, ref _sizeType, value);
get => GetValue(SizeTypeProperty);
set => SetValue(SizeTypeProperty, value);
}
private PathIcon? _icon;
public PathIcon? Icon
{
get => _icon;
set => SetAndRaise(IconProperty, ref _icon, value);
get => GetValue(IconProperty);
set => SetValue(IconProperty, value);
}
private string _text = string.Empty;
public string Text
public string? Text
{
get => _text;
set => SetAndRaise(TextProperty, ref _text, value);
get => GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
#endregion
static Button()
{
AffectsMeasure<Button>(SizeTypeProperty,
ButtonShapeProperty,
IconProperty,
WidthProperty,
IconProperty,
WidthProperty,
HeightProperty);
AffectsRender<Button>(ButtonTypeProperty,
IsDangerProperty,

View File

@ -79,7 +79,7 @@ public partial class Button : IWaveAdornerInfoProvider, IControlCustomStyle
private void CreateMainLayout()
{
if (Text.Length == 0 && Content is string content) {
if (Text is null && Content is string content) {
Text = content;
}
_label = new Label()
@ -402,7 +402,7 @@ public partial class Button : IWaveAdornerInfoProvider, IControlCustomStyle
{
if (Icon is not null) {
_stackPanel!.Children.Insert(0, Icon);
if (Text.Length != 0) {
if (Text is not null) {
if (SizeType == SizeType.Small) {
_controlTokenBinder.AddControlBinding(Icon, WidthProperty, GlobalResourceKey.IconSizeSM);
_controlTokenBinder.AddControlBinding(Icon, HeightProperty, GlobalResourceKey.IconSizeSM);

View File

@ -88,6 +88,14 @@
public const string IndicatorTristateMarkSize = "IndicatorTristateMarkSize";
}
public static class LoadingIndicatorResourceKey
{
public const string DotSize = "DotSize";
public const string DotSizeSM = "DotSizeSM";
public const string DotSizeLG = "DotSizeLG";
public const string IndicatorDuration = "IndicatorDuration";
}
public static class MarqueeLabelResourceKey
{
public const string CycleSpace = "CycleSpace";

View File

@ -0,0 +1,443 @@
using AtomUI.Data;
using AtomUI.Icon;
using AtomUI.Styling;
using AtomUI.Utils;
using Avalonia;
using Avalonia.Animation;
using Avalonia.Animation.Easings;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Media.Transformation;
using Avalonia.Styling;
namespace AtomUI.Controls;
public partial class LoadingIndicator : Control, ISizeTypeAware, IControlCustomStyle
{
#region
public static readonly StyledProperty<SizeType> SizeTypeProperty =
AvaloniaProperty.Register<LoadingIndicator, SizeType>(nameof(SizeType), SizeType.Middle);
public static readonly StyledProperty<string?> LoadingMsgProperty =
AvaloniaProperty.Register<LoadingIndicator, string?>(nameof(LoadingMsg));
public static readonly StyledProperty<bool> IsShowLoadingMsgProperty =
AvaloniaProperty.Register<LoadingIndicator, bool>(nameof(IsShowLoadingMsg), false);
public static readonly StyledProperty<PathIcon?> CustomIndicatorIconProperty =
AvaloniaProperty.Register<LoadingIndicator, PathIcon?>(nameof(CustomIndicatorIcon));
public static readonly StyledProperty<TimeSpan?> MotionDurationProperty =
AvaloniaProperty.Register<LoadingIndicator, TimeSpan?>(nameof(MotionDuration));
public static readonly StyledProperty<Easing?> MotionEasingCurveProperty =
AvaloniaProperty.Register<LoadingIndicator, Easing?>(nameof(MotionEasingCurve));
public SizeType SizeType
{
get => GetValue(SizeTypeProperty);
set => SetValue(SizeTypeProperty, value);
}
public string? LoadingMsg
{
get => GetValue(LoadingMsgProperty);
set => SetValue(LoadingMsgProperty, value);
}
public bool IsShowLoadingMsg
{
get => GetValue(IsShowLoadingMsgProperty);
set => SetValue(IsShowLoadingMsgProperty, value);
}
public PathIcon? CustomIndicatorIcon
{
get => GetValue(CustomIndicatorIconProperty);
set => SetValue(CustomIndicatorIconProperty, value);
}
public TimeSpan? MotionDuration
{
get => GetValue(MotionDurationProperty);
set => SetValue(MotionDurationProperty, value);
}
public Easing? MotionEasingCurve
{
get => GetValue(MotionEasingCurveProperty);
set => SetValue(MotionEasingCurveProperty, value);
}
#endregion
private bool _initialized = false;
private IControlCustomStyle _customStyle;
private ControlTokenBinder _controlTokenBinder;
private Animation? _animation;
private TextBlock? _textBlock;
private RenderInfo? _renderInfo;
private CancellationTokenSource?_cancellationTokenSource;
private const double LARGE_INDICATOR_SIZE = 48;
private const double MIDDLE_INDICATOR_SIZE = 32;
private const double SMALL_INDICATOR_SIZE = 16;
private const double MAX_CONTENT_WIDTH = 120; // 拍脑袋的决定
private const double MAX_CONTENT_HEIGHT = 400;
private const double DOT_START_OPACITY = 0.3;
static LoadingIndicator()
{
AffectsMeasure<LoadingIndicator>(SizeTypeProperty,
LoadingMsgProperty,
IsShowLoadingMsgProperty,
CustomIndicatorIconProperty);
AffectsRender<LoadingIndicator>(IndicatorAngleProperty);
}
public LoadingIndicator()
{
_customStyle = this;
_controlTokenBinder = new ControlTokenBinder(this, LoadingIndicatorToken.ID);
}
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnAttachedToLogicalTree(e);
if (!_initialized) {
_customStyle.SetupUi();
_initialized = true;
}
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
BuildIndicatorAnimation();
_cancellationTokenSource?.Cancel();
_cancellationTokenSource = new CancellationTokenSource();
_animation!.RunAsync(this, _cancellationTokenSource.Token);
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
_cancellationTokenSource?.Cancel();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
_customStyle.HandlePropertyChangedForStyle(e);
}
void IControlCustomStyle.HandlePropertyChangedForStyle(AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == CustomIndicatorIconProperty) {
if (_initialized) {
var oldCustomIcon = e.GetOldValue<PathIcon?>();
if (oldCustomIcon is not null) {
LogicalChildren.Remove(oldCustomIcon);
VisualChildren.Remove(oldCustomIcon);
}
SetupCustomIndicator();
}
} else if (e.Property == SizeTypeProperty) {
HandleSizeTypeChanged();
} else if (e.Property == IndicatorAngleProperty) {
if (CustomIndicatorIcon is not null) {
HandleIndicatorAngleChanged();
}
}
}
// 只在使用自定义的 Icon 的时候有效
private void HandleIndicatorAngleChanged()
{
if (CustomIndicatorIcon is not null) {
var builder = new TransformOperations.Builder(1);
builder.AppendRotate(MathUtils.Deg2Rad(IndicatorAngle));
CustomIndicatorIcon.RenderTransform = builder.Build();
}
}
private void SetupCustomIndicator()
{
if (CustomIndicatorIcon is not null) {
VisualChildren.Add(CustomIndicatorIcon);
LogicalChildren.Add(CustomIndicatorIcon);
// 暂时为了简单起见,我们在这里先只能使用 SizeType 的大小
var indicatorSize = GetIndicatorSize(SizeType);
CustomIndicatorIcon.Width = indicatorSize;
CustomIndicatorIcon.Height = indicatorSize;
CustomIndicatorIcon.IconMode = IconMode.Normal;
CustomIndicatorIcon.VerticalAlignment = VerticalAlignment.Center;
CustomIndicatorIcon.HorizontalAlignment = HorizontalAlignment.Center;
}
}
private void HandleSizeTypeChanged()
{
if (CustomIndicatorIcon is not null) {
var indicatorSize = GetIndicatorSize(SizeType);
CustomIndicatorIcon.Width = indicatorSize;
CustomIndicatorIcon.Height = indicatorSize;
}
}
protected override Size MeasureOverride(Size availableSize)
{
var targetWidth = 0d;
var targetHeight = 0d;
if (IsShowLoadingMsg) {
var size = base.MeasureOverride(new Size(Math.Min(availableSize.Width, MAX_CONTENT_WIDTH),
Math.Min(availableSize.Height, MAX_CONTENT_HEIGHT)));
targetWidth += size.Width;
targetHeight += size.Height;
if (size.Height > 0) {
targetHeight += GetLoadMsgPaddingTop();
}
}
var indicatorSize = GetIndicatorSize(SizeType);
targetWidth = Math.Max(indicatorSize, targetWidth);
targetHeight += indicatorSize;
return new Size(targetWidth, targetHeight);
}
private double GetLoadMsgPaddingTop()
{
return (GetEffectiveDotSize() - _fontSizeToken) / 2 + 2;
}
private double GetEffectiveDotSize()
{
var dotSize = 0d;
if (SizeType == SizeType.Large) {
dotSize = _dotSizeLGToken;
} else if (SizeType == SizeType.Middle) {
dotSize = _dotSizeToken;
} else {
dotSize = _dotSizeSMToken;
}
return dotSize;
}
protected override Size ArrangeOverride(Size finalSize)
{
if (IsShowLoadingMsg) {
var msgRect = GetLoadingMsgRect();
_textBlock!.Arrange(msgRect);
}
if (CustomIndicatorIcon is not null) {
var indicatorRect = GetIndicatorRect();
CustomIndicatorIcon.Arrange(indicatorRect);
}
return finalSize;
}
#region IControlCustomStyle
void IControlCustomStyle.SetupUi()
{
SetValue(HorizontalAlignmentProperty, HorizontalAlignment.Left, BindingPriority.Style);
SetValue(VerticalAlignmentProperty, VerticalAlignment.Top, BindingPriority.Style);
_textBlock = new TextBlock()
{
Text = LoadingMsg,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
};
LogicalChildren.Add(_textBlock);
VisualChildren.Add(_textBlock);
SetupCustomIndicator();
_customStyle.ApplyFixedStyleConfig();
}
void IControlCustomStyle.ApplyFixedStyleConfig()
{
_controlTokenBinder.AddControlBinding(DotSizeTokenProperty, LoadingIndicatorResourceKey.DotSize);
_controlTokenBinder.AddControlBinding(DotSizeSMTokenProperty, LoadingIndicatorResourceKey.DotSizeSM);
_controlTokenBinder.AddControlBinding(DotSizeLGTokenProperty, LoadingIndicatorResourceKey.DotSizeLG);
_controlTokenBinder.AddControlBinding(IndicatorDurationTokenProperty, LoadingIndicatorResourceKey.IndicatorDuration);
_controlTokenBinder.AddControlBinding(FontSizeTokenProperty, GlobalResourceKey.FontSize);
_controlTokenBinder.AddControlBinding(MarginXXSTokenProperty, GlobalResourceKey.MarginXXS);
_controlTokenBinder.AddControlBinding(ColorPrimaryTokenProperty, GlobalResourceKey.ColorPrimary);
}
private void BuildIndicatorAnimation(bool force = false)
{
if (force || _animation is null) {
_cancellationTokenSource?.Cancel();
_animation = new Animation()
{
IterationCount = IterationCount.Infinite,
Easing = MotionEasingCurve ?? new LinearEasing(),
Duration = MotionDuration ?? _indicatorDurationToken,
Children =
{
new KeyFrame
{
Setters = { new Setter(IndicatorAngleProperty, 0d), }, Cue = new Cue(0.0d)
},
new KeyFrame
{
Setters = { new Setter(IndicatorAngleProperty, 360d), }, Cue = new Cue(1.0d)
}
}
};
_cancellationTokenSource = null;
}
}
private Rect GetIndicatorRect()
{
var indicatorSize = GetIndicatorSize(SizeType);
var offsetX = (DesiredSize.Width - indicatorSize) / 2;
var offsetY = (DesiredSize.Height - indicatorSize) / 2;
if (IsShowLoadingMsg && LoadingMsg is not null) {
offsetY -= _textBlock!.DesiredSize.Height / 2;
}
return new Rect(new Point(offsetX, offsetY), new Size(indicatorSize, indicatorSize));
}
private Rect GetLoadingMsgRect()
{
if (!IsShowLoadingMsg) {
return default;
}
var indicatorRect = GetIndicatorRect();
var offsetX = indicatorRect.Left;
var offsetY = indicatorRect.Bottom;
offsetX -= (_textBlock!.DesiredSize.Width - indicatorRect.Width) / 2;
return new Rect(new Point(offsetX, offsetY), _textBlock.DesiredSize);
}
private static double GetIndicatorSize(SizeType sizeType)
{
return sizeType switch
{
SizeType.Small => SMALL_INDICATOR_SIZE,
SizeType.Middle => MIDDLE_INDICATOR_SIZE,
SizeType.Large => LARGE_INDICATOR_SIZE,
_ => throw new ArgumentOutOfRangeException(nameof(sizeType), sizeType, "Invalid value for SizeType")
};
}
private static double GetOpacityForAngle(double degree)
{
var mappedValue = (Math.Sin(MathUtils.Deg2Rad(degree)) + 1) / 2; // 将正弦波的范围从[-1, 1]映射到[0, 1]
return DOT_START_OPACITY + (1 - DOT_START_OPACITY) * mappedValue;
}
public override void Render(DrawingContext context)
{
_customStyle.PrepareRenderInfo();
if (CustomIndicatorIcon is null) {
RenderBuiltInIndicator(context);
}
_renderInfo = null;
}
private void RenderBuiltInIndicator(DrawingContext context)
{
if (_renderInfo is not null) {
var itemSize = _renderInfo.IndicatorItemSize;
var rightItemOpacity = GetOpacityForAngle(_indicatorAngle);
var bottomItemOpacity = GetOpacityForAngle(_indicatorAngle + 90);
var leftItemOpacity = GetOpacityForAngle(_indicatorAngle + 180);
var topItemOpacity = GetOpacityForAngle(_indicatorAngle + 270);
var itemEdgeMargin = _renderInfo.ItemEdgeMargin;
var indicatorRect = GetIndicatorRect();
var centerPoint = indicatorRect.Center;
var rightItemOffset = new Point(indicatorRect.Right - itemEdgeMargin - itemSize, centerPoint.Y - itemSize / 2);
var bottomItemOffset = new Point(centerPoint.X - itemSize / 2, indicatorRect.Bottom - itemEdgeMargin - itemSize);
var leftItemOffset = new Point(itemEdgeMargin, centerPoint.Y - itemSize / 2);
var topItemOffset = new Point(centerPoint.X - itemSize / 2, itemEdgeMargin);
var matrix = Matrix.CreateTranslation(-indicatorRect.Center.X, -indicatorRect.Center.Y);
matrix *= Matrix.CreateRotation(MathUtils.Deg2Rad(IndicatorAngle));
matrix *= Matrix.CreateTranslation(indicatorRect.Center.X, indicatorRect.Center.Y);
using var translateState = context.PushTransform(matrix);
{
using var opacityState = context.PushOpacity(rightItemOpacity);
var itemRect = new Rect(rightItemOffset, new Size(itemSize, itemSize));
context.DrawEllipse(_renderInfo.DotBgBrush, null, itemRect);
}
{
using var opacityState = context.PushOpacity(bottomItemOpacity);
var itemRect = new Rect(bottomItemOffset, new Size(itemSize, itemSize));
context.DrawEllipse(_renderInfo.DotBgBrush, null, itemRect);
}
{
using var opacityState = context.PushOpacity(leftItemOpacity);
var itemRect = new Rect(leftItemOffset, new Size(itemSize, itemSize));
context.DrawEllipse(_renderInfo.DotBgBrush, null, itemRect);
}
{
using var opacityState = context.PushOpacity(topItemOpacity);
var itemRect = new Rect(topItemOffset, new Size(itemSize, itemSize));
context.DrawEllipse(_renderInfo.DotBgBrush, null, itemRect);
}
}
}
private void RenderCustomIndicator(DrawingContext context)
{
}
void IControlCustomStyle.PrepareRenderInfo()
{
_renderInfo = new RenderInfo();
if (SizeType == SizeType.Large) {
_renderInfo.DotSize = _dotSizeLGToken;
_renderInfo.IndicatorItemSize = (_dotSizeLGToken - _marginXXSToken) / 2.5;
} else if (SizeType == SizeType.Middle) {
_renderInfo.DotSize = _dotSizeToken;
_renderInfo.IndicatorItemSize = (_dotSizeToken - _marginXXSToken) / 2;
} else {
_renderInfo.DotSize = _dotSizeSMToken;
_renderInfo.IndicatorItemSize = (_dotSizeSMToken - _marginXXSToken) / 2;
}
_renderInfo.IndicatorItemSize *= 0.9;
if (SizeType == SizeType.Large) {
_renderInfo.ItemEdgeMargin = _renderInfo.IndicatorItemSize / 1.5;
} else if (SizeType == SizeType.Middle) {
_renderInfo.ItemEdgeMargin = _renderInfo.IndicatorItemSize / 1.8;
} else {
_renderInfo.ItemEdgeMargin = 0.5;
}
_renderInfo.DotBgBrush = _colorPrimaryToken;
}
// 跟渲染相关的数据
private class RenderInfo
{
public double DotSize { get; set; }
public double IndicatorItemSize { get; set; }
public double ItemEdgeMargin { get; set; }
public IBrush? DotBgBrush { get; set; }
};
#endregion
}

View File

@ -0,0 +1,84 @@
using Avalonia;
using Avalonia.Media;
namespace AtomUI.Controls;
public partial class LoadingIndicator
{
#region Control token
private double _dotSizeToken;
private static readonly DirectProperty<LoadingIndicator, double> DotSizeTokenProperty =
AvaloniaProperty.RegisterDirect<LoadingIndicator, double>(
nameof(_dotSizeToken),
o => o._dotSizeToken,
(o, v) => o._dotSizeToken = v);
private double _dotSizeSMToken;
private static readonly DirectProperty<LoadingIndicator, double> DotSizeSMTokenProperty =
AvaloniaProperty.RegisterDirect<LoadingIndicator, double>(
nameof(_dotSizeSMToken),
o => o._dotSizeSMToken,
(o, v) => o._dotSizeSMToken = v);
private double _dotSizeLGToken;
private static readonly DirectProperty<LoadingIndicator, double> DotSizeLGTokenProperty =
AvaloniaProperty.RegisterDirect<LoadingIndicator, double>(
nameof(_dotSizeLGToken),
o => o._dotSizeLGToken,
(o, v) => o._dotSizeLGToken = v);
private TimeSpan _indicatorDurationToken;
private static readonly DirectProperty<LoadingIndicator, TimeSpan> IndicatorDurationTokenProperty =
AvaloniaProperty.RegisterDirect<LoadingIndicator, TimeSpan>(
nameof(_indicatorDurationToken),
o => o._indicatorDurationToken,
(o, v) => o._indicatorDurationToken = v);
private double _fontSizeToken;
private static readonly DirectProperty<LoadingIndicator, double> FontSizeTokenProperty =
AvaloniaProperty.RegisterDirect<LoadingIndicator, double>(
nameof(_fontSizeToken),
o => o._fontSizeToken,
(o, v) => o._fontSizeToken = v);
private double _marginXXSToken;
private static readonly DirectProperty<LoadingIndicator, double> MarginXXSTokenProperty =
AvaloniaProperty.RegisterDirect<LoadingIndicator, double>(
nameof(_marginXXSToken),
o => o._marginXXSToken,
(o, v) => o._marginXXSToken = v);
private IBrush? _colorPrimaryToken;
private static readonly DirectProperty<LoadingIndicator, IBrush?> ColorPrimaryTokenProperty =
AvaloniaProperty.RegisterDirect<LoadingIndicator, IBrush?>(
nameof(_colorPrimaryToken),
o => o._colorPrimaryToken,
(o, v) => o._colorPrimaryToken = v);
#endregion
#region
// 当前指示器的角度,动画输出目标属性
private static readonly DirectProperty<LoadingIndicator, double> IndicatorAngleProperty =
AvaloniaProperty.RegisterDirect<LoadingIndicator, double>(
nameof(IndicatorAngle),
o => o.IndicatorAngle,
(o, v) => o.IndicatorAngle = v);
private double _indicatorAngle;
private double IndicatorAngle
{
get => _indicatorAngle;
set => SetAndRaise(IndicatorAngleProperty, ref _indicatorAngle, value);
}
#endregion
}

View File

@ -0,0 +1,45 @@
using AtomUI.TokenSystem;
namespace AtomUI.Controls;
[ControlDesignToken]
internal class LoadingIndicatorToken : AbstractControlDesignToken
{
public const string ID = "LoadingIndicator";
public LoadingIndicatorToken()
: base(ID)
{
}
/// <summary>
/// 加载图标尺寸
/// </summary>
public double DotSize { get; set; }
/// <summary>
/// 加载图标尺寸
/// </summary>
public double DotSizeSM { get; set; }
/// <summary>
/// 大号加载图标尺寸
/// </summary>
public double DotSizeLG { get; set; }
/// <summary>
/// 加载器的周期时间
/// </summary>
public TimeSpan IndicatorDuration { get; set; }
internal override void CalculateFromAlias()
{
base.CalculateFromAlias();
var controlHeightLG = _globalToken.HeightToken.ControlHeightLG;
var controlHeight = _globalToken.SeedToken.ControlHeight;
DotSize = controlHeightLG / 2;
DotSizeSM = controlHeightLG * 0.35;
DotSizeLG = controlHeight;
IndicatorDuration = _globalToken.StyleToken.MotionDurationSlow * 4;
}
}

View File

@ -0,0 +1,6 @@
namespace AtomUI.Controls;
public class LoadingMask
{
}

View File

@ -14,4 +14,5 @@ internal interface IControlCustomStyle
void ApplyRenderScalingAwareStyleConfig() {}
void ApplySizeTypeStyleConfig() {}
void HandlePropertyChangedForStyle(AvaloniaPropertyChangedEventArgs e) {}
void PrepareRenderInfo() {}
}