2using System.Threading.Tasks;
3using Microsoft.Maui.Controls;
4using Microsoft.Maui.Graphics;
14 public static readonly BindableProperty PopupContentProperty = BindableProperty.Create(
19 propertyChanged: OnPopupContentChanged);
21 public static readonly BindableProperty PlacementProperty = BindableProperty.Create(
23 typeof(PopupPlacement),
25 PopupPlacement.Center,
26 propertyChanged: OnLayoutPropertyChanged);
28 public static readonly BindableProperty AnchorPointProperty = BindableProperty.Create(
33 propertyChanged: OnLayoutPropertyChanged);
35 public static readonly BindableProperty PopupMarginProperty = BindableProperty.Create(
40 propertyChanged: OnLayoutPropertyChanged);
42 public static readonly BindableProperty PopupPaddingProperty = BindableProperty.Create(
47 propertyChanged: OnPopupPaddingChanged);
49 public static readonly BindableProperty CloseOnBackgroundTapProperty = BindableProperty.Create(
55 public static readonly BindableProperty StretchContentWidthProperty = BindableProperty.Create(
60 propertyChanged: OnLayoutPropertyChanged);
62 public static readonly BindableProperty StretchContentHeightProperty = BindableProperty.Create(
67 propertyChanged: OnLayoutPropertyChanged);
69 private readonly AbsoluteLayout root;
70 private readonly ContentView popupContainer;
72 public event EventHandler? BackgroundTapped;
76 this.HorizontalOptions = LayoutOptions.Fill;
77 this.VerticalOptions = LayoutOptions.Fill;
79 this.popupContainer =
new ContentView
81 Padding = this.PopupPadding
84 this.root =
new AbsoluteLayout
86 HorizontalOptions = LayoutOptions.Fill,
87 VerticalOptions = LayoutOptions.Fill
91 BoxView background =
new BoxView
93 BackgroundColor = Colors.Transparent,
94 HorizontalOptions = LayoutOptions.Fill,
95 VerticalOptions = LayoutOptions.Fill
97 AbsoluteLayout.SetLayoutBounds(background,
new Rect(0, 0, 1, 1));
98 AbsoluteLayout.SetLayoutFlags(background,
Microsoft.Maui.Layouts.AbsoluteLayoutFlags.All);
100 TapGestureRecognizer backgroundTap =
new();
101 backgroundTap.Tapped += this.OnPopupBackgroundTapped;
102 background.GestureRecognizers.Add(backgroundTap);
104 this.root.Children.Add(background);
106 AbsoluteLayout.SetLayoutFlags(this.popupContainer,
Microsoft.Maui.Layouts.AbsoluteLayoutFlags.None);
107 this.root.Children.Add(this.popupContainer);
109 base.Content = this.root;
117 get => (View?)this.GetValue(PopupContentProperty);
118 set => this.SetValue(PopupContentProperty, value);
126 get => (PopupPlacement)this.GetValue(PlacementProperty);
127 set => this.SetValue(PlacementProperty, value);
135 get => (Point?)this.GetValue(AnchorPointProperty);
136 set => this.SetValue(AnchorPointProperty, value);
144 get => (Thickness)this.GetValue(PopupMarginProperty);
145 set => this.SetValue(PopupMarginProperty, value);
153 get => (Thickness)this.GetValue(PopupPaddingProperty);
154 set => this.SetValue(PopupPaddingProperty, value);
162 get => (bool)this.GetValue(CloseOnBackgroundTapProperty);
163 set => this.SetValue(CloseOnBackgroundTapProperty, value);
171 get => (bool)this.GetValue(StretchContentWidthProperty);
172 set => this.SetValue(StretchContentWidthProperty, value);
180 get => (bool)this.GetValue(StretchContentHeightProperty);
181 set => this.SetValue(StretchContentHeightProperty, value);
189 this.BackgroundTapped?.Invoke(
this, EventArgs.Empty);
194 return Task.CompletedTask;
199 return Task.CompletedTask;
204 return Task.CompletedTask;
209 return Task.CompletedTask;
212 protected override void OnSizeAllocated(
double width,
double height)
214 base.OnSizeAllocated(width, height);
215 this.UpdatePopupLayout(width, height);
218 private static void OnPopupContentChanged(BindableObject bindable,
object? oldValue,
object? newValue)
220 if (bindable is BasePopupView popupView)
222 popupView.popupContainer.Content = newValue as View;
223 popupView.InvalidateMeasure();
224 popupView.UpdatePopupLayout(popupView.Width, popupView.Height);
228 private static void OnLayoutPropertyChanged(BindableObject bindable,
object? oldValue,
object? newValue)
230 if (bindable is BasePopupView popupView)
231 popupView.UpdatePopupLayout(popupView.Width, popupView.Height);
234 private static void OnPopupPaddingChanged(BindableObject bindable,
object? oldValue,
object? newValue)
236 if (bindable is BasePopupView popupView && newValue is Thickness padding)
238 popupView.popupContainer.Padding = padding;
239 popupView.InvalidateMeasure();
240 popupView.UpdatePopupLayout(popupView.Width, popupView.Height);
244 private void OnPopupBackgroundTapped(
object? sender, TappedEventArgs e)
249 if (this.popupContainer.Width > 0 &&
this.popupContainer.Height > 0)
251 Point? point = e.GetPosition(this.popupContainer);
252 if (point is Point hit &&
253 hit.X >= 0 && hit.Y >= 0 &&
254 hit.X <=
this.popupContainer.Width &&
255 hit.Y <=
this.popupContainer.Height)
261 this.BackgroundTapped?.Invoke(
this, EventArgs.Empty);
264 private void UpdatePopupLayout(
double width,
double height)
266 if (width <= 0 || height <= 0)
270 double availableWidth = Math.Max(0, width - margin.Left - margin.Right);
271 double availableHeight = Math.Max(0, height - margin.Top - margin.Bottom);
273 if (availableWidth <= 0 || availableHeight <= 0)
275 AbsoluteLayout.SetLayoutBounds(this.popupContainer,
new Rect(margin.Left, margin.Top, Math.Max(0, availableWidth), Math.Max(0, availableHeight)));
276 this.root.InvalidateMeasure();
280 Size request = this.popupContainer.Measure(availableWidth, availableHeight);
281 double contentWidth =
double.IsInfinity(request.Width) || request.Width <= 0 ? availableWidth : Math.Min(request.Width, availableWidth);
282 double contentHeight =
double.IsInfinity(request.Height) || request.Height <= 0 ? availableHeight : Math.Min(request.Height, availableHeight);
284 if (this.
Placement == PopupPlacement.Fill)
286 contentWidth = availableWidth;
287 contentHeight = availableHeight;
292 contentWidth = availableWidth;
294 contentHeight = availableHeight;
297 double x = margin.Left;
298 double y = margin.Top;
302 case PopupPlacement.Center:
303 x += (availableWidth - contentWidth) / 2;
304 y += (availableHeight - contentHeight) / 2;
306 case PopupPlacement.Top:
307 x += (availableWidth - contentWidth) / 2;
309 case PopupPlacement.Bottom:
310 x += (availableWidth - contentWidth) / 2;
311 y = height - margin.Bottom - contentHeight;
313 case PopupPlacement.Left:
314 y += (availableHeight - contentHeight) / 2;
316 case PopupPlacement.Right:
317 x = width - margin.Right - contentWidth;
318 y += (availableHeight - contentHeight) / 2;
320 case PopupPlacement.Anchor:
321 Point anchor = this.AnchorPoint ??
new Point(width / 2, height / 2);
322 x = anchor.X - (contentWidth / 2);
323 y = anchor.Y - (contentHeight / 2);
324 double minX = margin.Left;
325 double maxX = width - margin.Right - contentWidth;
326 double minY = margin.Top;
327 double maxY = height - margin.Bottom - contentHeight;
328 x = Math.Min(Math.Max(x, minX), maxX);
329 y = Math.Min(Math.Max(y, minY), maxY);
331 case PopupPlacement.Fill:
335 case PopupPlacement.TopLeft:
339 case PopupPlacement.TopRight:
340 x = width - margin.Right - contentWidth;
343 case PopupPlacement.BottomLeft:
345 y = height - margin.Bottom - contentHeight;
347 case PopupPlacement.BottomRight:
348 x = width - margin.Right - contentWidth;
349 y = height - margin.Bottom - contentHeight;
352 x += (availableWidth - contentWidth) / 2;
353 y += (availableHeight - contentHeight) / 2;
357 AbsoluteLayout.SetLayoutBounds(this.popupContainer,
new Rect(x, y, contentWidth, contentHeight));
358 this.root.InvalidateMeasure();
361 protected override void OnPropertyChanged(
string? propertyName =
null)
363 base.OnPropertyChanged(propertyName);
364 if (propertyName == nameof(this.Content) && base.Content is View content && !ReferenceEquals(content, this.root))
366 base.Content = this.root;
367 this.PopupContent = content;
Interface for views who need to react to life-cycle events.