diff --git a/Samples/AtomUI.Demo.Desktop/ShowCase/LoadingIndicatorShowCase.axaml b/Samples/AtomUI.Demo.Desktop/ShowCase/LoadingIndicatorShowCase.axaml index c7561ba..8824c25 100644 --- a/Samples/AtomUI.Demo.Desktop/ShowCase/LoadingIndicatorShowCase.axaml +++ b/Samples/AtomUI.Demo.Desktop/ShowCase/LoadingIndicatorShowCase.axaml @@ -28,16 +28,65 @@ Title="Custom spinning indicator" Description="Use custom loading indicator."> - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + Loading state: + + + + \ No newline at end of file diff --git a/Samples/AtomUI.Demo.Desktop/ShowCase/LoadingIndicatorShowCase.axaml.cs b/Samples/AtomUI.Demo.Desktop/ShowCase/LoadingIndicatorShowCase.axaml.cs index df4244d..a1c7be9 100644 --- a/Samples/AtomUI.Demo.Desktop/ShowCase/LoadingIndicatorShowCase.axaml.cs +++ b/Samples/AtomUI.Demo.Desktop/ShowCase/LoadingIndicatorShowCase.axaml.cs @@ -1,11 +1,22 @@ +using Avalonia; using Avalonia.Controls; namespace AtomUI.Demo.Desktop.ShowCase; public partial class LoadingIndicatorShowCase : UserControl { + public static readonly StyledProperty IsLoadingSwitchCheckedProperty = + AvaloniaProperty.Register(nameof(IsLoadingSwitchChecked), false); + + public bool IsLoadingSwitchChecked + { + get => GetValue(IsLoadingSwitchCheckedProperty); + set => SetValue(IsLoadingSwitchCheckedProperty, value); + } + public LoadingIndicatorShowCase() { + DataContext = this; InitializeComponent(); } diff --git a/src/AtomUI.Controls/ICornerRadiusInfoProvider.cs b/src/AtomUI.Controls/ICornerRadiusInfoProvider.cs new file mode 100644 index 0000000..4b36d42 --- /dev/null +++ b/src/AtomUI.Controls/ICornerRadiusInfoProvider.cs @@ -0,0 +1,8 @@ +using Avalonia; + +namespace AtomUI.Controls; + +public interface ICornerRadiusInfoProvider +{ + public CornerRadius CornerRadius { get; } +} \ No newline at end of file diff --git a/src/AtomUI.Controls/Loading/LoadingIndicator.cs b/src/AtomUI.Controls/Loading/LoadingIndicator.cs index ed1751f..51636ed 100644 --- a/src/AtomUI.Controls/Loading/LoadingIndicator.cs +++ b/src/AtomUI.Controls/Loading/LoadingIndicator.cs @@ -274,6 +274,7 @@ public partial class LoadingIndicator : Control, ISizeTypeAware, IControlCustomS _controlTokenBinder.AddControlBinding(FontSizeTokenProperty, GlobalResourceKey.FontSize); _controlTokenBinder.AddControlBinding(MarginXXSTokenProperty, GlobalResourceKey.MarginXXS); _controlTokenBinder.AddControlBinding(ColorPrimaryTokenProperty, GlobalResourceKey.ColorPrimary); + _controlTokenBinder.AddControlBinding(_textBlock!, TextBlock.ForegroundProperty, GlobalResourceKey.ColorPrimary); } private void BuildIndicatorAnimation(bool force = false) @@ -369,14 +370,14 @@ public partial class LoadingIndicator : Control, ISizeTypeAware, IControlCustomS 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 leftItemOffset = new Point(indicatorRect.Left + itemEdgeMargin, centerPoint.Y - itemSize / 2); + var topItemOffset = new Point(centerPoint.X - itemSize / 2, indicatorRect.Top + 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)); diff --git a/src/AtomUI.Controls/Loading/LoadingIndicatorAdorner.cs b/src/AtomUI.Controls/Loading/LoadingIndicatorAdorner.cs new file mode 100644 index 0000000..cc71b2e --- /dev/null +++ b/src/AtomUI.Controls/Loading/LoadingIndicatorAdorner.cs @@ -0,0 +1,61 @@ +using AtomUI.Styling; +using Avalonia; +using Avalonia.Controls; +using Avalonia.LogicalTree; + +namespace AtomUI.Controls; + +public class LoadingIndicatorAdorner : Control, IControlCustomStyle +{ + private bool _initialized = false; + private IControlCustomStyle _customStyle; + private LoadingIndicator? _loadingIndicator; + + public EventHandler? IndicatorCreated; + + public LoadingIndicatorAdorner() + { + _customStyle = this; + } + + protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + base.OnAttachedToLogicalTree(e); + if (!_initialized) { + _customStyle.SetupUi(); + _initialized = true; + } + } + + void IControlCustomStyle.SetupUi() + { + _loadingIndicator = new LoadingIndicator(); + IndicatorCreated?.Invoke(this, new LoadingIndicatorCreatedEventArgs(_loadingIndicator)); + LogicalChildren.Add(_loadingIndicator); + VisualChildren.Add(_loadingIndicator); + } + + protected override Size ArrangeOverride(Size finalSize) + { + var offsetX = (finalSize.Width - _loadingIndicator!.DesiredSize.Width) / 2; + var offsetY = (finalSize.Height - _loadingIndicator.DesiredSize.Height) / 2; + _loadingIndicator.Arrange(new Rect(new Point(offsetX, offsetY), _loadingIndicator.DesiredSize)); + return finalSize; + } + + protected override Size MeasureOverride(Size availableSize) + { + base.MeasureOverride(availableSize); + return availableSize; + } +} + +public class LoadingIndicatorCreatedEventArgs : EventArgs +{ + public LoadingIndicator LoadingIndicator { get; set; } + + public LoadingIndicatorCreatedEventArgs(LoadingIndicator indicator) + { + LoadingIndicator = indicator; + } +} \ No newline at end of file diff --git a/src/AtomUI.Controls/Loading/LoadingMask.cs b/src/AtomUI.Controls/Loading/LoadingMask.cs index f4ad028..8fd17e1 100644 --- a/src/AtomUI.Controls/Loading/LoadingMask.cs +++ b/src/AtomUI.Controls/Loading/LoadingMask.cs @@ -1,6 +1,175 @@ -namespace AtomUI.Controls; +using AtomUI.Utils; +using Avalonia; +using Avalonia.Animation.Easings; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.LogicalTree; +using Avalonia.Media; -public class LoadingMask +namespace AtomUI.Controls; + +public class LoadingMask : AvaloniaObject, IDisposable { + #region 公共属性定义 + + public static readonly StyledProperty SizeTypeProperty = + LoadingIndicator.SizeTypeProperty.AddOwner(); + + public static readonly StyledProperty LoadingMsgProperty = + LoadingIndicator.LoadingMsgProperty.AddOwner(); + public static readonly StyledProperty IsShowLoadingMsgProperty = + LoadingIndicator.IsShowLoadingMsgProperty.AddOwner(); + + public static readonly StyledProperty CustomIndicatorIconProperty = + LoadingIndicator.CustomIndicatorIconProperty.AddOwner(); + + public static readonly StyledProperty MotionDurationProperty = + LoadingIndicator.MotionDurationProperty.AddOwner(); + + public static readonly StyledProperty MotionEasingCurveProperty = + LoadingIndicator.MotionEasingCurveProperty.AddOwner(); + + 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); + } + + public bool IsLoading + { + get; + private set; + } + + #endregion + + private Control? _attachTarget; + private LoadingIndicatorAdorner? _loadingIndicatorAdorner; + private AdornerLayer? _adornerLayer; + + public LoadingMask(Control? attachTarget = null) + { + if (attachTarget is not null) { + Attach(attachTarget); + } + } + + public void Attach(Control attachTarget) + { + if (_attachTarget is not null) { + Detach(); + } + + attachTarget.DetachedFromLogicalTree += HandleTargetDetachedFromVisualTree; + _attachTarget = attachTarget; + } + + public void Detach() + { + if (_attachTarget is null) { + return; + } + Hide(); + _attachTarget.DetachedFromLogicalTree -= HandleTargetDetachedFromVisualTree; + _attachTarget = null; + } + + public void Show() + { + if (_attachTarget is null || IsLoading) { + return; + } + + if (_adornerLayer is null) { + _adornerLayer = AdornerLayer.GetAdornerLayer(_attachTarget); + if (_adornerLayer == null) { + throw new SystemException("No Adorner Layer found."); + } + } + + _loadingIndicatorAdorner ??= new LoadingIndicatorAdorner(); + _loadingIndicatorAdorner.IndicatorCreated += HandleLoadingIndicatorCreated; + + // 尽力探测 cornerradius + _attachTarget.Effect = new BlurEffect() + { + Radius = 5 + }; + AdornerLayer.SetAdornedElement(_loadingIndicatorAdorner, _attachTarget); + AdornerLayer.SetIsClipEnabled(_loadingIndicatorAdorner, true); + _adornerLayer.Children.Add(_loadingIndicatorAdorner); + + IsLoading = true; + } + + // 在这里进行配置 + private void HandleLoadingIndicatorCreated(object? sender, LoadingIndicatorCreatedEventArgs args) + { + var indicator = args.LoadingIndicator; + BindUtils.RelayBind(this, SizeTypeProperty, indicator, SizeTypeProperty); + BindUtils.RelayBind(this, LoadingMsgProperty, indicator, LoadingMsgProperty); + BindUtils.RelayBind(this, IsShowLoadingMsgProperty, indicator, IsShowLoadingMsgProperty); + BindUtils.RelayBind(this, CustomIndicatorIconProperty, indicator, CustomIndicatorIconProperty); + BindUtils.RelayBind(this, MotionDurationProperty, indicator, MotionDurationProperty); + BindUtils.RelayBind(this, MotionEasingCurveProperty, indicator, MotionEasingCurveProperty); + } + + public void Hide() + { + if (!IsLoading || _attachTarget is null) { + return; + } + if (_adornerLayer is not null && _loadingIndicatorAdorner is not null) { + _adornerLayer.Children.Remove(_loadingIndicatorAdorner); + } + + _attachTarget.Effect = null; + IsLoading = false; + } + + public void Dispose() + { + if (_attachTarget is not null) { + Detach(); + } + + _adornerLayer = null; + _loadingIndicatorAdorner = null; + } + + private void HandleTargetDetachedFromVisualTree(object? sender, LogicalTreeAttachmentEventArgs args) + { + Dispose(); + } } \ No newline at end of file diff --git a/src/AtomUI.Controls/Loading/LoadingMaskHost.cs b/src/AtomUI.Controls/Loading/LoadingMaskHost.cs new file mode 100644 index 0000000..09eb629 --- /dev/null +++ b/src/AtomUI.Controls/Loading/LoadingMaskHost.cs @@ -0,0 +1,173 @@ +using AtomUI.Styling; +using AtomUI.Utils; +using Avalonia; +using Avalonia.Animation.Easings; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.LogicalTree; +using Avalonia.Metadata; + +namespace AtomUI.Controls; + +public class LoadingMaskHost : Control, IControlCustomStyle +{ + #region 公共属性定义 + + public static readonly StyledProperty SizeTypeProperty = + LoadingIndicator.SizeTypeProperty.AddOwner(); + + public static readonly StyledProperty LoadingMsgProperty = + LoadingIndicator.LoadingMsgProperty.AddOwner(); + + public static readonly StyledProperty IsShowLoadingMsgProperty = + LoadingIndicator.IsShowLoadingMsgProperty.AddOwner(); + + public static readonly StyledProperty CustomIndicatorIconProperty = + LoadingIndicator.CustomIndicatorIconProperty.AddOwner(); + + public static readonly StyledProperty MotionDurationProperty = + LoadingIndicator.MotionDurationProperty.AddOwner(); + + public static readonly StyledProperty MotionEasingCurveProperty = + LoadingIndicator.MotionEasingCurveProperty.AddOwner(); + + public static readonly StyledProperty IsLoadingProperty = + AvaloniaProperty.Register(nameof(IsLoading), false); + + public static readonly StyledProperty MaskTargetProperty = + AvaloniaProperty.Register(nameof(MaskTarget)); + + 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); + } + + public bool IsLoading + { + get => GetValue(IsLoadingProperty); + set => SetValue(IsLoadingProperty, value); + } + + [Content] + public Control? MaskTarget + { + get => GetValue(MaskTargetProperty); + set => SetValue(MaskTargetProperty, value); + } + + #endregion + + private LoadingMask? _loadingMask; + private bool _initialized = false; + private IControlCustomStyle _customStyle; + + public LoadingMaskHost() + { + _customStyle = this; + } + + void IControlCustomStyle.SetupUi() + { + if (MaskTarget is not null) { + LogicalChildren.Add(MaskTarget); + VisualChildren.Add(MaskTarget); + } + } + + public void ShowLoading() + { + if (_loadingMask is not null && _loadingMask.IsLoading) { + return; + } + if (_loadingMask is null && MaskTarget is not null) { + _loadingMask = new LoadingMask(); + _loadingMask.Attach(this); + BindUtils.RelayBind(this, SizeTypeProperty, _loadingMask, SizeTypeProperty); + BindUtils.RelayBind(this, LoadingMsgProperty, _loadingMask, LoadingMsgProperty); + BindUtils.RelayBind(this, IsShowLoadingMsgProperty, _loadingMask, IsShowLoadingMsgProperty); + BindUtils.RelayBind(this, CustomIndicatorIconProperty, _loadingMask, CustomIndicatorIconProperty); + BindUtils.RelayBind(this, MotionDurationProperty, _loadingMask, MotionDurationProperty); + BindUtils.RelayBind(this, MotionEasingCurveProperty, _loadingMask, MotionEasingCurveProperty); + } + _loadingMask!.Show(); + } + + public void HideLoading() + { + if (_loadingMask is null || + (_loadingMask is not null && !_loadingMask.IsLoading)) { + return; + } + _loadingMask?.Hide(); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + if (IsLoading && MaskTarget is not null) { + ShowLoading(); + } + } + + protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + base.OnAttachedToLogicalTree(e); + if (!_initialized) { + _customStyle.SetupUi(); + _initialized = true; + } + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + HideLoading(); + _loadingMask?.Dispose(); + _loadingMask = null; + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (_initialized && VisualRoot is not null) { + if (IsLoadingProperty == change.Property) { + if (IsLoading) { + ShowLoading(); + } else { + HideLoading(); + } + } + } + } +} \ No newline at end of file