Skip to content

Commit 2d3be83

Browse files
committed
moved Snackbar animations from XAML to code
1 parent b05de64 commit 2d3be83

File tree

3 files changed

+103
-91
lines changed

3 files changed

+103
-91
lines changed

MainDemo.Wpf/Snackbar.xaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242

4343
<materialDesign:Snackbar Grid.ColumnSpan="3" Grid.Row="4" x:Name="snackbar" Mode="HalfAutomatic" ActionClick="SnackbarActionClickHandler" />
4444
<!-- properties will be set in the order in the xaml, so set the Mode property first for the manual mode -->
45-
<materialDesign:Snackbar Grid.Column="3" Grid.Row="4" x:Name="manualSnackbar" Mode="Manual" Content="This is a manually controlled Snackbar." IsOpen="{Binding Path=IsChecked}" />
45+
<materialDesign:Snackbar Grid.Column="3" Grid.Row="4" x:Name="manualSnackbar" Mode="Manual" Content="This is a manually controlled Snackbar." IsOpen="{Binding Path=IsChecked, Mode=TwoWay}" />
4646
</Grid>
4747
</ScrollViewer>
4848
</Grid>

MaterialDesignThemes.Wpf/Snackbar.cs

Lines changed: 100 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Windows.Controls;
88
using System.Windows.Input;
99
using System.Windows.Media;
10+
using System.Windows.Media.Animation;
1011
using System.Windows.Threading;
1112

1213
namespace MaterialDesignThemes.Wpf
@@ -16,13 +17,17 @@ namespace MaterialDesignThemes.Wpf
1617
/// </summary>
1718
public class Snackbar : ContentControl
1819
{
19-
public const string PartActionButtonName = "PART_actionButton";
20+
private const string PartActionButtonName = "PART_actionButton";
21+
private const string PartContentGridName = "PART_contentGrid";
22+
private const string PartContentPanelName = "PART_contentPanel";
2023

2124
/// <summary>
22-
/// The duration of the animation in milliseconds.
25+
/// The duration of the open and close animations in milliseconds.
2326
/// </summary>
2427
public const int AnimationDuration = 300;
2528

29+
private const int OpacityAnimationHintOffset = 50;
30+
2631
/// <summary>
2732
/// The minimum timeout for a visible <see cref="Snackbar" /> in milliseconds.
2833
/// </summary>
@@ -150,22 +155,45 @@ public bool IsOpen
150155
}
151156
}
152157

153-
private async static void IsOpenChangedHandler(DependencyObject sender, DependencyPropertyChangedEventArgs args)
158+
private static void IsOpenChangedHandler(DependencyObject sender, DependencyPropertyChangedEventArgs args)
154159
{
155-
// the animations are triggered by the value of the IsOpen property
156-
160+
// trigger the animations
157161
Snackbar snackbar = (Snackbar)sender;
158162

159-
// stop the timer because it may mess up the behaviour of the Snackbar
160-
snackbar._timer?.Stop();
161-
162163
if ((bool)args.NewValue)
163164
{
164-
await snackbar.ShowAsync();
165+
if (snackbar._openStoryboard != null)
166+
{
167+
// set the duration of the dummy visibility timeout animation as it may has changed since the last call
168+
if (snackbar._dummyVisibilityAnimation != null)
169+
{
170+
int timeout = snackbar.VisibilityTimeout;
171+
172+
if (timeout < MinimumVisibilityTimeout)
173+
{
174+
timeout = MinimumVisibilityTimeout;
175+
}
176+
177+
snackbar._dummyVisibilityAnimation.Duration = TimeSpan.FromMilliseconds(timeout);
178+
}
179+
180+
// start the open animation
181+
snackbar._openStoryboard.Begin(snackbar, true);
182+
}
165183
}
166184
else
167185
{
168-
await snackbar.HideAsync();
186+
// stop the open animation if the Snackbar should close before the visibility timeout is reached
187+
if (snackbar._openStoryboard != null)
188+
{
189+
snackbar._openStoryboard.Stop(snackbar);
190+
}
191+
192+
// start the close animation
193+
if (snackbar._closeStoryboard != null)
194+
{
195+
snackbar._closeStoryboard.Begin(snackbar, true);
196+
}
169197
}
170198
}
171199

@@ -209,8 +237,9 @@ public int VisibilityTimeout
209237
}
210238
}
211239

212-
// used to implement the behaviour of the HalfAutomatic mode defined in the Material Design specs
213-
private DispatcherTimer _timer;
240+
private Storyboard _openStoryboard;
241+
private Storyboard _closeStoryboard;
242+
private DoubleAnimation _dummyVisibilityAnimation;
214243

215244
private Button _actionButton;
216245

@@ -219,13 +248,14 @@ static Snackbar()
219248
DefaultStyleKeyProperty.OverrideMetadata(typeof(Snackbar), new FrameworkPropertyMetadata(typeof(Snackbar)));
220249

221250
ContentProperty.OverrideMetadata(typeof(Snackbar), new FrameworkPropertyMetadata(ContentChangedHandler));
222-
TagProperty.OverrideMetadata(typeof(Snackbar), new FrameworkPropertyMetadata("0"));
251+
TagProperty.OverrideMetadata(typeof(Snackbar), new FrameworkPropertyMetadata(0.0));
223252
}
224253

225254
public Snackbar() : base() { }
226255

227256
public override void OnApplyTemplate()
228257
{
258+
// set the event handler for the action button from the template
229259
if (_actionButton != null)
230260
{
231261
_actionButton.Click -= ActionButtonClickHandler;
@@ -238,28 +268,80 @@ public override void OnApplyTemplate()
238268
_actionButton.Click += ActionButtonClickHandler;
239269
}
240270

271+
// Storyboard for the open animation
272+
_openStoryboard = new Storyboard();
273+
274+
// height
275+
DoubleAnimation contentPanelTagAnimation = new DoubleAnimation(0.0, 1.0, TimeSpan.FromMilliseconds(AnimationDuration));
276+
contentPanelTagAnimation.BeginTime = TimeSpan.FromMilliseconds(0.0);
277+
contentPanelTagAnimation.EasingFunction = new QuarticEase() { EasingMode = EasingMode.EaseOut };
278+
Storyboard.SetTarget(contentPanelTagAnimation, GetTemplateChild(PartContentPanelName));
279+
Storyboard.SetTargetProperty(contentPanelTagAnimation, new PropertyPath(StackPanel.TagProperty));
280+
_openStoryboard.Children.Add(contentPanelTagAnimation);
281+
282+
// opacity of the content
283+
DoubleAnimation contentGridOpacityAnimation = new DoubleAnimation(0.0, TimeSpan.FromMilliseconds(0.0));
284+
contentGridOpacityAnimation.BeginTime = TimeSpan.FromMilliseconds(0.0);
285+
Storyboard.SetTarget(contentGridOpacityAnimation, GetTemplateChild(PartContentPanelName));
286+
Storyboard.SetTargetProperty(contentGridOpacityAnimation, new PropertyPath(Grid.OpacityProperty));
287+
_openStoryboard.Children.Add(contentGridOpacityAnimation);
288+
289+
contentGridOpacityAnimation = new DoubleAnimation(0.0, 1.0, TimeSpan.FromMilliseconds(AnimationDuration - OpacityAnimationHintOffset));
290+
contentGridOpacityAnimation.BeginTime = TimeSpan.FromMilliseconds(OpacityAnimationHintOffset);
291+
contentGridOpacityAnimation.EasingFunction = new QuarticEase() { EasingMode = EasingMode.EaseOut };
292+
Storyboard.SetTarget(contentGridOpacityAnimation, GetTemplateChild(PartContentPanelName));
293+
Storyboard.SetTargetProperty(contentGridOpacityAnimation, new PropertyPath(Grid.OpacityProperty));
294+
_openStoryboard.Children.Add(contentGridOpacityAnimation);
295+
296+
// dummy animation to keep the HalfAutomatic mode Snackbar open during the visibility timeout
297+
_dummyVisibilityAnimation = new DoubleAnimation(1.0, 1.0, TimeSpan.FromMilliseconds(VisibilityTimeout));
298+
_dummyVisibilityAnimation.BeginTime = TimeSpan.FromMilliseconds(AnimationDuration);
299+
Storyboard.SetTarget(_dummyVisibilityAnimation, GetTemplateChild(PartContentPanelName));
300+
Storyboard.SetTargetProperty(_dummyVisibilityAnimation, new PropertyPath(StackPanel.TagProperty));
301+
_openStoryboard.Children.Add(_dummyVisibilityAnimation);
302+
303+
_openStoryboard.Completed += (object sender, EventArgs args) =>
304+
{
305+
// close the Snackbar after the animation in the HalfAutomatic mode
306+
if (Mode == SnackbarMode.HalfAutomatic)
307+
{
308+
IsOpen = false;
309+
}
310+
};
311+
312+
// Storyboard for the close animation
313+
_closeStoryboard = new Storyboard();
314+
315+
// height
316+
contentPanelTagAnimation = new DoubleAnimation(1.0, 0.0, TimeSpan.FromMilliseconds(AnimationDuration));
317+
contentPanelTagAnimation.BeginTime = TimeSpan.FromMilliseconds(0.0);
318+
contentPanelTagAnimation.EasingFunction = new QuarticEase() { EasingMode = EasingMode.EaseOut };
319+
Storyboard.SetTarget(contentPanelTagAnimation, GetTemplateChild(PartContentPanelName));
320+
Storyboard.SetTargetProperty(contentPanelTagAnimation, new PropertyPath(StackPanel.TagProperty));
321+
_closeStoryboard.Children.Add(contentPanelTagAnimation);
322+
241323
base.OnApplyTemplate();
242324
}
243325

244326
private async Task ShowContentAsync(object content)
245327
{
246328
if (Mode == SnackbarMode.HalfAutomatic)
247329
{
248-
// first hide the Snackbar if its already visible
330+
// first close the Snackbar if it is already visible
249331
if (IsOpen)
250332
{
251333
IsOpen = false;
252334

253-
// wait for the animation, otherwise the new content will already be shown in the hide animation
335+
// wait for the animation, otherwise the new content will already be shown in the close animation
254336
await Task.Delay(AnimationDuration);
255337
}
256338

257-
// now set the new content and show the Snackbar
339+
// now set the new content and open the Snackbar
258340
InternalContent = content;
259341

260342
if (content != null)
261343
{
262-
// only show it with content
344+
// only open it with content
263345
IsOpen = true;
264346

265347
await Task.Delay(AnimationDuration);
@@ -272,44 +354,6 @@ private async Task ShowContentAsync(object content)
272354
}
273355
}
274356

275-
private async Task ShowAsync()
276-
{
277-
// wait for the animation
278-
await Task.Delay(AnimationDuration);
279-
280-
// start the timeout in HalfAutomatic mode
281-
if (Mode == SnackbarMode.HalfAutomatic)
282-
{
283-
// start the timer which will hide the Snackbar
284-
int timeout = VisibilityTimeout;
285-
286-
if (timeout < MinimumVisibilityTimeout)
287-
{
288-
timeout = MinimumVisibilityTimeout;
289-
}
290-
291-
if (_timer == null)
292-
{
293-
_timer = new DispatcherTimer();
294-
}
295-
296-
_timer.Tick += async (object sender, EventArgs args) =>
297-
{
298-
IsOpen = false;
299-
300-
await Task.Delay(AnimationDuration);
301-
};
302-
_timer.Interval = new TimeSpan(0, 0, 0, 0, timeout);
303-
_timer.Start();
304-
}
305-
}
306-
307-
private async Task HideAsync()
308-
{
309-
// wait for the animation
310-
await Task.Delay(AnimationDuration);
311-
}
312-
313357
private async void ActionButtonClickHandler(object sender, RoutedEventArgs args)
314358
{
315359
// do not you raise the event if the Snackbar is not fully visible
@@ -320,7 +364,7 @@ private async void ActionButtonClickHandler(object sender, RoutedEventArgs args)
320364

321365
Task task = null;
322366

323-
// hide the Snackbar in HalfAutomatic mode
367+
// close the Snackbar in HalfAutomatic mode
324368
if (Mode == SnackbarMode.HalfAutomatic)
325369
{
326370
IsOpen = false;

MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.Snackbar.xaml

Lines changed: 2 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
<ControlTemplate TargetType="{x:Type wpf:Snackbar}">
2121
<Grid HorizontalAlignment="Center" VerticalAlignment="Bottom">
2222
<Border x:Name="PART_border" Background="#FF323232" CornerRadius="2" MaxWidth="568" MinWidth="288" SnapsToDevicePixels="True">
23-
<StackPanel x:Name="PART_contentPanel" HorizontalAlignment="Stretch" Orientation="Vertical">
23+
<StackPanel x:Name="PART_contentPanel" HorizontalAlignment="Stretch" Orientation="Vertical" SnapsToDevicePixels="True">
2424
<StackPanel.Tag>
2525
<system:Double>0.0</system:Double>
2626
</StackPanel.Tag>
@@ -52,7 +52,7 @@
5252
</DataTemplate>
5353
</ContentControl.Resources>
5454
</ContentControl>
55-
<Button x:Name="PART_actionButton" Grid.Column="1" Content="{Binding Path=ActionLabel, RelativeSource={RelativeSource TemplatedParent}}">
55+
<Button x:Name="PART_actionButton" Grid.Column="1" Content="{Binding Path=ActionLabel, RelativeSource={RelativeSource TemplatedParent}}" SnapsToDevicePixels="True">
5656
<Button.Style>
5757
<Style TargetType="{x:Type Button}" BasedOn="{StaticResource MaterialDesignFlatAccentButton}">
5858
<Setter Property="Margin" Value="8,0,8,0" />
@@ -66,38 +66,6 @@
6666
</StackPanel>
6767
</Border>
6868
</Grid>
69-
<ControlTemplate.Triggers>
70-
<Trigger Property="IsOpen" Value="True">
71-
<Trigger.EnterActions>
72-
<BeginStoryboard>
73-
<Storyboard>
74-
<DoubleAnimation Storyboard.TargetName="PART_contentPanel" Storyboard.TargetProperty="Tag" From="0" To="1" Duration="0:0:0.3">
75-
<DoubleAnimation.EasingFunction>
76-
<QuarticEase EasingMode="EaseOut" />
77-
</DoubleAnimation.EasingFunction>
78-
</DoubleAnimation>
79-
<DoubleAnimation Storyboard.TargetName="PART_contentGrid" Storyboard.TargetProperty="Opacity" To="0" BeginTime="0" Duration="0" />
80-
<DoubleAnimation Storyboard.TargetName="PART_contentGrid" Storyboard.TargetProperty="Opacity" From="0" To="1" BeginTime="0:0:0.05" Duration="0:0:0.25">
81-
<DoubleAnimation.EasingFunction>
82-
<QuarticEase EasingMode="EaseOut" />
83-
</DoubleAnimation.EasingFunction>
84-
</DoubleAnimation>
85-
</Storyboard>
86-
</BeginStoryboard>
87-
</Trigger.EnterActions>
88-
<Trigger.ExitActions>
89-
<BeginStoryboard>
90-
<Storyboard>
91-
<DoubleAnimation Storyboard.TargetName="PART_contentPanel" Storyboard.TargetProperty="Tag" From="1" To="0" Duration="0:0:0.3">
92-
<DoubleAnimation.EasingFunction>
93-
<QuarticEase EasingMode="EaseOut" />
94-
</DoubleAnimation.EasingFunction>
95-
</DoubleAnimation>
96-
</Storyboard>
97-
</BeginStoryboard>
98-
</Trigger.ExitActions>
99-
</Trigger>
100-
</ControlTemplate.Triggers>
10169
</ControlTemplate>
10270
</Setter.Value>
10371
</Setter>

0 commit comments

Comments
 (0)