Add SkeletonAvatar

This commit is contained in:
chinboy
2025-08-14 12:10:09 +08:00
parent 9830567a82
commit 45f5ffad9d
12 changed files with 466 additions and 26 deletions
@@ -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");
@@ -17,9 +17,6 @@ public abstract class AbstractSkeleton : TemplatedControl
public static readonly StyledProperty<bool> IsActiveProperty =
AvaloniaProperty.Register<AbstractSkeleton, bool>(nameof(IsActive));
// public static readonly StyledProperty<SizeType> SizeTypeProperty =
// SizeTypeAwareControlProperty.SizeTypeProperty.AddOwner<AbstractSkeleton>();
public static readonly StyledProperty<TimeSpan> MotionDurationProperty =
MotionAwareControlProperty.MotionDurationProperty.AddOwner<AbstractSkeleton>();
@@ -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();
}
}
}
}
+153 -1
View File
@@ -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<bool> IsLoadingProperty =
AvaloniaProperty.Register<Skeleton, bool>(nameof(IsLoading));
public static readonly StyledProperty<bool> IsShowAvatarProperty =
AvaloniaProperty.Register<Skeleton, bool>(nameof(IsShowAvatar));
public static readonly StyledProperty<bool> IsShowParagraphProperty =
AvaloniaProperty.Register<Skeleton, bool>(nameof(IsShowParagraph), true);
public static readonly StyledProperty<bool> IsShowTitleProperty =
AvaloniaProperty.Register<Skeleton, bool>(nameof(IsShowTitle), true);
public static readonly StyledProperty<bool> IsRoundProperty =
SkeletonLine.IsRoundProperty.AddOwner<Skeleton>();
public static readonly StyledProperty<SkeletonWidth> TitleWidthProperty =
AvaloniaProperty.Register<Skeleton, SkeletonWidth>(nameof(TitleWidth), new SkeletonWidth(50, SkeletonUnitType.Percentage));
public static readonly StyledProperty<int> ParagraphRowsProperty =
AvaloniaProperty.Register<Skeleton, int>(nameof(ParagraphRows), 2, validate: i => i >= 1);
public static readonly StyledProperty<SkeletonWidth> ParagraphLastLineWidthProperty =
AvaloniaProperty.Register<Skeleton, SkeletonWidth>(nameof(ParagraphLastLineWidth), new SkeletonWidth(61.0, SkeletonUnitType.Percentage));
public static readonly StyledProperty<List<SkeletonWidth>?> ParagraphLineWidthsProperty =
AvaloniaProperty.Register<Skeleton, List<SkeletonWidth>?>(nameof(ParagraphLineWidths));
public static readonly StyledProperty<AvatarShape> AvatarShapeProperty =
AvaloniaProperty.Register<Skeleton, AvatarShape>(nameof(AvatarShape), AvatarShape.Circle);
public static readonly StyledProperty<AvatarSizeType> AvatarSizeTypeProperty =
AvaloniaProperty.Register<Skeleton, AvatarSizeType>(nameof(AvatarSizeType), AvatarSizeType.Middle);
public static readonly StyledProperty<double> AvatarSizeProperty =
AvaloniaProperty.Register<Skeleton, double>(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<SkeletonWidth>? 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<SkeletonAvatar>(SkeletonThemeConstants.AvatarPart);
_title = e.NameScope.Find<SkeletonTitle>(SkeletonThemeConstants.TitlePart);
_paragraph = e.NameScope.Find<SkeletonParagraph>(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();
}
}
}
+112 -1
View File
@@ -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<AvatarShape> ShapeProperty =
AvaloniaProperty.Register<SkeletonAvatar, AvatarShape>(nameof(AvatarShape), AvatarShape.Circle);
public static readonly StyledProperty<AvatarSizeType> SizeTypeProperty =
Avatar.SizeTypeProperty.AddOwner<SkeletonAvatar>();
public static readonly StyledProperty<double> SizeProperty =
AvaloniaProperty.Register<SkeletonAvatar, double>(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();
}
}
}
}
@@ -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<int> RowsProperty =
AvaloniaProperty.Register<SkeletonParagraph, int>(nameof(Rows), 2, validate: i => i >= 1);
public static readonly StyledProperty<bool> IsRoundProperty =
SkeletonLine.IsRoundProperty.AddOwner<SkeletonParagraph>();
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);
+17 -1
View File
@@ -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<SkeletonTitle>(new SkeletonWidth(50, SkeletonUnitType.Percentage));
}
public SkeletonTitle()
{
this.RegisterResources();
}
}
@@ -40,6 +40,11 @@ internal class SkeletonToken : AbstractControlDesignToken
/// </summary>
public Thickness ParagraphMarginTop { get; set; }
/// <summary>
/// 头像骨架屏右间距
/// </summary>
public Thickness AvatarMarginRight { get; set; }
/// <summary>
/// 段落骨架屏单行高度
/// 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);
@@ -0,0 +1,63 @@
<ResourceDictionary
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:atom="https://atomui.net"
x:ClassModifier="internal">
<ControlTheme x:Key="{x:Type atom:SkeletonAvatar}" TargetType="atom:SkeletonAvatar">
<Setter Property="Template">
<ControlTemplate>
<Panel>
<Border Name="{x:Static atom:SkeletonThemeConstants.ActiveAnimationLayerPart}"
Background="{TemplateBinding AnimationLayerFill}"
CornerRadius="{TemplateBinding CornerRadius}"
IsVisible="{TemplateBinding IsActive}"/>
<Border Name="Avatar"
Background="{TemplateBinding Background}"
CornerRadius="{TemplateBinding CornerRadius}"
IsVisible="{TemplateBinding IsActive, Converter={x:Static BoolConverters.Not}}"/>
</Panel>
</ControlTemplate>
</Setter>
<Setter Property="MotionDuration" Value="{DynamicResource {x:Static atom:SkeletonTokenKey.LoadingMotionDuration}}"/>
<Setter Property="LoadingBackgroundStart" Value="{DynamicResource {x:Static atom:SkeletonTokenKey.LoadingBackgroundStart}}"/>
<Setter Property="LoadingBackgroundMiddle" Value="{DynamicResource {x:Static atom:SkeletonTokenKey.LoadingBackgroundMiddle}}"/>
<Setter Property="LoadingBackgroundEnd" Value="{DynamicResource {x:Static atom:SkeletonTokenKey.LoadingBackgroundEnd}}"/>
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Top" />
<Setter Property="Background" Value="{DynamicResource {x:Static atom:SkeletonTokenKey.GradientFromColor}}" />
<Style Selector="^[SizeType=Large]">
<Setter Property="Width" Value="{DynamicResource {x:Static atom:SharedTokenKey.ControlHeightLG}}" />
<Setter Property="Height" Value="{DynamicResource {x:Static atom:SharedTokenKey.ControlHeightLG}}" />
</Style>
<Style Selector="^[SizeType=Middle]">
<Setter Property="Width" Value="{DynamicResource {x:Static atom:SharedTokenKey.ControlHeight}}" />
<Setter Property="Height" Value="{DynamicResource {x:Static atom:SharedTokenKey.ControlHeight}}" />
</Style>
<Style Selector="^[SizeType=Small]">
<Setter Property="Width" Value="{DynamicResource {x:Static atom:SharedTokenKey.ControlHeightSM}}" />
<Setter Property="Height" Value="{DynamicResource {x:Static atom:SharedTokenKey.ControlHeightSM}}" />
</Style>
<Style Selector="^[Shape=Square]">
<Style Selector="^[SizeType=Large]">
<Setter Property="CornerRadius" Value="{DynamicResource {x:Static atom:SharedTokenKey.BorderRadiusLG}}" />
</Style>
<Style Selector="^[SizeType=Middle]">
<Setter Property="CornerRadius" Value="{DynamicResource {x:Static atom:SharedTokenKey.BorderRadius}}" />
</Style>
<Style Selector="^[SizeType=Small]">
<Setter Property="CornerRadius" Value="{DynamicResource {x:Static atom:SharedTokenKey.BorderRadiusSM}}" />
</Style>
<Style Selector="^[SizeType=Custom]">
<Setter Property="CornerRadius" Value="{DynamicResource {x:Static atom:SharedTokenKey.BorderRadius}}" />
</Style>
</Style>
</ControlTheme>
</ResourceDictionary>
@@ -11,7 +11,8 @@
<Rectangle Name="{x:Static atom:SkeletonThemeConstants.ActiveAnimationLayerPart}"
Fill="{TemplateBinding AnimationLayerFill}"
IsVisible="{TemplateBinding IsActive}"/>
<Rectangle Name="Line"/>
<Rectangle Name="Line"
IsVisible="{TemplateBinding IsActive, Converter={x:Static BoolConverters.Not}}"/>
</Panel>
</Border>
</ControlTemplate>
@@ -3,4 +3,75 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:atom="https://atomui.net"
x:ClassModifier="internal">
<ControlTheme x:Key="{x:Type atom:Skeleton}" TargetType="atom:Skeleton">
<Setter Property="Template">
<ControlTemplate>
<DockPanel LastChildFill="True">
<atom:SkeletonAvatar DockPanel.Dock="Left"
Name="{x:Static atom:SkeletonThemeConstants.AvatarPart}"
SizeType="{TemplateBinding AvatarSizeType}"
Shape="{TemplateBinding AvatarShape}"
Size="{TemplateBinding AvatarSize}"
IsVisible="{TemplateBinding IsShowAvatar}"
IsActive="{TemplateBinding IsActive}"/>
<StackPanel Orientation="Vertical"
Name="{x:Static atom:SkeletonThemeConstants.ContentPart}">
<atom:SkeletonTitle Name="{x:Static atom:SkeletonThemeConstants.TitlePart}"
IsVisible="{TemplateBinding IsShowTitle}"
IsRound="{TemplateBinding IsRound}"
LineWidth="{TemplateBinding TitleWidth}"
IsActive="{TemplateBinding IsActive}"/>
<atom:SkeletonParagraph Name="{x:Static atom:SkeletonThemeConstants.ParagraphPart}"
IsVisible="{TemplateBinding IsShowParagraph}"
IsRound="{TemplateBinding IsRound}"
Rows="{TemplateBinding ParagraphRows}"
LastLineWidth="{TemplateBinding ParagraphLastLineWidth}"
LineWidths="{TemplateBinding ParagraphLineWidths}"
IsActive="{TemplateBinding IsActive}"/>
</StackPanel>
</DockPanel>
</ControlTemplate>
</Setter>
<Setter Property="MotionDuration" Value="{DynamicResource {x:Static atom:SkeletonTokenKey.LoadingMotionDuration}}"/>
<Setter Property="LoadingBackgroundStart" Value="{DynamicResource {x:Static atom:SkeletonTokenKey.LoadingBackgroundStart}}"/>
<Setter Property="LoadingBackgroundMiddle" Value="{DynamicResource {x:Static atom:SkeletonTokenKey.LoadingBackgroundMiddle}}"/>
<Setter Property="LoadingBackgroundEnd" Value="{DynamicResource {x:Static atom:SkeletonTokenKey.LoadingBackgroundEnd}}"/>
<Setter Property="AvatarSizeType" Value="Large"/>
<Setter Property="AvatarShape" Value="Circle"/>
<Style Selector="^ /template/ atom|SkeletonAvatar">
<Setter Property="Margin" Value="{DynamicResource {x:Static atom:SkeletonTokenKey.AvatarMarginRight}}"/>
</Style>
<Style Selector="^[IsShowTitle=True][IsShowParagraph=False]">
<Setter Property="AvatarShape" Value="Square"/>
</Style>
<Style Selector="^[IsShowAvatar=False][IsShowParagraph=True]">
<Setter Property="TitleWidth" Value="38%"/>
</Style>
<Style Selector="^[IsShowAvatar=True][IsShowParagraph=True]">
<Setter Property="TitleWidth" Value="50%"/>
</Style>
<Style Selector="^[IsShowAvatar=False][IsShowTitle=False]">
<Setter Property="ParagraphLastLineWidth" Value="61%"/>
</Style>
<Style Selector="^[IsShowTitle=True]">
<Style Selector="^ /template/ atom|SkeletonParagraph">
<Setter Property="Margin" Value="{DynamicResource {x:Static atom:SkeletonTokenKey.ParagraphMarginTop}}"/>
</Style>
</Style>
<Setter Property="ParagraphRows" Value="2"/>
<Style Selector="^[IsShowAvatar=False][IsShowTitle=True]">
<Setter Property="ParagraphRows" Value="3"/>
</Style>
</ControlTheme>
</ResourceDictionary>
@@ -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
@@ -4,6 +4,7 @@
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source="SkeletonTheme.axaml" />
<ResourceInclude Source="SkeletonLineTheme.axaml" />
<ResourceInclude Source="SkeletonAvatarTheme.axaml" />
<ResourceInclude Source="SkeletonParagraphTheme.axaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>