From 43bc3dda74bd2d620d0b73446afbe1ae6fe95ff6 Mon Sep 17 00:00:00 2001 From: chinboy Date: Thu, 14 Aug 2025 12:10:09 +0800 Subject: [PATCH] Add SkeletonAvatar --- .../TokenResourceConst.g.cs | 1 + .../Skeleton/AbstractSkeleton.cs | 33 ++-- src/AtomUI.Controls/Skeleton/Skeleton.cs | 154 +++++++++++++++++- .../Skeleton/SkeletonAvatar.cs | 113 ++++++++++++- .../Skeleton/SkeletonParagraph.cs | 25 ++- src/AtomUI.Controls/Skeleton/SkeletonTitle.cs | 18 +- src/AtomUI.Controls/Skeleton/SkeletonToken.cs | 6 + .../Skeleton/Themes/SkeletonAvatarTheme.axaml | 63 +++++++ .../Skeleton/Themes/SkeletonLineTheme.axaml | 3 +- .../Skeleton/Themes/SkeletonTheme.axaml | 71 ++++++++ .../Skeleton/Themes/SkeletonThemeConstants.cs | 4 + .../Skeleton/Themes/SkeletonThemes.axaml | 1 + 12 files changed, 466 insertions(+), 26 deletions(-) create mode 100644 src/AtomUI.Controls/Skeleton/Themes/SkeletonAvatarTheme.axaml diff --git a/src/AtomUI.Controls/GeneratedFiles/AtomUI.Generator/AtomUI.Generator.TokenResourceKeyGenerator/TokenResourceConst.g.cs b/src/AtomUI.Controls/GeneratedFiles/AtomUI.Generator/AtomUI.Generator.TokenResourceKeyGenerator/TokenResourceConst.g.cs index d78741300..b1b566a8b 100644 --- a/src/AtomUI.Controls/GeneratedFiles/AtomUI.Generator/AtomUI.Generator.TokenResourceKeyGenerator/TokenResourceConst.g.cs +++ b/src/AtomUI.Controls/GeneratedFiles/AtomUI.Generator/AtomUI.Generator.TokenResourceKeyGenerator/TokenResourceConst.g.cs @@ -661,6 +661,7 @@ namespace AtomUI.Theme.Styling public static class SkeletonTokenKey { + public static readonly TokenResourceKey AvatarMarginRight = new TokenResourceKey("Skeleton.AvatarMarginRight"); public static readonly TokenResourceKey BlockRadius = new TokenResourceKey("Skeleton.BlockRadius"); public static readonly TokenResourceKey GradientFromColor = new TokenResourceKey("Skeleton.GradientFromColor"); public static readonly TokenResourceKey GradientToColor = new TokenResourceKey("Skeleton.GradientToColor"); diff --git a/src/AtomUI.Controls/Skeleton/AbstractSkeleton.cs b/src/AtomUI.Controls/Skeleton/AbstractSkeleton.cs index f38b68ff5..c0515f7f7 100644 --- a/src/AtomUI.Controls/Skeleton/AbstractSkeleton.cs +++ b/src/AtomUI.Controls/Skeleton/AbstractSkeleton.cs @@ -17,9 +17,6 @@ public abstract class AbstractSkeleton : TemplatedControl public static readonly StyledProperty IsActiveProperty = AvaloniaProperty.Register(nameof(IsActive)); - // public static readonly StyledProperty SizeTypeProperty = - // SizeTypeAwareControlProperty.SizeTypeProperty.AddOwner(); - public static readonly StyledProperty MotionDurationProperty = MotionAwareControlProperty.MotionDurationProperty.AddOwner(); @@ -31,13 +28,7 @@ public abstract class AbstractSkeleton : TemplatedControl get => GetValue(IsActiveProperty); set => SetValue(IsActiveProperty, value); } - - // public SizeType SizeType - // { - // get => GetValue(SizeTypeProperty); - // set => SetValue(SizeTypeProperty, value); - // } - // + public TimeSpan MotionDuration { get => GetValue(MotionDurationProperty); @@ -117,19 +108,19 @@ public abstract class AbstractSkeleton : TemplatedControl } } } - } - - if (change.Property == LoadingBackgroundStartProperty || - change.Property == LoadingBackgroundMiddleProperty || - change.Property == LoadingBackgroundEndProperty || - change.Property == MotionDurationProperty) - { - if (!IsFollowMode) + + if (change.Property == LoadingBackgroundStartProperty || + change.Property == LoadingBackgroundMiddleProperty || + change.Property == LoadingBackgroundEndProperty || + change.Property == MotionDurationProperty) { - BuildActiveAnimation(true); - if (IsActive) + if (!IsFollowMode) { - StartActiveAnimation(); + BuildActiveAnimation(true); + if (IsActive) + { + StartActiveAnimation(); + } } } } diff --git a/src/AtomUI.Controls/Skeleton/Skeleton.cs b/src/AtomUI.Controls/Skeleton/Skeleton.cs index 85d13941b..ed8e154b5 100644 --- a/src/AtomUI.Controls/Skeleton/Skeleton.cs +++ b/src/AtomUI.Controls/Skeleton/Skeleton.cs @@ -1,8 +1,160 @@ +using System.Diagnostics; +using AtomUI.Controls.Themes; +using AtomUI.Theme; +using AtomUI.Theme.Utils; +using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.Primitives; namespace AtomUI.Controls; -public class Skeleton : ContentControl +public class Skeleton : AbstractSkeleton, IControlSharedTokenResourcesHost { + #region 公共属性定义 + + public static readonly StyledProperty IsLoadingProperty = + AvaloniaProperty.Register(nameof(IsLoading)); + + public static readonly StyledProperty IsShowAvatarProperty = + AvaloniaProperty.Register(nameof(IsShowAvatar)); + public static readonly StyledProperty IsShowParagraphProperty = + AvaloniaProperty.Register(nameof(IsShowParagraph), true); + + public static readonly StyledProperty IsShowTitleProperty = + AvaloniaProperty.Register(nameof(IsShowTitle), true); + + public static readonly StyledProperty IsRoundProperty = + SkeletonLine.IsRoundProperty.AddOwner(); + + public static readonly StyledProperty TitleWidthProperty = + AvaloniaProperty.Register(nameof(TitleWidth), new SkeletonWidth(50, SkeletonUnitType.Percentage)); + + public static readonly StyledProperty ParagraphRowsProperty = + AvaloniaProperty.Register(nameof(ParagraphRows), 2, validate: i => i >= 1); + + public static readonly StyledProperty ParagraphLastLineWidthProperty = + AvaloniaProperty.Register(nameof(ParagraphLastLineWidth), new SkeletonWidth(61.0, SkeletonUnitType.Percentage)); + + public static readonly StyledProperty?> ParagraphLineWidthsProperty = + AvaloniaProperty.Register?>(nameof(ParagraphLineWidths)); + + public static readonly StyledProperty AvatarShapeProperty = + AvaloniaProperty.Register(nameof(AvatarShape), AvatarShape.Circle); + + public static readonly StyledProperty AvatarSizeTypeProperty = + AvaloniaProperty.Register(nameof(AvatarSizeType), AvatarSizeType.Middle); + + public static readonly StyledProperty AvatarSizeProperty = + AvaloniaProperty.Register(nameof(AvatarSizeType), Double.NaN); + + public bool IsLoading + { + get => GetValue(IsLoadingProperty); + set => SetValue(IsLoadingProperty, value); + } + + public bool IsShowAvatar + { + get => GetValue(IsShowAvatarProperty); + set => SetValue(IsShowAvatarProperty, value); + } + + public bool IsShowParagraph + { + get => GetValue(IsShowParagraphProperty); + set => SetValue(IsShowParagraphProperty, value); + } + + public bool IsShowTitle + { + get => GetValue(IsShowTitleProperty); + set => SetValue(IsShowTitleProperty, value); + } + + public bool IsRound + { + get => GetValue(IsRoundProperty); + set => SetValue(IsRoundProperty, value); + } + + public SkeletonWidth TitleWidth + { + get => GetValue(TitleWidthProperty); + set => SetValue(TitleWidthProperty, value); + } + + public int ParagraphRows + { + get => GetValue(ParagraphRowsProperty); + set => SetValue(ParagraphRowsProperty, value); + } + + public SkeletonWidth ParagraphLastLineWidth + { + get => GetValue(ParagraphLastLineWidthProperty); + set => SetValue(ParagraphLastLineWidthProperty, value); + } + + public List? ParagraphLineWidths + { + get => GetValue(ParagraphLineWidthsProperty); + set => SetValue(ParagraphLineWidthsProperty, value); + } + + public AvatarSizeType AvatarSizeType + { + get => GetValue(AvatarSizeTypeProperty); + set => SetValue(AvatarSizeTypeProperty, value); + } + + public AvatarShape AvatarShape + { + get => GetValue(AvatarShapeProperty); + set => SetValue(AvatarShapeProperty, value); + } + + public double AvatarSize + { + get => GetValue(AvatarSizeProperty); + set => SetValue(AvatarSizeProperty, value); + } + #endregion + + #region 内部属性定义 + + Control IControlSharedTokenResourcesHost.HostControl => this; + string IControlSharedTokenResourcesHost.TokenId => SkeletonToken.ID; + + #endregion + + public Skeleton() + { + this.RegisterResources(); + } + + private SkeletonAvatar? _avatar; + private SkeletonTitle? _title; + private SkeletonParagraph? _paragraph; + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + _avatar = e.NameScope.Find(SkeletonThemeConstants.AvatarPart); + _title = e.NameScope.Find(SkeletonThemeConstants.TitlePart); + _paragraph = e.NameScope.Find(SkeletonThemeConstants.ParagraphPart); + + Debug.Assert(_avatar != null); + Debug.Assert(_title != null); + Debug.Assert(_paragraph != null); + + _avatar.Follow(this); + _title.Follow(this); + _paragraph.Follow(this); + + if (IsActive) + { + StartActiveAnimation(); + } + } } \ No newline at end of file diff --git a/src/AtomUI.Controls/Skeleton/SkeletonAvatar.cs b/src/AtomUI.Controls/Skeleton/SkeletonAvatar.cs index 64d6e2651..5a9a9f794 100644 --- a/src/AtomUI.Controls/Skeleton/SkeletonAvatar.cs +++ b/src/AtomUI.Controls/Skeleton/SkeletonAvatar.cs @@ -1,6 +1,117 @@ +using System.Diagnostics; +using AtomUI.Theme; +using AtomUI.Theme.Utils; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Data; + namespace AtomUI.Controls; -public class SkeletonAvatar : AbstractSkeleton +public class SkeletonAvatar : AbstractSkeleton, IControlSharedTokenResourcesHost { + #region 公共属性定义 + public static readonly StyledProperty ShapeProperty = + AvaloniaProperty.Register(nameof(AvatarShape), AvatarShape.Circle); + public static readonly StyledProperty SizeTypeProperty = + Avatar.SizeTypeProperty.AddOwner(); + + public static readonly StyledProperty SizeProperty = + AvaloniaProperty.Register(nameof(Size), Double.NaN); + + public AvatarShape Shape + { + get => GetValue(ShapeProperty); + set => SetValue(ShapeProperty, value); + } + + public AvatarSizeType SizeType + { + get => GetValue(SizeTypeProperty); + set => SetValue(SizeTypeProperty, value); + } + + public double Size + { + get => GetValue(SizeProperty); + set => SetValue(SizeProperty, value); + } + + #endregion + + #region 内部属性定义 + + Control IControlSharedTokenResourcesHost.HostControl => this; + string IControlSharedTokenResourcesHost.TokenId => SkeletonToken.ID; + + #endregion + + private AvatarSizeType? _originSizeType; + + public SkeletonAvatar() + { + this.RegisterResources(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == SizeProperty) + { + if (!double.IsNaN(Size)) + { + _originSizeType = SizeType; + SizeType = AvatarSizeType.Custom; + } + else + { + if (_originSizeType.HasValue) + { + SizeType = _originSizeType.Value; + } + } + } + else if (change.Property == SizeTypeProperty) + { + ConfigureSize(); + } + else if (change.Property == ShapeProperty) + { + ConfigureShape(); + } + } + + private void ConfigureShape() + { + if (Shape == AvatarShape.Circle) + { + SetValue(CornerRadiusProperty, new CornerRadius(Width / 2), BindingPriority.Template); + } + } + + private void ConfigureSize() + { + if (SizeType == AvatarSizeType.Custom) + { + Debug.Assert(!double.IsNaN(Size)); + // 不影响模板设置 + SetValue(WidthProperty, Size, BindingPriority.Template); + SetValue(HeightProperty, Size, BindingPriority.Template); + } + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + ConfigureShape(); + ConfigureSize(); + if (!IsFollowMode) + { + if (IsActive) + { + StartActiveAnimation(); + } + } + } } \ No newline at end of file diff --git a/src/AtomUI.Controls/Skeleton/SkeletonParagraph.cs b/src/AtomUI.Controls/Skeleton/SkeletonParagraph.cs index 07e70354c..544868758 100644 --- a/src/AtomUI.Controls/Skeleton/SkeletonParagraph.cs +++ b/src/AtomUI.Controls/Skeleton/SkeletonParagraph.cs @@ -1,4 +1,6 @@ using AtomUI.Controls.Themes; +using AtomUI.Theme; +using AtomUI.Theme.Utils; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; @@ -6,7 +8,7 @@ using Avalonia.VisualTree; namespace AtomUI.Controls; -public class SkeletonParagraph : AbstractSkeleton +public class SkeletonParagraph : AbstractSkeleton, IControlSharedTokenResourcesHost { #region 公共属性定义 @@ -19,6 +21,9 @@ public class SkeletonParagraph : AbstractSkeleton public static readonly StyledProperty RowsProperty = AvaloniaProperty.Register(nameof(Rows), 2, validate: i => i >= 1); + public static readonly StyledProperty IsRoundProperty = + SkeletonLine.IsRoundProperty.AddOwner(); + public SkeletonWidth LastLineWidth { get => GetValue(LastLineWidthProperty); @@ -37,10 +42,28 @@ public class SkeletonParagraph : AbstractSkeleton set => SetValue(RowsProperty, value); } + public bool IsRound + { + get => GetValue(IsRoundProperty); + set => SetValue(IsRoundProperty, value); + } + + #endregion + + #region 内部属性定义 + + Control IControlSharedTokenResourcesHost.HostControl => this; + string IControlSharedTokenResourcesHost.TokenId => SkeletonToken.ID; + #endregion private StackPanel? _linesLayout; + public SkeletonParagraph() + { + this.RegisterResources(); + } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); diff --git a/src/AtomUI.Controls/Skeleton/SkeletonTitle.cs b/src/AtomUI.Controls/Skeleton/SkeletonTitle.cs index a310e55a2..d80e3494b 100644 --- a/src/AtomUI.Controls/Skeleton/SkeletonTitle.cs +++ b/src/AtomUI.Controls/Skeleton/SkeletonTitle.cs @@ -1,9 +1,25 @@ +using AtomUI.Theme; +using AtomUI.Theme.Utils; +using Avalonia.Controls; + namespace AtomUI.Controls; -public class SkeletonTitle : SkeletonLine +public class SkeletonTitle : SkeletonLine, IControlSharedTokenResourcesHost { + #region 内部属性定义 + + Control IControlSharedTokenResourcesHost.HostControl => this; + string IControlSharedTokenResourcesHost.TokenId => SkeletonToken.ID; + + #endregion + static SkeletonTitle() { LineWidthProperty.OverrideDefaultValue(new SkeletonWidth(50, SkeletonUnitType.Percentage)); } + + public SkeletonTitle() + { + this.RegisterResources(); + } } \ No newline at end of file diff --git a/src/AtomUI.Controls/Skeleton/SkeletonToken.cs b/src/AtomUI.Controls/Skeleton/SkeletonToken.cs index 7cec74d3c..3d34c12cc 100644 --- a/src/AtomUI.Controls/Skeleton/SkeletonToken.cs +++ b/src/AtomUI.Controls/Skeleton/SkeletonToken.cs @@ -40,6 +40,11 @@ internal class SkeletonToken : AbstractControlDesignToken /// public Thickness ParagraphMarginTop { get; set; } + /// + /// 头像骨架屏右间距 + /// + public Thickness AvatarMarginRight { get; set; } + /// /// 段落骨架屏单行高度 /// Line height of paragraph skeleton @@ -74,6 +79,7 @@ internal class SkeletonToken : AbstractControlDesignToken TitleHeight = SharedToken.ControlHeight / 2; BlockRadius = SharedToken.BorderRadiusSM; ParagraphMarginTop = new Thickness(0, SharedToken.UniformlyMarginLG + SharedToken.UniformlyMarginXXS, 0, 0); + AvatarMarginRight = new Thickness(0, 0, SharedToken.UniformlyMargin, 0); ParagraphLineHeight = SharedToken.ControlHeight / 2; ParagraphLineRoundCornerRadius = new CornerRadius(ParagraphLineHeight / 2); LoadingMotionDuration = TimeSpan.FromSeconds(1.4); diff --git a/src/AtomUI.Controls/Skeleton/Themes/SkeletonAvatarTheme.axaml b/src/AtomUI.Controls/Skeleton/Themes/SkeletonAvatarTheme.axaml new file mode 100644 index 000000000..f2c5a0f34 --- /dev/null +++ b/src/AtomUI.Controls/Skeleton/Themes/SkeletonAvatarTheme.axaml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/AtomUI.Controls/Skeleton/Themes/SkeletonLineTheme.axaml b/src/AtomUI.Controls/Skeleton/Themes/SkeletonLineTheme.axaml index 55ae24bea..65a1fbacb 100644 --- a/src/AtomUI.Controls/Skeleton/Themes/SkeletonLineTheme.axaml +++ b/src/AtomUI.Controls/Skeleton/Themes/SkeletonLineTheme.axaml @@ -11,7 +11,8 @@ - + diff --git a/src/AtomUI.Controls/Skeleton/Themes/SkeletonTheme.axaml b/src/AtomUI.Controls/Skeleton/Themes/SkeletonTheme.axaml index 02212c223..0b52a8ae6 100644 --- a/src/AtomUI.Controls/Skeleton/Themes/SkeletonTheme.axaml +++ b/src/AtomUI.Controls/Skeleton/Themes/SkeletonTheme.axaml @@ -3,4 +3,75 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:atom="https://atomui.net" x:ClassModifier="internal"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/AtomUI.Controls/Skeleton/Themes/SkeletonThemeConstants.cs b/src/AtomUI.Controls/Skeleton/Themes/SkeletonThemeConstants.cs index 1aebd0c47..58b647d3a 100644 --- a/src/AtomUI.Controls/Skeleton/Themes/SkeletonThemeConstants.cs +++ b/src/AtomUI.Controls/Skeleton/Themes/SkeletonThemeConstants.cs @@ -3,6 +3,10 @@ namespace AtomUI.Controls.Themes; internal static class SkeletonThemeConstants { public const string ActiveAnimationLayerPart = "PART_ActiveAnimationLayer"; + public const string AvatarPart = "PART_Avatar"; + public const string TitlePart = "PART_Title"; + public const string ContentPart = "PART_Content"; + public const string ParagraphPart = "PART_Paragraph"; } internal static class SkeletonParagraphThemeConstants diff --git a/src/AtomUI.Controls/Skeleton/Themes/SkeletonThemes.axaml b/src/AtomUI.Controls/Skeleton/Themes/SkeletonThemes.axaml index db13c7217..45521c5a5 100644 --- a/src/AtomUI.Controls/Skeleton/Themes/SkeletonThemes.axaml +++ b/src/AtomUI.Controls/Skeleton/Themes/SkeletonThemes.axaml @@ -4,6 +4,7 @@ + \ No newline at end of file