Skip to content

Commit 00302d3

Browse files
committed
feat: Drawer with animated sliding panel
1 parent 777ff91 commit 00302d3

3 files changed

Lines changed: 360 additions & 1 deletion

File tree

src/Wpf.Ui.Test/MainWindow.xaml

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1026,10 +1026,132 @@
10261026
RowHeight="35">
10271027
<TextBlock VerticalAlignment="Center" Text="Title1" />
10281028
<Button Content="Button1" />
1029-
<TextBlock VerticalAlignment="Center" Text="Title22222222" />
1029+
<TextBlock VerticalAlignment="Center" Text="Title 1" />
10301030
<Button Content="Button2" />
10311031
</ui:AutoGrid>
10321032
</StackPanel>
1033+
<StackPanel>
1034+
<TextBlock
1035+
MinWidth="120"
1036+
Margin="0,8,0,0"
1037+
FontSize="14"
1038+
FontWeight="Black"
1039+
Text="Drawer" />
1040+
<ui:StackPanel
1041+
Margin="0,8,0,0"
1042+
Orientation="Horizontal"
1043+
Spacing="8">
1044+
<Button
1045+
Command="{Binding ShowDrawerCommand}"
1046+
CommandParameter="Left"
1047+
Content="Show Left Drawer" />
1048+
<Button
1049+
Command="{Binding ShowDrawerCommand}"
1050+
CommandParameter="Top"
1051+
Content="Show Top Drawer" />
1052+
<Button
1053+
Command="{Binding ShowDrawerCommand}"
1054+
CommandParameter="Right"
1055+
Content="Show Right Drawer" />
1056+
<Button
1057+
Command="{Binding ShowDrawerCommand}"
1058+
CommandParameter="Bottom"
1059+
Content="Show Bottom Drawer" />
1060+
</ui:StackPanel>
1061+
</StackPanel>
1062+
<StackPanel Margin="0,8,0,0">
1063+
<ui:Grid
1064+
Margin="0,100,0,100"
1065+
Background="#10999999"
1066+
ColumnDefinitions="Auto,*,Auto"
1067+
RowDefinitions="Auto,*,Auto">
1068+
<TextBlock
1069+
Grid.Row="0"
1070+
Grid.RowSpan="3"
1071+
Grid.Column="0"
1072+
Grid.ColumnSpan="3"
1073+
HorizontalAlignment="Center"
1074+
VerticalAlignment="Center"
1075+
FontSize="21"
1076+
Text="Container" />
1077+
<!-- Drawer Left -->
1078+
<ui:Drawer
1079+
Grid.Row="0"
1080+
Grid.Column="0"
1081+
Width="300"
1082+
IsOpen="{Binding IsOpenOfLeftDrawer}"
1083+
Placement="Left">
1084+
<Border
1085+
Width="300"
1086+
Height="100"
1087+
Background="Black"
1088+
CornerRadius="9">
1089+
<TextBlock
1090+
HorizontalAlignment="Center"
1091+
VerticalAlignment="Center"
1092+
FontSize="21"
1093+
Text="Drawer - Left" />
1094+
</Border>
1095+
</ui:Drawer>
1096+
<!-- Drawer Top -->
1097+
<ui:Drawer
1098+
Grid.Row="0"
1099+
Grid.Column="1"
1100+
Width="300"
1101+
IsOpen="{Binding IsOpenOfTopDrawer}"
1102+
Placement="Top">
1103+
<Border
1104+
Width="300"
1105+
Height="100"
1106+
Background="Black"
1107+
CornerRadius="9">
1108+
<TextBlock
1109+
HorizontalAlignment="Center"
1110+
VerticalAlignment="Center"
1111+
FontSize="21"
1112+
Text="Drawer - Top" />
1113+
</Border>
1114+
</ui:Drawer>
1115+
<!-- Drawer Right -->
1116+
<ui:Drawer
1117+
Grid.Row="0"
1118+
Grid.Column="2"
1119+
Width="300"
1120+
IsOpen="{Binding IsOpenOfRightDrawer}"
1121+
Placement="Right">
1122+
<Border
1123+
Width="300"
1124+
Height="100"
1125+
Background="Black"
1126+
CornerRadius="9">
1127+
<TextBlock
1128+
HorizontalAlignment="Center"
1129+
VerticalAlignment="Center"
1130+
FontSize="21"
1131+
Text="Drawer - Right" />
1132+
</Border>
1133+
</ui:Drawer>
1134+
<!-- Drawer Bottom -->
1135+
<ui:Drawer
1136+
Grid.Row="0"
1137+
Grid.Column="1"
1138+
Width="300"
1139+
IsOpen="{Binding IsOpenOfBottomDrawer}"
1140+
Placement="Bottom">
1141+
<Border
1142+
Width="300"
1143+
Height="100"
1144+
Background="Black"
1145+
CornerRadius="9">
1146+
<TextBlock
1147+
HorizontalAlignment="Center"
1148+
VerticalAlignment="Center"
1149+
FontSize="21"
1150+
Text="Drawer - Bottom" />
1151+
</Border>
1152+
</ui:Drawer>
1153+
</ui:Grid>
1154+
</StackPanel>
10331155
</StackPanel>
10341156
</ScrollViewer>
10351157
</ui:Grid>

src/Wpf.Ui.Test/MainWindow.xaml.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,31 @@ private async Task ShowAsyncPendingBoxAsync()
561561
using STAThread<IPendingHandler> pending = PendingBox.ShowAsync();
562562
await Task.Delay(3000);
563563
}
564+
565+
[ObservableProperty]
566+
private bool isOpenOfLeftDrawer = false;
567+
568+
[ObservableProperty]
569+
private bool isOpenOfTopDrawer = false;
570+
571+
[ObservableProperty]
572+
private bool isOpenOfRightDrawer = false;
573+
574+
[ObservableProperty]
575+
private bool isOpenOfBottomDrawer = false;
576+
577+
[RelayCommand]
578+
private void ShowDrawer(string placementString)
579+
{
580+
if (placementString == "Left")
581+
IsOpenOfLeftDrawer = !IsOpenOfLeftDrawer;
582+
else if (placementString == "Top")
583+
IsOpenOfTopDrawer = !IsOpenOfTopDrawer;
584+
else if (placementString == "Right")
585+
IsOpenOfRightDrawer = !IsOpenOfRightDrawer;
586+
else if (placementString == "Bottom")
587+
IsOpenOfBottomDrawer = !IsOpenOfBottomDrawer;
588+
}
564589
}
565590

566591
public partial class RegistryModel : ITreeModel
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
using System;
2+
using System.Windows;
3+
using System.Windows.Controls;
4+
using System.Windows.Markup;
5+
using System.Windows.Media;
6+
using System.Windows.Media.Animation;
7+
8+
namespace Wpf.Ui.Controls;
9+
10+
/// <summary>
11+
/// A WPF ContentControl that displays a sliding panel (Drawer) from any edge of its container.
12+
/// Supports animated open/close, placement on any side (Left, Right, Top, Bottom), and optional automatic ZIndex management.
13+
/// </summary>
14+
[ContentProperty(nameof(Content))]
15+
public class Drawer : ContentControl
16+
{
17+
public TranslateTransform TranslateTransform => (TranslateTransform)RenderTransform;
18+
19+
public Drawer()
20+
{
21+
RenderTransform = new TranslateTransform();
22+
Loaded += OnLoaded;
23+
}
24+
25+
private void OnLoaded(object sender, RoutedEventArgs e)
26+
{
27+
Loaded -= OnLoaded; ApplyPlacement(); ToggleDrawer(IsOpen, false);
28+
}
29+
30+
public static readonly DependencyProperty IsOpenProperty =
31+
DependencyProperty.Register(nameof(IsOpen), typeof(bool), typeof(Drawer), new(false, OnIsOpenChanged));
32+
33+
public bool IsOpen
34+
{
35+
get => (bool)GetValue(IsOpenProperty);
36+
set => SetValue(IsOpenProperty, value);
37+
}
38+
39+
private static void OnIsOpenChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
40+
{
41+
var drawer = (Drawer)d;
42+
drawer.ToggleDrawer((bool)e.NewValue, true);
43+
}
44+
45+
/// <summary>
46+
/// Identifies the Duration dependency property.
47+
/// Controls the animation duration (in milliseconds) for opening/closing the Drawer. Default is 300ms.
48+
/// </summary>
49+
public static readonly DependencyProperty DurationProperty =
50+
DependencyProperty.Register(nameof(Duration), typeof(int), typeof(Drawer), new(300));
51+
52+
/// <summary>
53+
/// Gets or sets the animation duration (in milliseconds) for opening/closing the Drawer.
54+
/// </summary>
55+
public int Duration
56+
{
57+
get => (int)GetValue(DurationProperty);
58+
set => SetValue(DurationProperty, value);
59+
}
60+
61+
/// <summary>
62+
/// Identifies the AutoZIndex dependency property.
63+
/// When true (default), Drawer will automatically set its ZIndex to int.MaxValue when opened,
64+
/// ensuring it appears above other sibling elements in the same Panel.
65+
/// </summary>
66+
public static readonly DependencyProperty AutoZIndexProperty
67+
= DependencyProperty.Register(nameof(AutoZIndex), typeof(bool), typeof(Drawer), new(true));
68+
69+
/// <summary>
70+
/// Gets or sets whether the Drawer will automatically set its ZIndex to the topmost value (int.MaxValue) when opened.
71+
/// Set to false if you want to control ZIndex manually.
72+
/// </summary>
73+
public bool AutoZIndex
74+
{
75+
get => (bool)GetValue(AutoZIndexProperty);
76+
set => SetValue(AutoZIndexProperty, value);
77+
}
78+
79+
public static readonly DependencyProperty PlacementProperty
80+
= DependencyProperty.Register(nameof(Placement), typeof(DrawerPlacement), typeof(Drawer), new(DrawerPlacement.Left, OnPlacementChanged));
81+
82+
public DrawerPlacement Placement
83+
{
84+
get => (DrawerPlacement)GetValue(PlacementProperty);
85+
set => SetValue(PlacementProperty, value);
86+
}
87+
88+
private static void OnPlacementChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
89+
{
90+
var drawer = (Drawer)d;
91+
drawer.ApplyPlacement();
92+
}
93+
94+
private void ApplyPlacement()
95+
{
96+
switch (Placement)
97+
{
98+
case DrawerPlacement.Left:
99+
(Content as FrameworkElement)?.HorizontalAlignment = HorizontalAlignment.Left;
100+
(Content as FrameworkElement)?.VerticalAlignment = VerticalAlignment.Stretch;
101+
break;
102+
103+
case DrawerPlacement.Right:
104+
(Content as FrameworkElement)?.HorizontalAlignment = HorizontalAlignment.Right;
105+
(Content as FrameworkElement)?.VerticalAlignment = VerticalAlignment.Stretch;
106+
break;
107+
108+
case DrawerPlacement.Top:
109+
(Content as FrameworkElement)?.VerticalAlignment = VerticalAlignment.Top;
110+
(Content as FrameworkElement)?.HorizontalAlignment = HorizontalAlignment.Stretch;
111+
break;
112+
113+
case DrawerPlacement.Bottom:
114+
(Content as FrameworkElement)?.VerticalAlignment = VerticalAlignment.Bottom;
115+
(Content as FrameworkElement)?.HorizontalAlignment = HorizontalAlignment.Stretch;
116+
break;
117+
}
118+
}
119+
120+
private void ToggleDrawer(bool isOpen, bool animated)
121+
{
122+
double targetX = Placement switch
123+
{
124+
DrawerPlacement.Left => isOpen ? 0 : (IsLoaded ? -ActualWidth : -Width),
125+
DrawerPlacement.Right => isOpen ? 0 : (IsLoaded ? ActualWidth : Width),
126+
_ => 0
127+
};
128+
129+
double targetY = Placement switch
130+
{
131+
DrawerPlacement.Top => isOpen ? 0 : (IsLoaded ? -ActualHeight : -Height),
132+
DrawerPlacement.Bottom => isOpen ? 0 : (IsLoaded ? ActualHeight : Height),
133+
_ => 0
134+
};
135+
136+
// Automatically set ZIndex to topmost if enabled and opening
137+
if (isOpen && AutoZIndex)
138+
{
139+
Panel.SetZIndex(this, int.MaxValue);
140+
}
141+
142+
if (animated)
143+
{
144+
TimeSpan duration = TimeSpan.FromMilliseconds(Duration);
145+
DoubleAnimation animX = new()
146+
{
147+
To = targetX,
148+
Duration = duration,
149+
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut }
150+
};
151+
DoubleAnimation animY = new()
152+
{
153+
To = targetY,
154+
Duration = duration,
155+
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut }
156+
};
157+
158+
TranslateTransform.BeginAnimation(TranslateTransform.XProperty, animX);
159+
TranslateTransform.BeginAnimation(TranslateTransform.YProperty, animY);
160+
}
161+
else
162+
{
163+
TranslateTransform.BeginAnimation(TranslateTransform.XProperty, null);
164+
TranslateTransform.BeginAnimation(TranslateTransform.YProperty, null);
165+
TranslateTransform.X = targetX;
166+
TranslateTransform.Y = targetY;
167+
}
168+
}
169+
170+
public void Show(bool animated = true)
171+
{
172+
if (animated)
173+
IsOpen = true; // This will trigger the animation via the IsOpen property change handler
174+
else
175+
SetCurrentValue(IsOpenProperty, true);
176+
}
177+
178+
public void Hide(bool animated = true)
179+
{
180+
if (animated)
181+
IsOpen = false; // This will trigger the animation via the IsOpen property change handler
182+
else
183+
SetCurrentValue(IsOpenProperty, false);
184+
}
185+
}
186+
187+
/// <summary>
188+
/// Specifies the position from which the <see cref="Drawer"/> control will appear and how it is laid out.
189+
/// Determines the edge of the container where the Drawer is anchored and slides in/out.
190+
/// </summary>
191+
public enum DrawerPlacement
192+
{
193+
/// <summary>
194+
/// Drawer is anchored to the left edge and slides horizontally from the left.
195+
/// </summary>
196+
Left,
197+
198+
/// <summary>
199+
/// Drawer is anchored to the right edge and slides horizontally from the right.
200+
/// </summary>
201+
Right,
202+
203+
/// <summary>
204+
/// Drawer is anchored to the top edge and slides vertically from the top.
205+
/// </summary>
206+
Top,
207+
208+
/// <summary>
209+
/// Drawer is anchored to the bottom edge and slides vertically from the bottom.
210+
/// </summary>
211+
Bottom,
212+
}

0 commit comments

Comments
 (0)