Skip to content

Commit 2c8cee6

Browse files
github-actions[bot]CopilotPureWeen
authored
[release/10.0.1xx-sr1] [Android] Refactor WindowInsetListener to per-view registry with MauiWindowInsetListener (#32443)
* Initial plan * Implement improved WindowInsetListener with per-coordinator layout support - Add CoordinatorLayoutEntry registry for tracking local listeners - Implement optimized IsViewContainedIn with early termination - Add proper nullability markers on all fields - Update GlobalWindowInsetListener with comprehensive thread safety documentation - Refactor IME animation handling with helper method - Remove global listener from MauiAppCompatActivity - Update ShellContentFragment and ShellSectionRenderer with local listeners - Update NavigationRootManager with local listener support - Improve code formatting and comments throughout Co-authored-by: PureWeen <[email protected]> * Complete improved WindowInsetListener implementation - Update ViewHandler.Android.cs to use registry-based approach - Update ContentViewGroup, LayoutViewGroup, MauiScrollView to use FindListenerForView - Update SafeAreaExtensions.cs to use new listener pattern - Update WindowHandler.Android.cs with cleanup comments - Remove modal-specific listener code from ModalNavigationManager.Android.cs - Format all changed files according to .editorconfig Co-authored-by: PureWeen <[email protected]> * Fix nullable annotations in files with #nullable disable - Remove '?' from field declarations in ShellContentFragment and ShellSectionRenderer - These files have #nullable disable directive so explicit nullable annotations cause errors - Fields are still properly handled as nullable through null checks Co-authored-by: PureWeen <[email protected]> * Simplify Shell fragment storage by using typed CoordinatorLayout fields - Change _root from AView to CoordinatorLayout in ShellContentFragment - Change _rootView from AView to CoordinatorLayout in ShellSectionRenderer - Remove redundant _managedCoordinatorLayout fields since _root/_rootView are now typed - GlobalWindowInsetListener registry already tracks the layout-listener pairs - NavigationRootManager keeps _managedCoordinatorLayout since _rootView can be different types Co-authored-by: PureWeen <[email protected]> * - simplify listener code * - rework a bit with how listeners are applied * - fix code so it doesn't add layout listener to everything * - fix a few wrong registrations * - fix test for CollectionView * Rename GlobalWindowInsetListener to MauiWindowInsetListener and add RegisterParentForChildViews - Rename GlobalWindowInsetListener class to MauiWindowInsetListener for better clarity - Rename GlobalWindowInsetListenerExtensions to MauiWindowInsetListenerExtensions - Update extension methods: TrySetMauiWindowInsetListener, RemoveMauiWindowInsetListener - Add RegisterParentForChildViews static method to register a parent for child insets without attaching listener to parent - Update FlyoutViewHandler to use RegisterParentForChildViews instead of instantiating listener - Update all references across codebase Co-authored-by: PureWeen <[email protected]> * Set SafeAreaEdges to None in ScrollViewTests.ShouldGrow test - Set SafeAreaEdges = SafeAreaEdges.None on childLayout (VerticalStackLayout) - Set SafeAreaEdges = SafeAreaEdges.None on scrollView (ScrollView) - Set SafeAreaEdges = SafeAreaEdges.None on parentLayout (Grid) - This ensures the test is isolated from safe area inset handling changes Co-authored-by: PureWeen <[email protected]> * Revert formatting-only changes to focus on functional changes Reverted formatting changes (indentation, whitespace) from files that had no functional changes: - EditTextExtensions.cs - ViewExtensions.cs - PickerExtensions.cs - SearchViewExtensions.cs - StepperHandlerManager.cs - MauiPageControl.cs - MauiSwipeView.cs - MauiWebView.cs - IHandleWindowInsets.cs - ShellToolbarTracker.cs - ShellFlyoutTemplatedContentRenderer.cs Kept functional change in SafeAreaExtensions.cs (MauiWindowInsetListener.FindListenerForView) while reverting formatting changes. This makes the PR focused on functional changes only, with formatting changes deferred to a separate PR. Co-authored-by: PureWeen <[email protected]> * Revert "Revert formatting-only changes to focus on functional changes" This reverts commit 534c071. --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: PureWeen <[email protected]> Co-authored-by: Shane Neuville <[email protected]>
1 parent 5762c7b commit 2c8cee6

30 files changed

+956
-790
lines changed

src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using AndroidX.Core.View;
1010
using AndroidX.Fragment.App;
1111
using Google.Android.Material.AppBar;
12+
using Microsoft.Maui.Platform;
1213
using AndroidAnimation = Android.Views.Animations.Animation;
1314
using AnimationSet = Android.Views.Animations.AnimationSet;
1415
using AToolbar = AndroidX.AppCompat.Widget.Toolbar;
@@ -66,7 +67,7 @@ void IAppearanceObserver.OnAppearanceChanged(ShellAppearance appearance)
6667
IShellToolbarAppearanceTracker _appearanceTracker;
6768
Page _page;
6869
IPlatformViewHandler _viewhandler;
69-
AView _root;
70+
CoordinatorLayout _root;
7071
ShellPageContainer _shellPageContainer;
7172
ShellContent _shellContent;
7273
AToolbar _toolbar;
@@ -135,6 +136,8 @@ public override AView OnCreateView(LayoutInflater inflater, ViewGroup container,
135136

136137
_root = inflater.Inflate(Controls.Resource.Layout.shellcontent, null).JavaCast<CoordinatorLayout>();
137138

139+
MauiWindowInsetListener.SetupViewWithLocalListener(_root);
140+
138141
var shellContentMauiContext = _shellContext.Shell.Handler.MauiContext.MakeScoped(layoutInflater: inflater, fragmentManager: ChildFragmentManager);
139142

140143
Maui.IElement parentElement = (_shellContent as Maui.IElement) ?? _page;
@@ -143,9 +146,6 @@ public override AView OnCreateView(LayoutInflater inflater, ViewGroup container,
143146
_toolbar = (AToolbar)shellToolbar.ToPlatform(shellContentMauiContext);
144147

145148
var appBar = _root.FindViewById<AppBarLayout>(Resource.Id.shellcontent_appbar);
146-
147-
GlobalWindowInsetListenerExtensions.TrySetGlobalWindowInsetListener(_root, this.Context);
148-
149149
appBar.AddView(_toolbar);
150150
_viewhandler = _page.ToHandler(shellContentMauiContext);
151151

@@ -183,6 +183,12 @@ void Destroy()
183183
// to avoid the navigation `TaskCompletionSource` to be stuck forever.
184184
AnimationFinished?.Invoke(this, EventArgs.Empty);
185185

186+
// Clean up the coordinator layout and local listener first
187+
if (_root is not null)
188+
{
189+
MauiWindowInsetListener.RemoveViewWithLocalListener(_root);
190+
}
191+
186192
(_shellContext?.Shell as IShellController)?.RemoveAppearanceObserver(this);
187193

188194
if (_shellContent != null)

src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellFlyoutTemplatedContentRenderer.cs

Lines changed: 56 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ void OnFlyoutStateChanging(object sender, AndroidX.DrawerLayout.Widget.DrawerLay
8585
// - Keep this minimal.
8686
// - Will be replaced by the planned comprehensive window insets solution.
8787
// - Do not extend; add new logic to the forthcoming implementation instead.
88-
internal class WindowsListener : Java.Lang.Object, IOnApplyWindowInsetsListener
88+
internal class WindowsListener : MauiWindowInsetListener, IOnApplyWindowInsetsListener
8989
{
9090
private WeakReference<ImageView> _bgImageRef;
9191
private WeakReference<AView> _flyoutViewRef;
@@ -100,10 +100,10 @@ public AView FlyoutView
100100

101101
return null;
102102
}
103-
set
104-
{
103+
set
104+
{
105105
_flyoutViewRef = new WeakReference<AView>(value);
106-
}
106+
}
107107
}
108108
public AView FooterView
109109
{
@@ -114,62 +114,68 @@ public AView FooterView
114114

115115
return null;
116116
}
117-
set
118-
{
117+
set
118+
{
119119
_footerViewRef = new WeakReference<AView>(value);
120-
}
120+
}
121121
}
122122

123123
public WindowsListener(ImageView bgImage)
124124
{
125125
_bgImageRef = new WeakReference<ImageView>(bgImage);
126126
}
127127

128-
public WindowInsetsCompat OnApplyWindowInsets(AView v, WindowInsetsCompat insets)
128+
public override WindowInsetsCompat OnApplyWindowInsets(AView v, WindowInsetsCompat insets)
129129
{
130130
if (insets == null || v == null)
131131
return insets;
132132

133-
// The flyout overlaps the status bar so we don't really care about insetting it
134-
var systemBars = insets.GetInsets(WindowInsetsCompat.Type.SystemBars());
135-
var displayCutout = insets.GetInsets(WindowInsetsCompat.Type.DisplayCutout());
136-
var topInset = Math.Max(systemBars?.Top ?? 0, displayCutout?.Top ?? 0);
137-
var bottomInset = Math.Max(systemBars?.Bottom ?? 0, displayCutout?.Bottom ?? 0);
138-
var appbarLayout = v.FindDescendantView<AppBarLayout>((v) => true);
133+
if (v is CoordinatorLayout)
134+
{
135+
// The flyout overlaps the status bar so we don't really care about insetting it
136+
var systemBars = insets.GetInsets(WindowInsetsCompat.Type.SystemBars());
137+
var displayCutout = insets.GetInsets(WindowInsetsCompat.Type.DisplayCutout());
138+
var topInset = Math.Max(systemBars?.Top ?? 0, displayCutout?.Top ?? 0);
139+
var bottomInset = Math.Max(systemBars?.Bottom ?? 0, displayCutout?.Bottom ?? 0);
140+
var appbarLayout = v.FindDescendantView<AppBarLayout>((v) => true);
139141

140-
int flyoutViewBottomInset = 0;
142+
int flyoutViewBottomInset = 0;
141143

142-
if (FooterView is not null)
143-
{
144-
v.SetPadding(0, 0, 0, bottomInset);
145-
flyoutViewBottomInset = 0;
146-
}
147-
else
148-
{
149-
flyoutViewBottomInset = bottomInset;
150-
v.SetPadding(0, 0, 0, 0);
151-
}
144+
if (FooterView is not null)
145+
{
146+
v.SetPadding(0, 0, 0, bottomInset);
147+
flyoutViewBottomInset = 0;
148+
}
149+
else
150+
{
151+
flyoutViewBottomInset = bottomInset;
152+
v.SetPadding(0, 0, 0, 0);
153+
}
152154

153-
if (appbarLayout.MeasuredHeight > 0)
154-
{
155-
FlyoutView?.SetPadding(0, 0, 0, flyoutViewBottomInset);
156-
appbarLayout?.SetPadding(0, topInset, 0, 0);
157-
}
158-
else
159-
{
160-
FlyoutView?.SetPadding(0, topInset, 0, flyoutViewBottomInset);
161-
appbarLayout?.SetPadding(0, 0, 0, 0);
162-
}
155+
if (appbarLayout.MeasuredHeight > 0)
156+
{
157+
FlyoutView?.SetPadding(0, 0, 0, flyoutViewBottomInset);
158+
appbarLayout?.SetPadding(0, topInset, 0, 0);
159+
}
160+
else
161+
{
162+
FlyoutView?.SetPadding(0, topInset, 0, flyoutViewBottomInset);
163+
appbarLayout?.SetPadding(0, 0, 0, 0);
164+
}
163165

164-
if (_bgImageRef != null && _bgImageRef.TryGetTarget(out var bgImage) && bgImage != null)
165-
{
166-
bgImage.SetPadding(0, topInset, 0, bottomInset);
166+
if (_bgImageRef != null && _bgImageRef.TryGetTarget(out var bgImage) && bgImage != null)
167+
{
168+
bgImage.SetPadding(0, topInset, 0, bottomInset);
169+
}
170+
171+
return WindowInsetsCompat.Consumed;
167172
}
168173

169-
return WindowInsetsCompat.Consumed;
174+
175+
return base.OnApplyWindowInsets(v, insets);
170176
}
171177
}
172-
178+
173179
protected virtual void LoadView(IShellContext shellContext)
174180
{
175181
var context = shellContext.AndroidContext;
@@ -202,7 +208,7 @@ protected virtual void LoadView(IShellContext shellContext)
202208
};
203209

204210
_windowsListener = new WindowsListener(_bgImage);
205-
ViewCompat.SetOnApplyWindowInsetsListener(coordinator, _windowsListener);
211+
MauiWindowInsetListener.SetupViewWithLocalListener(coordinator, _windowsListener);
206212

207213
UpdateFlyoutHeaderBehavior();
208214
_shellContext.Shell.PropertyChanged += OnShellPropertyChanged;
@@ -718,6 +724,12 @@ public void OnOffsetChanged(AppBarLayout appBarLayout, int verticalOffset)
718724

719725
internal void Disconnect()
720726
{
727+
728+
if (_rootView is CoordinatorLayout coordinator)
729+
{
730+
MauiWindowInsetListener.RemoveViewWithLocalListener(coordinator);
731+
}
732+
721733
if (_shellContext?.Shell != null)
722734
_shellContext.Shell.PropertyChanged -= OnShellPropertyChanged;
723735

@@ -726,14 +738,12 @@ internal void Disconnect()
726738

727739
_flyoutHeader = null;
728740

729-
if (_footerView != null)
730-
_footerView.View = null;
741+
_footerView?.View = null;
731742

732743
_headerView?.Disconnect();
733744
DisconnectRecyclerView();
734745

735-
if (_contentView != null)
736-
_contentView.View = null;
746+
_contentView?.View = null;
737747
}
738748

739749
protected override void Dispose(bool disposing)
@@ -764,8 +774,7 @@ protected override void Dispose(bool disposing)
764774
if (_headerView != null)
765775
_headerView.LayoutChange -= OnHeaderViewLayoutChange;
766776

767-
if (_contentView != null)
768-
_contentView.View = null;
777+
_contentView?.View = null;
769778

770779
_flyoutContentView?.Dispose();
771780
_headerView?.Dispose();

src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellSectionRenderer.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313
using AndroidX.Fragment.App;
1414
using AndroidX.ViewPager.Widget;
1515
using AndroidX.ViewPager2.Widget;
16+
using Google.Android.Material.AppBar;
1617
using Google.Android.Material.Tabs;
1718
using Microsoft.Extensions.Logging;
19+
using Microsoft.Maui.Platform;
1820
using AToolbar = AndroidX.AppCompat.Widget.Toolbar;
1921
using AView = Android.Views.View;
2022

@@ -66,7 +68,7 @@ void AView.IOnClickListener.OnClick(AView v)
6668
#endregion IOnClickListener
6769

6870
readonly IShellContext _shellContext;
69-
AView _rootView;
71+
CoordinatorLayout _rootView;
7072
bool _selecting;
7173
TabLayout _tablayout;
7274
IShellTabLayoutAppearanceTracker _tabLayoutAppearanceTracker;
@@ -101,7 +103,9 @@ public override AView OnCreateView(LayoutInflater inflater, ViewGroup container,
101103
var context = Context;
102104
var root = PlatformInterop.CreateShellCoordinatorLayout(context);
103105
var appbar = PlatformInterop.CreateShellAppBar(context, Resource.Attribute.appBarLayoutStyle, root);
104-
GlobalWindowInsetListenerExtensions.TrySetGlobalWindowInsetListener(root, this.Context);
106+
107+
MauiWindowInsetListener.SetupViewWithLocalListener(root);
108+
105109
int actionBarHeight = context.GetActionBarHeight();
106110

107111
var shellToolbar = new Toolbar(shellSection);
@@ -194,6 +198,12 @@ void Destroy()
194198
{
195199
if (_rootView != null)
196200
{
201+
// Clean up the coordinator layout and local listener first
202+
if (_rootView is not null)
203+
{
204+
MauiWindowInsetListener.RemoveViewWithLocalListener(_rootView);
205+
}
206+
197207
UnhookEvents();
198208

199209
_shellContext?.Shell?.Toolbar?.Handler?.DisconnectHandler();

src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellToolbarTracker.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -474,8 +474,7 @@ protected virtual async void UpdateLeftBarButtonItem(Context context, AToolbar t
474474
defaultDrawerArrowDrawable = true;
475475
}
476476

477-
if (icon != null)
478-
icon.Progress = (CanNavigateBack) ? 1 : 0;
477+
icon?.Progress = (CanNavigateBack) ? 1 : 0;
479478

480479
if (command != null || CanNavigateBack)
481480
{

src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
namespace Microsoft.Maui.Controls.Handlers.Items
1616
{
1717

18-
public class MauiRecyclerView<TItemsView, TAdapter, TItemsViewSource> : RecyclerView, IMauiRecyclerView<TItemsView>
18+
public class MauiRecyclerView<TItemsView, TAdapter, TItemsViewSource> : RecyclerView, IMauiRecyclerView<TItemsView>, IMauiRecyclerView
1919
where TItemsView : ItemsView
2020
where TAdapter : ItemsViewAdapter<TItemsView, TItemsViewSource>
2121
where TItemsViewSource : IItemsViewSource

src/Controls/src/Core/Platform/ModalNavigationManager/ModalNavigationManager.Android.cs

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88
using Android.Views;
99
using Android.Views.Animations;
1010
using AndroidX.Activity;
11+
using AndroidX.Core.View;
1112
using AndroidX.Fragment.App;
1213
using Microsoft.Maui.LifecycleEvents;
1314
using AAnimation = Android.Views.Animations.Animation;
1415
using AColor = Android.Graphics.Color;
1516
using AView = Android.Views.View;
16-
using AndroidX.Core.View;
1717

1818
namespace Microsoft.Maui.Controls.Platform
1919
{
@@ -206,7 +206,6 @@ internal class ModalFragment : DialogFragment
206206
Page _modal;
207207
IMauiContext _mauiWindowContext;
208208
NavigationRootManager? _navigationRootManager;
209-
GlobalWindowInsetListener? _modalInsetListener;
210209
static readonly ColorDrawable TransparentColorDrawable = new(AColor.Transparent);
211210
bool _pendingAnimation = true;
212211

@@ -320,15 +319,6 @@ public override AView OnCreateView(LayoutInflater inflater, ViewGroup? container
320319
var rootView = _navigationRootManager?.RootView ??
321320
throw new InvalidOperationException("Root view not initialized");
322321

323-
var context = rootView.Context ?? inflater.Context;
324-
if (context is not null)
325-
{
326-
// Modal pages get their own separate GlobalWindowInsetListener instance
327-
// This prevents cross-contamination with the main window's inset tracking
328-
_modalInsetListener = new GlobalWindowInsetListener();
329-
ViewCompat.SetOnApplyWindowInsetsListener(rootView, _modalInsetListener);
330-
}
331-
332322
if (IsAnimated)
333323
{
334324
_ = new GenericGlobalLayoutListener((listener, view) =>
@@ -383,20 +373,6 @@ public override void OnDismiss(IDialogInterface dialog)
383373
_modal.Toolbar.Handler = null;
384374
}
385375

386-
// Clean up the modal's separate GlobalWindowInsetListener
387-
if (_modalInsetListener is not null)
388-
{
389-
_modalInsetListener.ResetAllViews();
390-
_modalInsetListener.Dispose();
391-
_modalInsetListener = null;
392-
}
393-
394-
var rootView = _navigationRootManager?.RootView;
395-
if (rootView is not null)
396-
{
397-
ViewCompat.SetOnApplyWindowInsetsListener(rootView, null);
398-
}
399-
400376
_modal.Handler = null;
401377
_modal = null!;
402378
_mauiWindowContext = null!;

src/Controls/tests/DeviceTests/Elements/ScrollView/ScrollViewTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,9 +174,9 @@ public async Task ShouldGrow()
174174
var screenHeightConstraint = 600;
175175

176176
var label = new Label() { Text = "Text inside a ScrollView" };
177-
var childLayout = new VerticalStackLayout { label };
178-
var scrollView = new ScrollView() { VerticalOptions = LayoutOptions.Fill, Content = childLayout };
179-
var parentLayout = new Grid { scrollView };
177+
var childLayout = new VerticalStackLayout { SafeAreaEdges = SafeAreaEdges.None, Children = { label } };
178+
var scrollView = new ScrollView() { SafeAreaEdges = SafeAreaEdges.None, VerticalOptions = LayoutOptions.Fill, Content = childLayout };
179+
var parentLayout = new Grid { SafeAreaEdges = SafeAreaEdges.None, Children = { scrollView } };
180180

181181
var expectedHeight = 100;
182182
parentLayout.HeightRequest = expectedHeight;

src/Controls/tests/TestCases.HostApp/Elements/CollectionView/HeaderFooterGalleries/HeaderFooterView.xaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
xmlns:d="http://schemas.microsoft.com/dotnet/2021/maui/design"
55
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
66
mc:Ignorable="d"
7+
SafeAreaEdges="Container"
78
x:Class=" Maui.Controls.Sample.CollectionViewGalleries.HeaderFooterGalleries.HeaderFooterView">
89
<ContentPage.Content>
910

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using System.Collections.Generic;
2+
3+
namespace Microsoft.Maui
4+
{
5+
internal interface IMauiRecyclerView
6+
{
7+
}
8+
}

0 commit comments

Comments
 (0)