1using Microsoft.Maui.Controls.PlatformConfiguration;
2using Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;
7using ControlsVisualElement =
Microsoft.Maui.Controls.VisualElement;
8using Microsoft.Maui.Devices;
9using CommunityToolkit.Maui.Core;
10using CommunityToolkit.Maui.Behaviors;
20 private readonly Grid layout;
21 private readonly ContentView topBar;
22 private readonly ContentView navBar;
23 private readonly ContentView contentHostA;
24 private readonly ContentView contentHostB;
25 private bool isSlotAActive =
true;
26 private readonly Grid popupOverlay;
27 private readonly BoxView popupBackground;
28 private readonly Grid popupHost;
30 private readonly Grid toastLayer;
31 private readonly Thickness toastBasePadding =
new Thickness(16, 32, 16, 16);
34 private View? activeToast;
35 private ToastPlacement activeToastPlacement = ToastPlacement.Top;
36 private ContentView? currentScreen;
37 private double currentKeyboardInset;
38 private bool isKeyboardVisible;
39 public event EventHandler? PopupBackgroundTapped;
40 public event EventHandler? PopupBackRequested;
47 ArgumentNullException.ThrowIfNull(keyboardInsetsService);
48 ArgumentNullException.ThrowIfNull(animationCoordinator);
49 this.keyboardInsetsService = keyboardInsetsService;
50 this.animationCoordinator = animationCoordinator;
51 this.currentKeyboardInset = this.keyboardInsetsService.
KeyboardHeight;
53 this.keyboardInsetsService.KeyboardInsetChanged += this.OnKeyboardInsetChanged;
55 this.layout =
new Grid
59 new RowDefinition { Height = GridLength.Auto },
60 new RowDefinition { Height =
new GridLength(1, GridUnitType.Star) },
61 new RowDefinition { Height = GridLength.Auto }
64 this.layout.SetDynamicResource(Grid.BackgroundColorProperty,
"SurfaceBackgroundWL");
66 this.topBar =
new ContentView { IsVisible =
false };
67 this.navBar =
new ContentView { IsVisible =
false };
68 this.contentHostA =
new ContentView();
69 this.contentHostB =
new ContentView();
70 this.contentHostA.IsVisible =
true;
71 this.contentHostB.IsVisible =
false;
73 this.popupBackground =
new BoxView { Opacity = 0, Color = Colors.Black };
74 this.popupOverlay =
new Grid { IsVisible =
false, InputTransparent =
false };
75 this.popupOverlay.Add(this.popupBackground);
76 this.popupHost =
new Grid { VerticalOptions = LayoutOptions.Fill, HorizontalOptions = LayoutOptions.Fill };
77 this.popupOverlay.Add(this.popupHost);
78 this.popupOverlay.IgnoreSafeArea =
true;
80 TapGestureRecognizer popupBackgroundTap =
new();
81 popupBackgroundTap.Tapped += this.OnPopupBackgroundTapped;
82 this.popupBackground.GestureRecognizers.Add(popupBackgroundTap);
84 this.toastLayer =
new Grid { IsVisible =
false, InputTransparent =
false, Padding = this.toastBasePadding, VerticalOptions = LayoutOptions.Fill, HorizontalOptions = LayoutOptions.Fill };
85 this.toastLayer.RowDefinitions.Add(
new RowDefinition { Height = GridLength.Auto });
86 this.toastLayer.RowDefinitions.Add(
new RowDefinition { Height =
new GridLength(1, GridUnitType.Star) });
87 this.toastLayer.RowDefinitions.Add(
new RowDefinition { Height = GridLength.Auto });
88 this.toastLayer.IgnoreSafeArea =
true;
91 this.layout.Add(this.topBar, 0, 0);
92 this.layout.Add(this.contentHostA, 0, 1);
93 this.layout.Add(this.contentHostB, 0, 1);
94 this.layout.Add(this.navBar, 0, 2);
95 this.layout.Add(this.popupOverlay);
96 Grid.SetRowSpan(this.popupOverlay, 3);
97 this.layout.Add(this.toastLayer);
98 Grid.SetRowSpan(this.toastLayer, 3);
100 this.layout.IgnoreSafeArea =
true;
101 this.On<iOS>().SetUseSafeArea(
false);
102 this.HideSoftInputOnTapped =
true;
104 this.Content = this.layout;
106 this.SetDynamicResource(BackgroundColorProperty,
"SurfaceBackgroundWL");
109 this.contentHostA.Content = loadingPageInstance;
110 this.contentHostB.Content =
null;
111 this.currentScreen = loadingPageInstance;
112 Thickness initialSafeArea = this.ApplyInsetsToHost(this.contentHostA, loadingPageInstance,
false,
false);
113 this.UpdateOverlayInsets(initialSafeArea,
false);
115 this.Dispatcher.Dispatch(async () =>
122 AppTheme CurrentTheme =
Microsoft.Maui.Controls.Application.Current?.RequestedTheme ?? AppTheme.Light;
123 StatusBarStyle StatusStyle = CurrentTheme == AppTheme.Dark ? StatusBarStyle.LightContent : StatusBarStyle.DarkContent;
124 StatusBarBehavior StatusBehaviour =
new() { StatusBarColor = Colors.Transparent, StatusBarStyle = StatusStyle };
125 this.Behaviors.Add(StatusBehaviour);
127 if (
Microsoft.Maui.Controls.Application.Current is not
null)
129 Microsoft.Maui.Controls.Application.Current.RequestedThemeChanged += (s, e) =>
131 StatusBarStyle StatusStyle = CurrentTheme == AppTheme.Dark ? StatusBarStyle.LightContent : StatusBarStyle.DarkContent;
132 StatusBehaviour.StatusBarStyle = StatusStyle;
136 this.SizeChanged += this.OnShellSizeChanged;
139 private void OnPopupBackgroundTapped(
object? sender, EventArgs e)
141 if (this.currentPopupState is
null)
143 if (!this.currentPopupState.AllowBackgroundTap)
145 this.PopupBackgroundTapped?.Invoke(
this, EventArgs.Empty);
152 ContentView ActiveSlot = this.isSlotAActive ? this.contentHostA : this.contentHostB;
153 ContentView InactiveSlot = this.isSlotAActive ? this.contentHostB : this.contentHostA;
156 InactiveSlot.Content = Screen;
157 InactiveSlot.BindingContext = Screen.BindingContext;
158 InactiveSlot.IsVisible =
true;
159 _ = this.ApplyInsetsToHost(InactiveSlot, Screen,
false,
false);
160 this.UpdateBars(Screen);
179 InactiveSlot.Opacity = 1;
180 ActiveSlot.Opacity = 1;
185 InactiveSlot.TranslationX = 0;
186 ActiveSlot.TranslationX = 0;
187 InactiveSlot.Opacity = 1;
188 ActiveSlot.Opacity = 1;
191 if (OutgoingPage is not
null)
194 catch (Exception ex) { ServiceRef.LogService.LogException(ex); }
197 ActiveSlot.IsVisible =
false;
198 ActiveSlot.Content =
null;
199 this.isSlotAActive = !this.isSlotAActive;
200 this.currentScreen = Screen;
201 ContentView NewlyActiveSlot = this.isSlotAActive ? this.contentHostA : this.contentHostB;
202 Thickness SafeAreaThickness = this.ApplyInsetsToHost(NewlyActiveSlot, Screen,
false,
true);
203 this.UpdateOverlayInsets(SafeAreaThickness,
false);
206 public async Task ShowPopup(ContentView popup, PopupTransition transition, PopupVisualState visualState)
208 ArgumentNullException.ThrowIfNull(popup);
209 ArgumentNullException.ThrowIfNull(visualState);
211 bool overlayWasVisible = this.popupOverlay.IsVisible;
212 if (!overlayWasVisible)
214 this.popupOverlay.IsVisible =
true;
215 this.popupBackground.Opacity = 0;
218 this.ApplyOverlayInteractionState(visualState);
220 double targetOpacity = visualState.OverlayOpacity;
221 double currentOpacity = this.popupBackground.Opacity;
222 if (!overlayWasVisible)
224 await this.popupBackground.FadeToAsync(targetOpacity, 150, Easing.CubicOut);
226 else if (Math.Abs(currentOpacity - targetOpacity) > 0.01)
228 await this.popupBackground.FadeToAsync(targetOpacity, 120, Easing.CubicOut);
233 popupView.BackgroundTapped -= this.OnPopupBackgroundTapped;
234 popupView.BackgroundTapped += this.OnPopupBackgroundTapped;
237 this.popupHost.Children.Add(popup);
238 this.NotifyKeyboardAwareTargets(popup);
239 popup.ZIndex = this.popupHost.Children.Count;
241 if (transition == PopupTransition.None)
245 popup.TranslationX = 0;
246 popup.TranslationY = 0;
258 if (ShowKey.HasValue)
259 await this.animationCoordinator.PlayAsync(ShowKey.
Value, popup,
null,
this.CreateAnimationContextOptions());
264 popup.TranslationX = 0;
265 popup.TranslationY = 0;
269 this.currentPopupState = visualState;
272 public async Task HidePopup(ContentView popup, PopupTransition transition, PopupVisualState? nextVisualState)
274 ArgumentNullException.ThrowIfNull(popup);
276 if (transition == PopupTransition.None)
290 if (HideKey.HasValue)
291 await this.animationCoordinator.PlayAsync(HideKey.
Value, popup,
null,
this.CreateAnimationContextOptions());
296 popup.TranslationX = 0;
297 popup.TranslationY = 0;
301 popupView.BackgroundTapped -= this.OnPopupBackgroundTapped;
303 this.popupHost.Children.Remove(popup);
305 if (nextVisualState is
null)
307 await this.popupBackground.FadeToAsync(0, 150, Easing.CubicOut);
308 this.popupOverlay.IsVisible =
false;
309 this.popupOverlay.InputTransparent =
true;
310 this.currentPopupState =
null;
314 this.ApplyOverlayInteractionState(nextVisualState);
315 double targetOpacity = nextVisualState.OverlayOpacity;
316 double currentOpacity = this.popupBackground.Opacity;
317 if (!this.popupOverlay.IsVisible)
318 this.popupOverlay.IsVisible =
true;
319 if (Math.Abs(currentOpacity - targetOpacity) > 0.01)
320 await this.popupBackground.FadeToAsync(targetOpacity, 120, Easing.CubicOut);
321 this.currentPopupState = nextVisualState;
325 public async Task ShowToast(View Toast, ToastTransition Transition = ToastTransition.SlideFromTop, ToastPlacement Placement = ToastPlacement.Top)
327 ArgumentNullException.ThrowIfNull(Toast);
328 if (this.activeToast is not
null)
329 this.toastLayer.Children.Remove(this.activeToast);
330 this.activeToast = Toast;
331 this.activeToastPlacement = Placement;
332 int TargetRow = Placement == ToastPlacement.Top ? 0 : 2;
333 Grid.SetRow(Toast, TargetRow);
334 if (!this.toastLayer.Children.Contains(Toast))
335 this.toastLayer.Children.Add(Toast);
336 if (Toast is BindableObject toastBindable)
337 this.NotifyKeyboardAwareTargets(toastBindable);
338 this.toastLayer.IsVisible =
true;
349 if (ShowKey.HasValue)
351 await this.animationCoordinator.PlayAsync(ShowKey.
Value, Toast,
null,
this.CreateAnimationContextOptions());
356 Toast.TranslationX = 0;
357 Toast.TranslationY = 0;
360 Toast.TranslationX = 0;
361 Toast.TranslationY = 0;
365 public async Task HideToast(ToastTransition Transition = ToastTransition.SlideFromTop)
367 if (this.activeToast is
null)
369 View Toast = this.activeToast;
380 if (HideKey.HasValue)
382 await this.animationCoordinator.PlayAsync(HideKey.
Value, Toast,
null,
this.CreateAnimationContextOptions());
389 this.toastLayer.Children.Remove(Toast);
390 this.activeToast =
null;
391 this.activeToastPlacement = ToastPlacement.Top;
392 if (this.toastLayer.Children.Count == 0)
393 this.toastLayer.IsVisible =
false;
398 double? ViewportWidth = this.Width > 0 ? this.Width :
null;
399 double? ViewportHeight = this.Height > 0 ? this.Height :
null;
402 KeyboardInset = this.currentKeyboardInset,
403 ViewportWidth = ViewportWidth,
404 ViewportHeight = ViewportHeight
408 private void ApplyOverlayInteractionState(PopupVisualState visualState)
410 this.popupOverlay.InputTransparent =
false;
413 private void OnShellSizeChanged(
object? sender, EventArgs e)
415 if (this.currentScreen is
null)
418 ContentView ActiveSlot = this.isSlotAActive ? this.contentHostA : this.contentHostB;
419 Thickness
SafeArea = this.ApplyInsetsToHost(ActiveSlot, this.currentScreen,
false,
true);
420 this.UpdateOverlayInsets(
SafeArea,
false);
423 private Thickness ApplyInsetsToHost(ContentView host, BindableObject screen,
bool animate,
bool notify)
425 ArgumentNullException.ThrowIfNull(screen);
426 ArgumentNullException.ThrowIfNull(host);
428 Thickness SafeAreaThickness =
SafeArea.ResolveInsetsFor(screen);
430 double KeyboardInsetForPadding = Mode == KeyboardInsetMode.Automatic ? this.ResolveEffectiveKeyboardInset(SafeAreaThickness) : 0;
431 Thickness TargetPadding =
new Thickness(
432 SafeAreaThickness.Left,
433 SafeAreaThickness.Top,
434 SafeAreaThickness.Right,
435 SafeAreaThickness.Bottom + KeyboardInsetForPadding);
437 this.SetViewPadding(host, TargetPadding, animate);
440 this.NotifyKeyboardAwareTargets(screen);
442 return SafeAreaThickness;
445 private void UpdateOverlayInsets(Thickness safeArea,
bool animate)
447 double EffectiveKeyboardInset = this.ResolveEffectiveKeyboardInset(safeArea);
448 Thickness OverlayPadding =
new Thickness(safeArea.Left, safeArea.Top, safeArea.Right, safeArea.Bottom + EffectiveKeyboardInset);
449 this.SetViewPadding(this.popupOverlay, OverlayPadding, animate);
451 Thickness ToastPadding =
new Thickness(
452 this.toastBasePadding.Left + safeArea.Left,
453 this.toastBasePadding.Top + safeArea.Top,
454 this.toastBasePadding.Right + safeArea.Right,
455 this.toastBasePadding.Bottom + safeArea.Bottom + EffectiveKeyboardInset);
456 this.SetViewPadding(this.toastLayer, ToastPadding, animate);
458 Thickness TopBarPadding =
new Thickness(safeArea.Left, safeArea.Top, safeArea.Right, 0);
459 this.SetViewPadding(this.topBar, TopBarPadding, animate);
461 Thickness NavBarPadding =
new Thickness(safeArea.Left, 0, safeArea.Right, safeArea.Bottom);
462 this.SetViewPadding(this.navBar, NavBarPadding, animate);
465 private double ResolveEffectiveKeyboardInset(Thickness safeArea)
467 double EffectiveInset = this.currentKeyboardInset;
470 if (DeviceInfo.Platform == DevicePlatform.iOS)
472 EffectiveInset -= safeArea.Bottom;
475 if (EffectiveInset < 0)
477 return EffectiveInset;
480 private void SetViewPadding(ControlsVisualElement view, Thickness targetPadding,
bool animate)
482 if (!this.TryGetPadding(view, out Thickness currentPadding))
487 this.AssignPadding(view, targetPadding);
491 if (this.AreThicknessClose(currentPadding, targetPadding))
493 this.AssignPadding(view, targetPadding);
497 const uint Duration = 180;
498 Microsoft.Maui.Controls.ViewExtensions.CancelAnimations(view);
501 progress => this.AssignPadding(view, this.InterpolateThickness(currentPadding, targetPadding, progress)),
504 easing: Easing.CubicInOut,
505 finished: (v, cancelled) =>
this.AssignPadding(view, targetPadding));
508 private bool TryGetPadding(ControlsVisualElement Element, out Thickness Padding)
512 case Layout LayoutElement:
513 Padding = LayoutElement.Padding;
515 case ContentView ContentViewElement:
516 Padding = ContentViewElement.Padding;
518 case TemplatedView TemplatedViewElement:
519 Padding = TemplatedViewElement.Padding;
521 case Border BorderElement:
522 Padding = BorderElement.Padding;
525 Padding =
new Thickness(0);
530 private void AssignPadding(ControlsVisualElement Element, Thickness Padding)
534 case Layout LayoutElement:
535 LayoutElement.Padding = Padding;
537 case ContentView ContentViewElement:
538 ContentViewElement.Padding = Padding;
540 case TemplatedView TemplatedViewElement:
541 TemplatedViewElement.Padding = Padding;
543 case Border BorderElement:
544 BorderElement.Padding = Padding;
549 private Thickness InterpolateThickness(Thickness From, Thickness To,
double Progress)
551 return new Thickness(
552 this.InterpolateDouble(From.Left, To.Left, Progress),
553 this.InterpolateDouble(From.Top, To.Top, Progress),
554 this.InterpolateDouble(From.Right, To.Right, Progress),
555 this.InterpolateDouble(From.Bottom, To.Bottom, Progress));
558 private double InterpolateDouble(
double From,
double To,
double Progress)
560 return From + ((To - From) * Progress);
563 private bool AreThicknessClose(Thickness a, Thickness b)
565 return Math.Abs(a.Left - b.Left) < 0.5 &&
566 Math.Abs(a.Top - b.Top) < 0.5 &&
567 Math.Abs(a.Right - b.Right) < 0.5 &&
568 Math.Abs(a.Bottom - b.Bottom) < 0.5;
571 private void NotifyKeyboardAwareTargets(BindableObject Screen)
573 KeyboardInsetChangedEventArgs Args =
new KeyboardInsetChangedEventArgs(this.currentKeyboardInset, this.isKeyboardVisible);
576 awareView.OnKeyboardInsetChanged(Args);
579 awareContext.OnKeyboardInsetChanged(Args);
582 private void OnKeyboardInsetChanged(
object? sender, KeyboardInsetChangedEventArgs e)
584 this.currentKeyboardInset = e.KeyboardHeight;
585 this.isKeyboardVisible = e.IsVisible;
587 if (this.currentScreen is not
null)
589 ContentView activeSlot = this.isSlotAActive ? this.contentHostA : this.contentHostB;
590 Thickness safeArea = this.ApplyInsetsToHost(activeSlot, this.currentScreen,
true,
true);
591 this.UpdateOverlayInsets(safeArea,
true);
594 foreach (
Microsoft.Maui.IView Child in
this.popupHost.Children)
596 if (Child is BindableObject bindableChild)
597 this.NotifyKeyboardAwareTargets(bindableChild);
600 if (this.activeToast is BindableObject toastBindable)
601 this.NotifyKeyboardAwareTargets(toastBindable);
604 public void UpdateBars(BindableObject Screen)
606 bool TopVisible = NavigationBars.GetTopBarVisible(Screen);
607 bool NavVisible = NavigationBars.GetNavBarVisible(Screen);
608 this.topBar.IsVisible = TopVisible;
609 this.navBar.IsVisible = NavVisible;
610 if (Screen is IBarContentProvider Provider)
612 this.topBar.Content = Provider.TopBarContent;
613 this.navBar.Content = Provider.NavBarContent;
617 this.topBar.Content =
null;
618 this.navBar.Content =
null;
621 if (ReferenceEquals(Screen, this.currentScreen))
623 ContentView ActiveSlot = this.isSlotAActive ? this.contentHostA : this.contentHostB;
624 Thickness SafeAreaThickness = this.ApplyInsetsToHost(ActiveSlot, Screen,
false,
true);
625 this.UpdateOverlayInsets(SafeAreaThickness,
false);
629 protected override bool OnBackButtonPressed()
633 if (this.popupHost.Children.Count > 0)
635 this.PopupBackRequested?.Invoke(
this, EventArgs.Empty);
638 if (this.activeToast is not
null)
640 _ = this.Dispatcher.DispatchAsync(async () => await this.HideToast());
643 NavigationService? nav = ServiceRef.Provider.GetService<INavigationService>() as NavigationService;
644 if (nav is not
null && nav.WouldHandleBack())
646 _ = this.Dispatcher.DispatchAsync(async () => await nav.HandleBackAsync());
652 ServiceRef.LogService.LogException(ex);
654 return base.OnBackButtonPressed();
669 public enum PopupTransition
677 public enum ToastTransition
684 public enum ToastPlacement
695 View TopBarContent {
get; }
696 View NavBarContent {
get; }
704 public static readonly BindableProperty TopBarVisibleProperty =
705 BindableProperty.CreateAttached(
711 public static readonly BindableProperty NavBarVisibleProperty =
712 BindableProperty.CreateAttached(
718 public static bool GetTopBarVisible(BindableObject View) => (bool)View.GetValue(TopBarVisibleProperty);
719 public static void SetTopBarVisible(BindableObject View,
bool Value) => View.SetValue(TopBarVisibleProperty, Value);
720 public static bool GetNavBarVisible(BindableObject View) => (bool)View.GetValue(NavBarVisibleProperty);
721 public static void SetNavBarVisible(BindableObject View,
bool Value) => View.SetValue(NavBarVisibleProperty, Value);
Data structure used when building animation contexts.
Keys for shell-level animations.
static AnimationKey ToastShowSlideTop
Toast slide-in from top animation key.
static AnimationKey PageSlideLeft
Page slide from right to left transition key.
static AnimationKey PopupHideSlideUp
Popup slide-up hide animation key.
static AnimationKey ToastShowFade
Toast fade-in animation key.
static AnimationKey PopupShowSlideUp
Popup slide-up show animation key.
static AnimationKey PopupShowFade
Popup fade show animation key.
static AnimationKey ToastHideSlideBottom
Toast slide-out to bottom animation key.
static AnimationKey ToastHideSlideTop
Toast slide-out to top animation key.
static AnimationKey ToastHideFade
Toast fade-out animation key.
static AnimationKey ToastShowSlideBottom
Toast slide-in from bottom animation key.
static AnimationKey PageCrossFade
Page cross-fade transition key.
static AnimationKey PopupHideFade
Popup fade hide animation key.
static AnimationKey PopupShowScale
Popup scale show animation key.
static AnimationKey PageSlideRight
Page slide from left to right transition key.
static AnimationKey PopupHideScale
Popup scale hide animation key.
Provides strongly typed animation key definitions.
Custom shell hosting app content, popups and Toast layers.
CustomShell(LoadingPage loadingPage, IKeyboardInsetsService keyboardInsetsService, IAnimationCoordinator animationCoordinator)
Initializes a new instance of CustomShell.
Helper class for attached properties To show/hide bars.
Attached properties for configuring keyboard inset handling on views.
static KeyboardInsetMode GetMode(BindableObject view)
Gets the KeyboardInsetMode value for the provided view.
A base class for all pages, intended for custom navigation with explicit life-cycle events.
virtual async Task OnDisappearingAsync()
Called when the page is about to be hidden. Triggers save state, disappearing logic,...
override Task OnInitializeAsync()
Override to register event handlers, process navigation args, etc. Called ONCE per page lifetime.
override async Task OnAppearingAsync()
Called when the page is about to be shown. Triggers restore state, appearing logic,...
Coordinates animation execution across the application.
Optional interface for pages that provide their own bar content.
Provides normalized keyboard inset information for UI components.
bool IsKeyboardVisible
Gets a value indicating whether the software keyboard is currently visible.
double KeyboardHeight
Gets the current keyboard height in device-independent units.
Presenter abstraction host for page, popup and toast transitions.
Represents a component that consumes keyboard inset updates manually.
TransitionType
Transition types for page navigation in CustomShell.
KeyboardInsetMode
Specifies how keyboard insets should be applied to a view.
Represents a strongly typed key used for identifying animations.
string Value
Gets the raw key value.