Skip to content

Commit bda0057

Browse files
authored
Feature: Added shimmer animation when loading icons (#14905)
1 parent a83507e commit bda0057

File tree

7 files changed

+328
-38
lines changed

7 files changed

+328
-38
lines changed

src/Files.App/App.xaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
<ResourceDictionary Source="ms-appx:///ResourceDictionaries/PathIcons.xaml" />
3838
<ResourceDictionary Source="ms-appx:///UserControls/SideBar/SideBarControls.xaml" />
3939
<ResourceDictionary Source="ms-appx:///ResourceDictionaries/App.Theme.TextBlockStyles.xaml" />
40+
<ResourceDictionary Source="ms-appx:///ResourceDictionaries/ShimmerStyles.xaml" />
4041
<ResourceDictionary Source="ms-appx:///ResourceDictionaries/MenuFlyoutSubItemWithImageStyle.xaml" />
4142
<ResourceDictionary>
4243
<ResourceDictionary.ThemeDictionaries>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<!-- Copyright (c) 2023 Files Community. Licensed under the MIT License. See the LICENSE. -->
2+
<ResourceDictionary
3+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
4+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
5+
xmlns:uc="using:Files.App.UserControls">
6+
7+
<Style TargetType="uc:Shimmer">
8+
<Setter Property="CornerRadius" Value="4" />
9+
<Setter Property="MinWidth" Value="8" />
10+
<Setter Property="MinHeight" Value="8" />
11+
<Setter Property="Template">
12+
<Setter.Value>
13+
<ControlTemplate TargetType="uc:Shimmer">
14+
<Border
15+
x:Name="Shape"
16+
Background="{TemplateBinding Background}"
17+
CornerRadius="{TemplateBinding CornerRadius}" />
18+
</ControlTemplate>
19+
</Setter.Value>
20+
</Setter>
21+
</Style>
22+
23+
</ResourceDictionary>
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright (c) 2023 Files Community
2+
// Licensed under the MIT License. See the LICENSE.
3+
4+
using Microsoft.UI.Xaml;
5+
using Microsoft.UI.Xaml.Controls;
6+
7+
namespace Files.App.UserControls
8+
{
9+
public partial class Shimmer : Control
10+
{
11+
/// <summary>
12+
/// Identifies the <see cref="Duration"/> dependency property.
13+
/// </summary>
14+
public static readonly DependencyProperty DurationProperty =
15+
DependencyProperty.Register(
16+
nameof(Duration),
17+
typeof(object),
18+
typeof(Shimmer),
19+
new PropertyMetadata(defaultValue: TimeSpan.FromMilliseconds(1600), PropertyChanged));
20+
21+
/// <summary>
22+
/// Identifies the <see cref="IsActive"/> dependency property.
23+
/// </summary>
24+
public static readonly DependencyProperty IsActiveProperty =
25+
DependencyProperty.Register(
26+
nameof(IsActive),
27+
typeof(bool),
28+
typeof(Shimmer),
29+
new PropertyMetadata(defaultValue: true, PropertyChanged));
30+
31+
/// <summary>
32+
/// Gets or sets the animation duration
33+
/// </summary>
34+
public TimeSpan Duration
35+
{
36+
get => (TimeSpan)GetValue(DurationProperty);
37+
set => SetValue(DurationProperty, value);
38+
}
39+
40+
/// <summary>
41+
/// Gets or sets if the animation is playing
42+
/// </summary>
43+
public bool IsActive
44+
{
45+
get => (bool)GetValue(IsActiveProperty);
46+
set => SetValue(IsActiveProperty, value);
47+
}
48+
49+
private static void PropertyChanged(DependencyObject s, DependencyPropertyChangedEventArgs e)
50+
{
51+
var self = (Shimmer)s;
52+
if (self.IsActive)
53+
{
54+
self.StopAnimation();
55+
self.TryStartAnimation();
56+
}
57+
else
58+
{
59+
self.StopAnimation();
60+
}
61+
}
62+
}
63+
}
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
// Copyright (c) 2023 Files Community
2+
// Licensed under the MIT License. See the LICENSE.
3+
4+
using CommunityToolkit.WinUI.UI;
5+
using CommunityToolkit.WinUI;
6+
using CommunityToolkit.WinUI.UI.Animations;
7+
using CommunityToolkit.WinUI.UI.Animations.Expressions;
8+
using Microsoft.UI.Composition;
9+
using Microsoft.UI.Xaml;
10+
using Microsoft.UI.Xaml.Controls;
11+
using Microsoft.UI.Xaml.Hosting;
12+
using Microsoft.UI.Xaml.Shapes;
13+
using System.Numerics;
14+
using Windows.UI;
15+
16+
namespace Files.App.UserControls
17+
{
18+
/// <summary>
19+
/// A generic shimmer control that can be used to construct a beautiful loading effect.
20+
/// </summary>
21+
[TemplatePart(Name = PART_Shape, Type = typeof(Rectangle))]
22+
public partial class Shimmer : Control
23+
{
24+
private const float InitialStartPointX = -7.92f;
25+
private const string PART_Shape = "Shape";
26+
27+
private Vector2Node? _sizeAnimation;
28+
private Vector2KeyFrameAnimation? _gradientStartPointAnimation;
29+
private Vector2KeyFrameAnimation? _gradientEndPointAnimation;
30+
private CompositionColorGradientStop? _gradientStop1;
31+
private CompositionColorGradientStop? _gradientStop2;
32+
private CompositionColorGradientStop? _gradientStop3;
33+
private CompositionColorGradientStop? _gradientStop4;
34+
private CompositionRoundedRectangleGeometry? _rectangleGeometry;
35+
private ShapeVisual? _shapeVisual;
36+
private CompositionLinearGradientBrush? _shimmerMaskGradient;
37+
private Border? _shape;
38+
39+
private bool _initialized;
40+
private bool _animationStarted;
41+
42+
public Shimmer()
43+
{
44+
DefaultStyleKey = typeof(Shimmer);
45+
Loaded += OnLoaded;
46+
Unloaded += OnUnloaded;
47+
}
48+
49+
protected override void OnApplyTemplate()
50+
{
51+
base.OnApplyTemplate();
52+
53+
_shape = GetTemplateChild(PART_Shape) as Border;
54+
if (_initialized is false && TryInitializationResource() && IsActive)
55+
{
56+
TryStartAnimation();
57+
}
58+
}
59+
60+
private void OnLoaded(object sender, RoutedEventArgs e)
61+
{
62+
if (_initialized is false && TryInitializationResource() && IsActive)
63+
{
64+
TryStartAnimation();
65+
}
66+
67+
ActualThemeChanged += OnActualThemeChanged;
68+
}
69+
70+
private void OnUnloaded(object sender, RoutedEventArgs e)
71+
{
72+
ActualThemeChanged -= OnActualThemeChanged;
73+
StopAnimation();
74+
75+
if (_initialized && _shape != null)
76+
{
77+
ElementCompositionPreview.SetElementChildVisual(_shape, null);
78+
79+
_rectangleGeometry!.Dispose();
80+
_shapeVisual!.Dispose();
81+
_shimmerMaskGradient!.Dispose();
82+
_gradientStop1!.Dispose();
83+
_gradientStop2!.Dispose();
84+
_gradientStop3!.Dispose();
85+
_gradientStop4!.Dispose();
86+
87+
_initialized = false;
88+
}
89+
}
90+
91+
private void OnActualThemeChanged(FrameworkElement sender, object args)
92+
{
93+
if (_initialized is false)
94+
{
95+
return;
96+
}
97+
98+
SetGradientStopColorsByTheme();
99+
}
100+
101+
private bool TryInitializationResource()
102+
{
103+
if (_initialized)
104+
{
105+
return true;
106+
}
107+
108+
if (_shape is null || IsLoaded is false)
109+
{
110+
return false;
111+
}
112+
113+
var compositor = _shape.GetVisual().Compositor;
114+
115+
_rectangleGeometry = compositor.CreateRoundedRectangleGeometry();
116+
_shapeVisual = compositor.CreateShapeVisual();
117+
_shimmerMaskGradient = compositor.CreateLinearGradientBrush();
118+
_gradientStop1 = compositor.CreateColorGradientStop();
119+
_gradientStop2 = compositor.CreateColorGradientStop();
120+
_gradientStop3 = compositor.CreateColorGradientStop();
121+
_gradientStop4 = compositor.CreateColorGradientStop();
122+
SetGradientAndStops();
123+
SetGradientStopColorsByTheme();
124+
_rectangleGeometry.CornerRadius = new Vector2((float)CornerRadius.TopLeft);
125+
var spriteShape = compositor.CreateSpriteShape(_rectangleGeometry);
126+
spriteShape.FillBrush = _shimmerMaskGradient;
127+
_shapeVisual.Shapes.Add(spriteShape);
128+
ElementCompositionPreview.SetElementChildVisual(_shape, _shapeVisual);
129+
130+
_initialized = true;
131+
return true;
132+
}
133+
134+
private void SetGradientAndStops()
135+
{
136+
_shimmerMaskGradient!.StartPoint = new Vector2(InitialStartPointX, 0.0f);
137+
_shimmerMaskGradient.EndPoint = new Vector2(0.0f, 1.0f); //Vector2.One
138+
139+
_gradientStop1!.Offset = 0.273f;
140+
_gradientStop2!.Offset = 0.436f;
141+
_gradientStop3!.Offset = 0.482f;
142+
_gradientStop4!.Offset = 0.643f;
143+
144+
_shimmerMaskGradient.ColorStops.Add(_gradientStop1);
145+
_shimmerMaskGradient.ColorStops.Add(_gradientStop2);
146+
_shimmerMaskGradient.ColorStops.Add(_gradientStop3);
147+
_shimmerMaskGradient.ColorStops.Add(_gradientStop4);
148+
}
149+
150+
private void SetGradientStopColorsByTheme()
151+
{
152+
switch (ActualTheme)
153+
{
154+
case ElementTheme.Default:
155+
case ElementTheme.Dark:
156+
_gradientStop1!.Color = Color.FromArgb((byte)(255 * 6.05 / 100), 255, 255, 255);
157+
_gradientStop2!.Color = Color.FromArgb((byte)(255 * 3.26 / 100), 255, 255, 255);
158+
_gradientStop3!.Color = Color.FromArgb((byte)(255 * 3.26 / 100), 255, 255, 255);
159+
_gradientStop4!.Color = Color.FromArgb((byte)(255 * 6.05 / 100), 255, 255, 255);
160+
break;
161+
case ElementTheme.Light:
162+
_gradientStop1!.Color = Color.FromArgb((byte)(255 * 5.37 / 100), 0, 0, 0);
163+
_gradientStop2!.Color = Color.FromArgb((byte)(255 * 2.89 / 100), 0, 0, 0);
164+
_gradientStop3!.Color = Color.FromArgb((byte)(255 * 2.89 / 100), 0, 0, 0);
165+
_gradientStop4!.Color = Color.FromArgb((byte)(255 * 5.37 / 100), 0, 0, 0);
166+
break;
167+
}
168+
}
169+
170+
private void TryStartAnimation()
171+
{
172+
if (_animationStarted || _initialized is false || _shape is null || _shapeVisual is null || _rectangleGeometry is null)
173+
{
174+
return;
175+
}
176+
177+
var rootVisual = _shape.GetVisual();
178+
_sizeAnimation = rootVisual.GetReference().Size;
179+
_shapeVisual.StartAnimation(nameof(ShapeVisual.Size), _sizeAnimation);
180+
_rectangleGeometry.StartAnimation(nameof(CompositionRoundedRectangleGeometry.Size), _sizeAnimation);
181+
182+
_gradientStartPointAnimation = rootVisual.Compositor.CreateVector2KeyFrameAnimation();
183+
_gradientStartPointAnimation.Duration = Duration;
184+
_gradientStartPointAnimation.IterationBehavior = AnimationIterationBehavior.Forever;
185+
_gradientStartPointAnimation.InsertKeyFrame(0.0f, new Vector2(InitialStartPointX, 0.0f));
186+
_gradientStartPointAnimation.InsertKeyFrame(1.0f, Vector2.Zero);
187+
_shimmerMaskGradient!.StartAnimation(nameof(CompositionLinearGradientBrush.StartPoint), _gradientStartPointAnimation);
188+
189+
_gradientEndPointAnimation = rootVisual.Compositor.CreateVector2KeyFrameAnimation();
190+
_gradientEndPointAnimation.Duration = Duration;
191+
_gradientEndPointAnimation.IterationBehavior = AnimationIterationBehavior.Forever;
192+
_gradientEndPointAnimation.InsertKeyFrame(0.0f, new Vector2(1.0f, 0.0f)); //Vector2.One
193+
_gradientEndPointAnimation.InsertKeyFrame(1.0f, new Vector2(-InitialStartPointX, 1.0f));
194+
_shimmerMaskGradient.StartAnimation(nameof(CompositionLinearGradientBrush.EndPoint), _gradientEndPointAnimation);
195+
196+
_animationStarted = true;
197+
}
198+
199+
private void StopAnimation()
200+
{
201+
if (_animationStarted is false)
202+
{
203+
return;
204+
}
205+
206+
_shapeVisual!.StopAnimation(nameof(ShapeVisual.Size));
207+
_rectangleGeometry!.StopAnimation(nameof(CompositionRoundedRectangleGeometry.Size));
208+
_shimmerMaskGradient!.StopAnimation(nameof(CompositionLinearGradientBrush.StartPoint));
209+
_shimmerMaskGradient.StopAnimation(nameof(CompositionLinearGradientBrush.EndPoint));
210+
211+
_sizeAnimation!.Dispose();
212+
_gradientStartPointAnimation!.Dispose();
213+
_gradientEndPointAnimation!.Dispose();
214+
_animationStarted = false;
215+
}
216+
}
217+
}

src/Files.App/Views/Layouts/ColumnLayoutPage.xaml

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -243,15 +243,13 @@
243243
Source="{x:Bind FileImage, Mode=OneWay}"
244244
Stretch="Uniform" />
245245
</ContentPresenter>
246-
<Border
246+
247+
<!-- Loading indicator -->
248+
<uc:Shimmer
247249
x:Name="TypeUnknownGlyph"
248-
Width="20"
249-
Height="20"
250-
HorizontalAlignment="Stretch"
251-
VerticalAlignment="Stretch"
252-
x:Load="{x:Bind NeedsPlaceholderGlyph, Mode=OneWay}"
253-
Background="{ThemeResource SystemChromeHighColor}"
254-
CornerRadius="4" />
250+
Margin="2"
251+
x:Load="{x:Bind NeedsPlaceholderGlyph, Mode=OneWay}" />
252+
255253
<Image
256254
x:Name="IconOverlay"
257255
Width="16"

src/Files.App/Views/Layouts/DetailsLayoutPage.xaml

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -933,15 +933,13 @@
933933
Source="{x:Bind FileImage, Mode=OneWay}"
934934
Stretch="Uniform" />
935935
</ContentPresenter>
936-
<Border
936+
937+
<!-- Loading indicator -->
938+
<uc:Shimmer
937939
x:Name="TypeUnknownGlyph"
938-
Width="20"
939-
Height="20"
940-
HorizontalAlignment="Stretch"
941-
VerticalAlignment="Stretch"
942-
x:Load="{x:Bind NeedsPlaceholderGlyph, Mode=OneWay}"
943-
Background="{ThemeResource SystemChromeHighColor}"
944-
CornerRadius="4" />
940+
Margin="2"
941+
x:Load="{x:Bind NeedsPlaceholderGlyph, Mode=OneWay}" />
942+
945943
<FontIcon
946944
x:Name="WebShortcutGlyph"
947945
x:Load="{x:Bind LoadWebShortcutGlyph, Mode=OneWay}"

0 commit comments

Comments
 (0)