diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs index 5bee0ab220dc..9b759a22f17e 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs @@ -9,6 +9,7 @@ using AndroidX.Core.View; using AndroidX.Fragment.App; using Google.Android.Material.AppBar; +using Microsoft.Maui.Platform; using AndroidAnimation = Android.Views.Animations.Animation; using AnimationSet = Android.Views.Animations.AnimationSet; using AToolbar = AndroidX.AppCompat.Widget.Toolbar; @@ -73,6 +74,8 @@ void IAppearanceObserver.OnAppearanceChanged(ShellAppearance appearance) IShellToolbarTracker _toolbarTracker; bool _disposed; bool _destroyed; + GlobalWindowInsetListener _localInsetListener; + CoordinatorLayout _managedCoordinatorLayout; public ShellContentFragment(IShellContext shellContext, ShellContent shellContent) { @@ -135,6 +138,13 @@ public override AView OnCreateView(LayoutInflater inflater, ViewGroup container, _root = inflater.Inflate(Controls.Resource.Layout.shellcontent, null).JavaCast(); + // Set up the CoordinatorLayout with a local inset listener + if (_root is CoordinatorLayout rootLayout) + { + _localInsetListener = new GlobalWindowInsetListener(); + _managedCoordinatorLayout = rootLayout; + _root = GlobalWindowInsetListener.SetupCoordinatorLayoutWithLocalListener(rootLayout, _localInsetListener); + } var shellContentMauiContext = _shellContext.Shell.Handler.MauiContext.MakeScoped(layoutInflater: inflater, fragmentManager: ChildFragmentManager); Maui.IElement parentElement = (_shellContent as Maui.IElement) ?? _page; @@ -143,9 +153,6 @@ public override AView OnCreateView(LayoutInflater inflater, ViewGroup container, _toolbar = (AToolbar)shellToolbar.ToPlatform(shellContentMauiContext); var appBar = _root.FindViewById(Resource.Id.shellcontent_appbar); - - GlobalWindowInsetListenerExtensions.TrySetGlobalWindowInsetListener(_root, this.Context); - appBar.AddView(_toolbar); _viewhandler = _page.ToHandler(shellContentMauiContext); @@ -183,6 +190,12 @@ void Destroy() // to avoid the navigation `TaskCompletionSource` to be stuck forever. AnimationFinished?.Invoke(this, EventArgs.Empty); + // Clean up the coordinator layout and local listener first + if (_managedCoordinatorLayout is not null && _localInsetListener is not null) + { + GlobalWindowInsetListener.RemoveCoordinatorLayoutWithLocalListener(_managedCoordinatorLayout, _localInsetListener); + } + (_shellContext?.Shell as IShellController)?.RemoveAppearanceObserver(this); if (_shellContent != null) @@ -214,6 +227,8 @@ void Destroy() _viewhandler = null; _shellContent = null; _shellPageContainer = null; + _localInsetListener = null; + _managedCoordinatorLayout = null; } protected override void Dispose(bool disposing) diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellSectionRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellSectionRenderer.cs index 83310a7c5575..a8ba49beb83d 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellSectionRenderer.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellSectionRenderer.cs @@ -15,6 +15,8 @@ using AndroidX.ViewPager2.Widget; using Google.Android.Material.Tabs; using Microsoft.Extensions.Logging; +using Microsoft.Maui.Platform; +using Google.Android.Material.AppBar; using AToolbar = AndroidX.AppCompat.Widget.Toolbar; using AView = Android.Views.View; @@ -75,6 +77,8 @@ void AView.IOnClickListener.OnClick(AView v) IShellToolbarTracker _toolbarTracker; ViewPager2 _viewPager; bool _disposed; + GlobalWindowInsetListener _localInsetListener; + CoordinatorLayout _managedCoordinatorLayout; IShellController ShellController => _shellContext.Shell; public event EventHandler AnimationFinished; Fragment IShellObservableFragment.Fragment => this; @@ -101,7 +105,12 @@ public override AView OnCreateView(LayoutInflater inflater, ViewGroup container, var context = Context; var root = PlatformInterop.CreateShellCoordinatorLayout(context); var appbar = PlatformInterop.CreateShellAppBar(context, Resource.Attribute.appBarLayoutStyle, root); - GlobalWindowInsetListenerExtensions.TrySetGlobalWindowInsetListener(root, this.Context); + + // Set up the CoordinatorLayout with a local inset listener + _localInsetListener = new GlobalWindowInsetListener(); + _managedCoordinatorLayout = root; + root = GlobalWindowInsetListener.SetupCoordinatorLayoutWithLocalListener(root, _localInsetListener); + int actionBarHeight = context.GetActionBarHeight(); var shellToolbar = new Toolbar(shellSection); @@ -194,6 +203,12 @@ void Destroy() { if (_rootView != null) { + // Clean up the coordinator layout and local listener first + if (_managedCoordinatorLayout is not null && _localInsetListener is not null) + { + GlobalWindowInsetListener.RemoveCoordinatorLayoutWithLocalListener(_managedCoordinatorLayout, _localInsetListener); + } + UnhookEvents(); _shellContext?.Shell?.Toolbar?.Handler?.DisconnectHandler(); @@ -220,6 +235,8 @@ void Destroy() _toolbar = null; _viewPager = null; _rootView = null; + _localInsetListener = null; + _managedCoordinatorLayout = null; } diff --git a/src/Controls/src/Core/Platform/ModalNavigationManager/ModalNavigationManager.Android.cs b/src/Controls/src/Core/Platform/ModalNavigationManager/ModalNavigationManager.Android.cs index 2e6b7bf29e9b..edf70833c14b 100644 --- a/src/Controls/src/Core/Platform/ModalNavigationManager/ModalNavigationManager.Android.cs +++ b/src/Controls/src/Core/Platform/ModalNavigationManager/ModalNavigationManager.Android.cs @@ -206,7 +206,6 @@ internal class ModalFragment : DialogFragment Page _modal; IMauiContext _mauiWindowContext; NavigationRootManager? _navigationRootManager; - GlobalWindowInsetListener? _modalInsetListener; static readonly ColorDrawable TransparentColorDrawable = new(AColor.Transparent); bool _pendingAnimation = true; @@ -313,15 +312,6 @@ public override AView OnCreateView(LayoutInflater inflater, ViewGroup? container var rootView = _navigationRootManager?.RootView ?? throw new InvalidOperationException("Root view not initialized"); - var context = rootView.Context ?? inflater.Context; - if (context is not null) - { - // Modal pages get their own separate GlobalWindowInsetListener instance - // This prevents cross-contamination with the main window's inset tracking - _modalInsetListener = new GlobalWindowInsetListener(); - ViewCompat.SetOnApplyWindowInsetsListener(rootView, _modalInsetListener); - } - if (IsAnimated) { _ = new GenericGlobalLayoutListener((listener, view) => @@ -376,20 +366,6 @@ public override void OnDismiss(IDialogInterface dialog) _modal.Toolbar.Handler = null; } - // Clean up the modal's separate GlobalWindowInsetListener - if (_modalInsetListener is not null) - { - _modalInsetListener.ResetAllViews(); - _modalInsetListener.Dispose(); - _modalInsetListener = null; - } - - var rootView = _navigationRootManager?.RootView; - if (rootView is not null) - { - ViewCompat.SetOnApplyWindowInsetsListener(rootView, null); - } - _modal.Handler = null; _modal = null!; _mauiWindowContext = null!; diff --git a/src/Core/src/Handlers/View/ViewHandler.Android.cs b/src/Core/src/Handlers/View/ViewHandler.Android.cs index 48898719e0b2..55d35c6e12c2 100644 --- a/src/Core/src/Handlers/View/ViewHandler.Android.cs +++ b/src/Core/src/Handlers/View/ViewHandler.Android.cs @@ -1,6 +1,7 @@ using System; using Android.Views; using AndroidX.Core.View; +using Microsoft.Maui.Platform; using PlatformView = Android.Views.View; namespace Microsoft.Maui.Handlers @@ -260,27 +261,32 @@ internal static void MapSafeAreaEdges(IViewHandler handler, IView view) { return; } - - if (handler.MauiContext?.Context is null || handler.PlatformView is not PlatformView platformView) + + if (handler.MauiContext?.Context is null || handler.PlatformView is not View platformView) { return; } - switch (platformView) + // Use our static registry approach to find and reset the appropriate listener + var listener = GlobalWindowInsetListener.FindListenerForView(platformView); + + // Check for specific view group types that handle safe area + if (handler.PlatformView is ContentViewGroup cvg) + { + listener?.ResetAppliedSafeAreas(cvg); + cvg.MarkSafeAreaEdgeConfigurationChanged(); + } + else if (handler.PlatformView is LayoutViewGroup lvg) { - case ContentViewGroup cvg: - handler.MauiContext.Context.GetGlobalWindowInsetListener()?.ResetAppliedSafeAreas(cvg); - cvg.MarkSafeAreaEdgeConfigurationChanged(); - break; - case LayoutViewGroup lvg: - handler.MauiContext.Context.GetGlobalWindowInsetListener()?.ResetAppliedSafeAreas(lvg); - lvg.MarkSafeAreaEdgeConfigurationChanged(); - break; - case MauiScrollView msv: - handler.MauiContext.Context.GetGlobalWindowInsetListener()?.ResetAppliedSafeAreas(msv); - msv.MarkSafeAreaEdgeConfigurationChanged(); - break; + listener?.ResetAppliedSafeAreas(lvg); + lvg.MarkSafeAreaEdgeConfigurationChanged(); } + else if (handler.PlatformView is MauiScrollView msv) + { + listener?.ResetAppliedSafeAreas(msv); + msv.MarkSafeAreaEdgeConfigurationChanged(); + } + view.InvalidateMeasure(); } } diff --git a/src/Core/src/Handlers/Window/WindowHandler.Android.cs b/src/Core/src/Handlers/Window/WindowHandler.Android.cs index 51434832a9d1..a5a29386a19e 100644 --- a/src/Core/src/Handlers/Window/WindowHandler.Android.cs +++ b/src/Core/src/Handlers/Window/WindowHandler.Android.cs @@ -5,6 +5,7 @@ using AndroidX.Core.View; using AndroidX.Window.Layout; using Google.Android.Material.AppBar; +using Microsoft.Maui.Platform; using AView = Android.Views.View; using AColor = Android.Graphics.Color; using Android.Content.Res; @@ -99,6 +100,10 @@ private protected override void OnDisconnectHandler(object platformView) if (_rootManager != null) _rootManager.RootViewChanged -= OnRootViewChanged; + + // The MauiCoordinatorLayout will automatically unregister from the static registry + // when it's detached from the window, but we can ensure cleanup here as well + _rootManager = null; } void OnRootViewChanged(object? sender, EventArgs e) @@ -125,7 +130,12 @@ internal static void DisconnectHandler(NavigationRootManager? navigationRootMana var rootManager = handler.MauiContext.GetNavigationRootManager(); rootManager.Connect(window.Content); - return rootManager.RootView; + + // The NavigationRootManager creates a MauiCoordinatorLayout which automatically + // registers its GlobalWindowInsetListener in the static registry for child views to use + var rootView = rootManager.RootView; + + return rootView; } void UpdateVirtualViewFrame(Activity activity) diff --git a/src/Core/src/Platform/Android/ContentViewGroup.cs b/src/Core/src/Platform/Android/ContentViewGroup.cs index e95cab7d67dd..7cc918c741f2 100644 --- a/src/Core/src/Platform/Android/ContentViewGroup.cs +++ b/src/Core/src/Platform/Android/ContentViewGroup.cs @@ -155,7 +155,7 @@ protected override void OnConfigurationChanged(Configuration? newConfig) { base.OnConfigurationChanged(newConfig); - Context?.GetGlobalWindowInsetListener()?.ResetView(this); + GlobalWindowInsetListener.FindListenerForView(this)?.ResetView(this); _didSafeAreaEdgeConfigurationChange = true; } @@ -231,7 +231,7 @@ internal IBorderStroke? Clip { _originalPadding = (PaddingLeft, PaddingTop, PaddingRight, PaddingBottom); _hasStoredOriginalPadding = true; - } + } return SafeAreaExtensions.ApplyAdjustedSafeAreaInsetsPx(insets, CrossPlatformLayout, _context, view); } diff --git a/src/Core/src/Platform/Android/GlobalWindowInsetListener.cs b/src/Core/src/Platform/Android/GlobalWindowInsetListener.cs index 36e0e65028ff..80c35462099f 100644 --- a/src/Core/src/Platform/Android/GlobalWindowInsetListener.cs +++ b/src/Core/src/Platform/Android/GlobalWindowInsetListener.cs @@ -1,17 +1,22 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using Android.Content; using Android.Views; using AndroidX.Core.Graphics; using AndroidX.Core.View; using AndroidX.Core.Widget; +using AndroidX.CoordinatorLayout.Widget; using Google.Android.Material.AppBar; using AView = Android.Views.View; namespace Microsoft.Maui.Platform { + /// + /// Registry entry for tracking CoordinatorLayout instances and their associated listeners + /// + internal record CoordinatorLayoutEntry(WeakReference Layout, GlobalWindowInsetListener Listener); + internal class GlobalWindowInsetListener : WindowInsetsAnimationCompat.Callback, IOnApplyWindowInsetsListener { readonly HashSet _trackedViews = []; @@ -19,20 +24,135 @@ internal class GlobalWindowInsetListener : WindowInsetsAnimationCompat.Callback, AView? _pendingView; - public GlobalWindowInsetListener() : base(DispatchModeStop) - { - } + // Static tracking for CoordinatorLayouts that have local inset listeners + // No locking needed since this runs on UI thread + static readonly List _registeredCoordinatorLayouts = new(); + + /// + /// Registers a CoordinatorLayout to use this local listener instead of the global one + /// + internal void RegisterCoordinatorLayout(CoordinatorLayout coordinatorLayout) + { + // Clean up dead references and check for existing registration + for (int i = _registeredCoordinatorLayouts.Count - 1; i >= 0; i--) + { + var entry = _registeredCoordinatorLayouts[i]; + if (!entry.Layout.TryGetTarget(out var existingLayout)) + { + _registeredCoordinatorLayouts.RemoveAt(i); + } + else if (existingLayout == coordinatorLayout) + { + // Already registered, no need to add again + return; + } + } + + // Add this layout to the registry + _registeredCoordinatorLayouts.Add(new CoordinatorLayoutEntry(new WeakReference(coordinatorLayout), this)); + } + + /// + /// Unregisters a CoordinatorLayout from using this local listener + /// + internal static void UnregisterCoordinatorLayout(CoordinatorLayout coordinatorLayout) + { + for (int i = _registeredCoordinatorLayouts.Count - 1; i >= 0; i--) + { + if (_registeredCoordinatorLayouts[i].Layout.TryGetTarget(out var layout) && layout == coordinatorLayout) + { + _registeredCoordinatorLayouts.RemoveAt(i); + break; + } + } + } + + /// + /// Finds the appropriate GlobalWindowInsetListener for a given view by checking + /// if it's contained within any registered CoordinatorLayout + /// + internal static GlobalWindowInsetListener? FindListenerForView(AView view) + { + // Clean up dead references and find listener + for (int i = _registeredCoordinatorLayouts.Count - 1; i >= 0; i--) + { + var entry = _registeredCoordinatorLayouts[i]; + if (!entry.Layout.TryGetTarget(out var layout)) + { + _registeredCoordinatorLayouts.RemoveAt(i); + } + else if (IsViewContainedIn(view, layout)) + { + return entry.Listener; + } + } + + return null; + } + + /// + /// Checks if a view is contained within the specified layout's hierarchy + /// + static bool IsViewContainedIn(AView view, ViewGroup layout) + { + var parent = view.Parent; + while (parent is not null) + { + if (parent == layout) + { + return true; + } + + parent = parent.Parent; + } + return false; + } + + /// + /// Sets up a CoordinatorLayout to use this listener and handle attach/detach events + /// + internal static CoordinatorLayout SetupCoordinatorLayoutWithLocalListener(CoordinatorLayout coordinatorLayout, GlobalWindowInsetListener listener) + { + ViewCompat.SetOnApplyWindowInsetsListener(coordinatorLayout, listener); + ViewCompat.SetWindowInsetsAnimationCallback(coordinatorLayout, listener); + + listener.RegisterCoordinatorLayout(coordinatorLayout); + + return coordinatorLayout; + } + + /// + /// Removes the local listener from a CoordinatorLayout and properly cleans up + /// + internal static void RemoveCoordinatorLayoutWithLocalListener(CoordinatorLayout coordinatorLayout, GlobalWindowInsetListener listener) + { + // Remove the listener from the coordinator layout + ViewCompat.SetOnApplyWindowInsetsListener(coordinatorLayout, null); + ViewCompat.SetWindowInsetsAnimationCallback(coordinatorLayout, null); + + // Unregister from the registry + UnregisterCoordinatorLayout(coordinatorLayout); + + // Reset any tracked views within this coordinator layout + listener.ResetAppliedSafeAreas(coordinatorLayout); + } + + public GlobalWindowInsetListener() : base(DispatchModeStop) + { + } - public WindowInsetsCompat? OnApplyWindowInsets(AView? v, WindowInsetsCompat? insets) + public WindowInsetsCompat? OnApplyWindowInsets(AView? v, WindowInsetsCompat? insets) { if (insets is null || !insets.HasInsets || v is null || IsImeAnimating) { if (IsImeAnimating) + { _pendingView = v; + } return insets; } - + _pendingView = null; // Handle custom inset views first @@ -50,51 +170,48 @@ public GlobalWindowInsetListener() : base(DispatchModeStop) var systemBars = insets.GetInsets(WindowInsetsCompat.Type.SystemBars()); var displayCutout = insets.GetInsets(WindowInsetsCompat.Type.DisplayCutout()); - var leftInset = Math.Max(systemBars?.Left ?? 0, displayCutout?.Left ?? 0); - var topInset = Math.Max(systemBars?.Top ?? 0, displayCutout?.Top ?? 0); - var rightInset = Math.Max(systemBars?.Right ?? 0, displayCutout?.Right ?? 0); - var bottomInset = Math.Max(systemBars?.Bottom ?? 0, displayCutout?.Bottom ?? 0); - + // Handle MaterialToolbar special case early if (v is MaterialToolbar) { v.SetPadding(displayCutout?.Left ?? 0, 0, displayCutout?.Right ?? 0, 0); return WindowInsetsCompat.Consumed; } - // Handle special cases + // Find AppBarLayout - check direct child first, then first two children var appBarLayout = v.FindViewById(Resource.Id.navigationlayout_appbar); - if (appBarLayout is null && v is ViewGroup group) { - if (group.ChildCount > 0 && group.GetChildAt(0) is AppBarLayout firstChildAppBar) + if (group.ChildCount > 0 && group.GetChildAt(0) is AppBarLayout firstChild) { - appBarLayout = firstChildAppBar; + appBarLayout = firstChild; } - else if (group.ChildCount > 1 && group.GetChildAt(1) is AppBarLayout secondChildAppBar) + else if (group.ChildCount > 1 && group.GetChildAt(1) is AppBarLayout secondChild) { - appBarLayout = secondChildAppBar; + appBarLayout = secondChild; } } - bool appBarLayoutContainsSomething = appBarLayout?.MeasuredHeight > 0; - - for (int i = 0; i < (appBarLayout?.ChildCount ?? 0) && !appBarLayoutContainsSomething; i++) + // Check if AppBarLayout has meaningful content + bool appBarHasContent = appBarLayout?.MeasuredHeight > 0; + if (!appBarHasContent && appBarLayout is not null) { - var child = appBarLayout?.GetChildAt(i); - if (child is not null && child.MeasuredHeight > 0) + for (int i = 0; i < appBarLayout.ChildCount; i++) { - appBarLayoutContainsSomething = true; - break; + var child = appBarLayout.GetChildAt(i); + if (child?.MeasuredHeight > 0) + { + appBarHasContent = true; + break; + } } } + // Apply padding to AppBarLayout based on content and system insets if (appBarLayout is not null) { - if (appBarLayoutContainsSomething) + if (appBarHasContent) { - // Pad the AppBarLayout to avoid the navigation bar in landscape orientation and system UI in portrait. - // In landscape, the navigation bar is on the left or right edge; in portrait, we account for the status bar and display cutouts. - // Without this padding, the AppBarLayout would extend behind these system UI elements and be partially hidden or non-interactive. + var topInset = Math.Max(systemBars?.Top ?? 0, displayCutout?.Top ?? 0); appBarLayout.SetPadding(systemBars?.Left ?? 0, topInset, systemBars?.Right ?? 0, 0); } else @@ -103,11 +220,11 @@ public GlobalWindowInsetListener() : base(DispatchModeStop) } } - - var bottomNavigation = v.FindViewById(Resource.Id.navigationlayout_bottomtabs)?.MeasuredHeight > 0; - - if (bottomNavigation) + // Handle bottom navigation + var hasBottomNav = v.FindViewById(Resource.Id.navigationlayout_bottomtabs)?.MeasuredHeight > 0; + if (hasBottomNav) { + var bottomInset = Math.Max(systemBars?.Bottom ?? 0, displayCutout?.Bottom ?? 0); v.SetPadding(0, 0, 0, bottomInset); } else @@ -118,16 +235,16 @@ public GlobalWindowInsetListener() : base(DispatchModeStop) // Create new insets with consumed values var newSystemBars = Insets.Of( systemBars?.Left ?? 0, - appBarLayoutContainsSomething ? 0 : systemBars?.Top ?? 0, + appBarHasContent ? 0 : systemBars?.Top ?? 0, systemBars?.Right ?? 0, - bottomNavigation ? 0 : systemBars?.Bottom ?? 0 + hasBottomNav ? 0 : systemBars?.Bottom ?? 0 ) ?? Insets.None; var newDisplayCutout = Insets.Of( displayCutout?.Left ?? 0, - appBarLayoutContainsSomething ? 0 : displayCutout?.Top ?? 0, + appBarHasContent ? 0 : displayCutout?.Top ?? 0, displayCutout?.Right ?? 0, - bottomNavigation ? 0 : displayCutout?.Bottom ?? 0 + hasBottomNav ? 0 : displayCutout?.Bottom ?? 0 ) ?? Insets.None; return new WindowInsetsCompat.Builder(insets) @@ -155,7 +272,8 @@ public void ResetView(AView view) public void ResetAllViews() { - var viewsToReset = new List(_trackedViews); // Create a copy to avoid modification during enumeration + // Create a copy to avoid modification during enumeration + var viewsToReset = _trackedViews.ToArray(); foreach (var view in viewsToReset) { ResetView(view); @@ -186,7 +304,7 @@ public void ResetAppliedSafeAreas(AView view) /// static bool IsDescendantOf(AView? child, AView parent) { - if (child is null || parent is null) + if (child is null) { return false; } @@ -204,70 +322,51 @@ static bool IsDescendantOf(AView? child, AView parent) return false; } - protected override void Dispose(bool disposing) + public override void OnPrepare(WindowInsetsAnimationCompat? animation) { - if (disposing) + base.OnPrepare(animation); + if (IsImeAnimation(animation)) { - ResetAllViews(); + IsImeAnimating = true; } - base.Dispose(disposing); } - public override void OnPrepare(WindowInsetsAnimationCompat? animation) - { - base.OnPrepare(animation); - - if (animation is null) - return; + public override WindowInsetsAnimationCompat.BoundsCompat? OnStart(WindowInsetsAnimationCompat? animation, WindowInsetsAnimationCompat.BoundsCompat? bounds) + { + if (IsImeAnimation(animation)) + { + IsImeAnimating = true; + } - // Check if this is an IME animation - if ((animation.TypeMask & WindowInsetsCompat.Type.Ime()) != 0) - { - IsImeAnimating = true; - } - } + return bounds; + } - public override WindowInsetsAnimationCompat.BoundsCompat? OnStart(WindowInsetsAnimationCompat? animation, WindowInsetsAnimationCompat.BoundsCompat? bounds) + public override WindowInsetsCompat? OnProgress(WindowInsetsCompat? insets, IList? runningAnimations) { - if (animation is null) - return bounds; - - if ((animation.TypeMask & WindowInsetsCompat.Type.Ime()) != 0) - { - IsImeAnimating = true; - } - return bounds; - } - - public override WindowInsetsCompat? OnProgress(WindowInsetsCompat? insets, IList? runningAnimations) - { - if (insets != null && runningAnimations != null) - { - // Check for IME animations - foreach (var animation in runningAnimations) - { - if ((animation.TypeMask & WindowInsetsCompat.Type.Ime()) != 0) - { - var imeInsets = insets.GetInsets(WindowInsetsCompat.Type.Ime()); - var imeHeight = imeInsets?.Bottom ?? 0; - // IME height during animation: imeHeight - } - } - } + if (insets is null || runningAnimations is null) + { + return insets; + } + + // Process any IME animations + foreach (var animation in runningAnimations) + { + if (IsImeAnimation(animation)) + { + var imeInsets = insets.GetInsets(WindowInsetsCompat.Type.Ime()); + // IME height available as: imeInsets?.Bottom ?? 0 + break; // Only need to process one IME animation + } + } return insets; - } + } - public override void OnEnd(WindowInsetsAnimationCompat? animation) - { + public override void OnEnd(WindowInsetsAnimationCompat? animation) + { base.OnEnd(animation); - if (animation is null) - return; - - // Check if this was an IME animation - if ((animation.TypeMask & WindowInsetsCompat.Type.Ime()) != 0) - { - + if (IsImeAnimation(animation)) + { if (_pendingView is AView view) { _pendingView = null; @@ -278,12 +377,18 @@ public override void OnEnd(WindowInsetsAnimationCompat? animation) }); } else - { + { IsImeAnimating = false; } - } - } - } + } + } + + /// + /// Helper method to check if an animation involves the IME + /// + static bool IsImeAnimation(WindowInsetsAnimationCompat? animation) => + animation is not null && (animation.TypeMask & WindowInsetsCompat.Type.Ime()) != 0; + } } /// @@ -292,44 +397,30 @@ public override void OnEnd(WindowInsetsAnimationCompat? animation) internal static class GlobalWindowInsetListenerExtensions { /// - /// Gets the shared GlobalWindowInsetListener instance from the current MauiAppCompatActivity. - /// - /// The Android context - /// The shared GlobalWindowInsetListener instance, or null if not available - public static GlobalWindowInsetListener? GetGlobalWindowInsetListener(this Context context) - { - return context.GetActivity() as MauiAppCompatActivity is MauiAppCompatActivity activity - ? activity.GlobalWindowInsetListener - : null; - } - - /// - /// Sets the shared GlobalWindowInsetListener on the specified view. - /// This ensures all views use the same listener instance for coordinated inset management. + /// Sets the appropriate GlobalWindowInsetListener on the specified view. + /// This prioritizes local coordinator layout listeners over global ones. /// /// The Android view to set the listener on /// The Android context to get the listener from public static bool TrySetGlobalWindowInsetListener(this View view, Context context) { - if (view is not MaterialToolbar && view.FindParent( - (parent) => - parent is NestedScrollView || - parent is AppBarLayout || - parent is MauiScrollView) - is not null) + // Check if this view is contained within a registered CoordinatorLayout first + if (GlobalWindowInsetListener.FindListenerForView(view) is GlobalWindowInsetListener localListener) { - // Don't set the listener on views inside a NestedScrollView or AppBarLayout - return false; + ViewCompat.SetOnApplyWindowInsetsListener(view, localListener); + ViewCompat.SetWindowInsetsAnimationCallback(view, localListener); + return true; } - var listener = context.GetGlobalWindowInsetListener(); - if (listener is not null) + // Skip setting listener on views inside nested scroll containers or AppBarLayout (except MaterialToolbar) + if (view is not MaterialToolbar && + view.FindParent(parent => parent is NestedScrollView || parent is AppBarLayout || parent is MauiScrollView) is not null) { - ViewCompat.SetOnApplyWindowInsetsListener(view, listener); - ViewCompat.SetWindowInsetsAnimationCallback(view, listener); + return false; } - return true; + // If no listener available, this is likely a configuration issue but not critical + return false; } /// @@ -340,9 +431,12 @@ parent is AppBarLayout || /// The Android context to get the listener from public static void RemoveGlobalWindowInsetListener(this View view, Context context) { - var listener = context.GetGlobalWindowInsetListener(); - listener?.ResetView(view); + // Clear the listeners first ViewCompat.SetOnApplyWindowInsetsListener(view, null); ViewCompat.SetWindowInsetsAnimationCallback(view, null); + + // Reset view state - prefer local listener if available, otherwise use global + var listener = GlobalWindowInsetListener.FindListenerForView(view); + listener?.ResetView(view); } } \ No newline at end of file diff --git a/src/Core/src/Platform/Android/LayoutViewGroup.cs b/src/Core/src/Platform/Android/LayoutViewGroup.cs index b951c2543dba..fb69a9bda77d 100644 --- a/src/Core/src/Platform/Android/LayoutViewGroup.cs +++ b/src/Core/src/Platform/Android/LayoutViewGroup.cs @@ -60,7 +60,7 @@ protected override void OnDetachedFromWindow() base.OnDetachedFromWindow(); if (_isInsetListenerSet) GlobalWindowInsetListenerExtensions.RemoveGlobalWindowInsetListener(this, _context); - + _didSafeAreaEdgeConfigurationChange = true; _isInsetListenerSet = false; } @@ -172,7 +172,7 @@ protected override void OnConfigurationChanged(Configuration? newConfig) { base.OnConfigurationChanged(newConfig); - Context?.GetGlobalWindowInsetListener()?.ResetView(this); + GlobalWindowInsetListener.FindListenerForView(this)?.ResetView(this); _didSafeAreaEdgeConfigurationChange = true; } diff --git a/src/Core/src/Platform/Android/MauiAppCompatActivity.cs b/src/Core/src/Platform/Android/MauiAppCompatActivity.cs index 5b0788767b3e..839daa8cd0d5 100644 --- a/src/Core/src/Platform/Android/MauiAppCompatActivity.cs +++ b/src/Core/src/Platform/Android/MauiAppCompatActivity.cs @@ -11,16 +11,6 @@ namespace Microsoft.Maui { public partial class MauiAppCompatActivity : AppCompatActivity { - - GlobalWindowInsetListener? _globalWindowInsetListener; - - /// - /// Gets the shared GlobalWindowInsetListener instance for this activity. - /// This ensures all views use the same listener instance for coordinated inset management. - /// - internal GlobalWindowInsetListener GlobalWindowInsetListener => - _globalWindowInsetListener ??= new GlobalWindowInsetListener(); - // Override this if you want to handle the default Android behavior of restoring fragments on an application restart protected virtual bool AllowFragmentRestore => false; @@ -44,8 +34,6 @@ protected override void OnCreate(Bundle? savedInstanceState) protected override void OnDestroy() { - _globalWindowInsetListener?.Dispose(); - _globalWindowInsetListener = null; base.OnDestroy(); } diff --git a/src/Core/src/Platform/Android/MauiScrollView.cs b/src/Core/src/Platform/Android/MauiScrollView.cs index 1653344a6ee0..295f5b4f72b3 100644 --- a/src/Core/src/Platform/Android/MauiScrollView.cs +++ b/src/Core/src/Platform/Android/MauiScrollView.cs @@ -309,7 +309,7 @@ protected override void OnConfigurationChanged(Configuration? newConfig) { base.OnConfigurationChanged(newConfig); - Context?.GetGlobalWindowInsetListener()?.ResetView(this); + GlobalWindowInsetListener.FindListenerForView(this)?.ResetView(this); _didSafeAreaEdgeConfigurationChange = true; } diff --git a/src/Core/src/Platform/Android/Navigation/NavigationRootManager.cs b/src/Core/src/Platform/Android/Navigation/NavigationRootManager.cs index 37fa641fc12b..c219111d1cf4 100644 --- a/src/Core/src/Platform/Android/Navigation/NavigationRootManager.cs +++ b/src/Core/src/Platform/Android/Navigation/NavigationRootManager.cs @@ -1,4 +1,5 @@ using System; +using Android.Content; using Android.OS; using Android.Runtime; using Android.Views; @@ -17,6 +18,8 @@ public class NavigationRootManager AView? _rootView; ScopedFragment? _viewFragment; IToolbarElement? _toolbarElement; + GlobalWindowInsetListener? _localInsetListener; + CoordinatorLayout? _managedCoordinatorLayout; // TODO MAUI: temporary event to alert when rootview is ready // handlers and various bits use this to start interacting with rootview @@ -68,25 +71,26 @@ internal void Connect(IView? view, IMauiContext? mauiContext = null) } else { - navigationLayout ??= + navigationLayout = LayoutInflater .Inflate(Resource.Layout.navigationlayout, null) .JavaCast(); - _rootView = navigationLayout; - } + // Set up the CoordinatorLayout with a local inset listener + if (navigationLayout != null) + { + _localInsetListener = new GlobalWindowInsetListener(); + _managedCoordinatorLayout = navigationLayout; + navigationLayout = GlobalWindowInsetListener.SetupCoordinatorLayoutWithLocalListener(navigationLayout, _localInsetListener); + } - if (navigationLayout is CoordinatorLayout && mauiContext.Context is not null) - { - GlobalWindowInsetListenerExtensions.TrySetGlobalWindowInsetListener(navigationLayout, mauiContext.Context); - } - - // if the incoming view is a Drawer Layout then the Drawer Layout - // will be the root view and internally handle all if its view management - // this is mainly used for FlyoutView - // - // if it's not a drawer layout then we just use our default CoordinatorLayout inside navigationlayout - // and place the content there + _rootView = navigationLayout; + } // if the incoming view is a Drawer Layout then the Drawer Layout + // will be the root view and internally handle all if its view management + // this is mainly used for FlyoutView + // + // if it's not a drawer layout then we just use our default CoordinatorLayout inside navigationlayout + // and place the content there if (DrawerLayout == null) { SetContentView(view); @@ -114,6 +118,12 @@ void OnWindowContentPlatformViewCreated() public virtual void Disconnect() { + // Clean up the coordinator layout and local listener first + if (_managedCoordinatorLayout is not null && _localInsetListener is not null) + { + GlobalWindowInsetListener.RemoveCoordinatorLayoutWithLocalListener(_managedCoordinatorLayout, _localInsetListener); + } + ClearPlatformParts(); SetContentView(null); } @@ -125,6 +135,8 @@ void ClearPlatformParts() DrawerLayout = null; _rootView = null; _toolbarElement = null; + _localInsetListener = null; + _managedCoordinatorLayout = null; } IDisposable? _pendingFragment; diff --git a/src/Core/src/Platform/Android/SafeAreaExtensions.cs b/src/Core/src/Platform/Android/SafeAreaExtensions.cs index 5c029196975a..742d7e55566e 100644 --- a/src/Core/src/Platform/Android/SafeAreaExtensions.cs +++ b/src/Core/src/Platform/Android/SafeAreaExtensions.cs @@ -77,7 +77,7 @@ internal static SafeAreaRegions GetSafeAreaRegionForEdge(int edge, ICrossPlatfor } } - var globalWindowInsetsListener = context.GetGlobalWindowInsetListener(); + var globalWindowInsetsListener = GlobalWindowInsetListener.FindListenerForView(view); bool hasTrackedViews = globalWindowInsetsListener?.HasTrackedView == true; // Check intersection with view bounds to determine which edges actually need padding @@ -237,7 +237,7 @@ internal static SafeAreaRegions GetSafeAreaRegionForEdge(int edge, ICrossPlatfor } else { - newWindowInsets = windowInsets; + newWindowInsets = windowInsets; } // Fallback: return the base safe area for legacy views