|
| 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