Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ public class MauiRecyclerView<TItemsView, TAdapter, TItemsViewSource> : Recycler
readonly DataChangeObserver _emptyCollectionObserver;
readonly DataChangeObserver _itemsUpdateScrollObserver;

// IMauiRecyclerView (Core) — true while the EmptyView adapter is active so that
// SafeArea inset logic can allow listeners on EmptyView items (#34634 gate exception).
bool IMauiRecyclerView.IsShowingEmptyView => _emptyViewAdapter != null && GetAdapter() == _emptyViewAdapter;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[critical] Compile correctness — This explicit implementation targets Microsoft.Maui.IMauiRecyclerView.IsShowingEmptyView, but the Core IMauiRecyclerView interface is still empty. This will not compile (IMauiRecyclerView does not contain a definition for IsShowingEmptyView), and the read in MauiWindowInsetListener has the same problem. Add the member to the Core interface or avoid reading it through that interface.


ScrollBarVisibility _defaultHorizontalScrollVisibility = ScrollBarVisibility.Default;
ScrollBarVisibility _defaultVerticalScrollVisibility = ScrollBarVisibility.Default;

Expand Down
64 changes: 57 additions & 7 deletions src/Core/src/Platform/Android/MauiWindowInsetListener.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using Android.Content;
Expand Down Expand Up @@ -91,20 +92,40 @@
/// Must be called on UI thread.
/// </summary>
/// <param name="view">The view to find a listener for</param>
/// <param name="applyRecyclerViewGate">
/// When true (attachment only), skips RecyclerView data-item views with default SafeAreaEdges
/// to prevent recycled views from retaining stale inset-derived padding (#34634/#34635).
/// Reset/cleanup callers must pass false (the default) so they can always find listeners
/// to clear stale padding regardless of the currently bound element's SafeAreaEdges.
/// </param>
/// <returns>The local listener if view is in a registered view hierarchy, null otherwise</returns>
internal static MauiWindowInsetListener? FindListenerForView(AView view)
internal static MauiWindowInsetListener? FindListenerForView(AView view, bool applyRecyclerViewGate = false)
{
// Walk up the view hierarchy looking for a registered view
var parent = view.Parent;
while (parent is not null)
{
// Skip setting listener on views inside nested scroll containers or AppBarLayout (except MaterialToolbar)
// We want the layout listener logic to get applied to the MaterialToolbar itself
// But we don't want any layout listeners to get applied to the children of MaterialToolbar (like the TitleView)
// CollectionView/CarouselView items are not excluded to enable per-item SafeAreaEdges control.
// Performance overhead is negligible due to early pass-through for items without insets.
// But we don't want any layout listeners to get applied to the children of MaterialToolbar (like the TitleView).
if (view is not MaterialToolbar &&
(parent is AppBarLayout || parent is MauiScrollView))
(parent is AppBarLayout ||
parent is MauiScrollView))
{
return null;
}

// Attachment gate (applyRecyclerViewGate=true only): skip RecyclerView data-item views with default
// SafeAreaEdges to prevent recycled item views from carrying stale inset-derived padding (#34634/#34635).
// EmptyView items (IsShowingEmptyView=true) are exempt — they are never recycled across data rows
// so they safely receive the listener without risk of stale-padding accumulation.
// Reset/cleanup callers use the default applyRecyclerViewGate=false so they always find the listener
// needed to clear any previously applied padding when a view is detached or SafeAreaEdges changes.
if (applyRecyclerViewGate &&
view is not MaterialToolbar &&
parent is IMauiRecyclerView mauiRecyclerView &&
!mauiRecyclerView.IsShowingEmptyView &&

Check failure on line 127 in src/Core/src/Platform/Android/MauiWindowInsetListener.cs

View check run for this annotation

Azure Pipelines / maui-pr (Build .NET MAUI Build macOS (Release))

src/Core/src/Platform/Android/MauiWindowInsetListener.cs#L127

src/Core/src/Platform/Android/MauiWindowInsetListener.cs(127,24): error CS1061: (NETCORE_ENGINEERING_TELEMETRY=Build) 'IMauiRecyclerView' does not contain a definition for 'IsShowingEmptyView' and no accessible extension method 'IsShowingEmptyView' accepting a first argument of type 'IMauiRecyclerView' could be found (are you missing a using directive or an assembly reference?)

Check failure on line 127 in src/Core/src/Platform/Android/MauiWindowInsetListener.cs

View check run for this annotation

Azure Pipelines / maui-pr (Build .NET MAUI Build macOS (Debug))

src/Core/src/Platform/Android/MauiWindowInsetListener.cs#L127

src/Core/src/Platform/Android/MauiWindowInsetListener.cs(127,24): error CS1061: (NETCORE_ENGINEERING_TELEMETRY=Build) 'IMauiRecyclerView' does not contain a definition for 'IsShowingEmptyView' and no accessible extension method 'IsShowingEmptyView' accepting a first argument of type 'IMauiRecyclerView' could be found (are you missing a using directive or an assembly reference?)

Check failure on line 127 in src/Core/src/Platform/Android/MauiWindowInsetListener.cs

View check run for this annotation

Azure Pipelines / maui-pr (Pack .NET MAUI Pack macOS)

src/Core/src/Platform/Android/MauiWindowInsetListener.cs#L127

src/Core/src/Platform/Android/MauiWindowInsetListener.cs(127,24): error CS1061: (NETCORE_ENGINEERING_TELEMETRY=Build) 'IMauiRecyclerView' does not contain a definition for 'IsShowingEmptyView' and no accessible extension method 'IsShowingEmptyView' accepting a first argument of type 'IMauiRecyclerView' could be found (are you missing a using directive or an assembly reference?)

Check failure on line 127 in src/Core/src/Platform/Android/MauiWindowInsetListener.cs

View check run for this annotation

Azure Pipelines / maui-pr (Pack .NET MAUI Pack macOS)

src/Core/src/Platform/Android/MauiWindowInsetListener.cs#L127

src/Core/src/Platform/Android/MauiWindowInsetListener.cs(127,24): error CS1061: (NETCORE_ENGINEERING_TELEMETRY=Build) 'IMauiRecyclerView' does not contain a definition for 'IsShowingEmptyView' and no accessible extension method 'IsShowingEmptyView' accepting a first argument of type 'IMauiRecyclerView' could be found (are you missing a using directive or an assembly reference?)

Check failure on line 127 in src/Core/src/Platform/Android/MauiWindowInsetListener.cs

View check run for this annotation

Azure Pipelines / maui-pr

src/Core/src/Platform/Android/MauiWindowInsetListener.cs#L127

src/Core/src/Platform/Android/MauiWindowInsetListener.cs(127,24): error CS1061: (NETCORE_ENGINEERING_TELEMETRY=Build) 'IMauiRecyclerView' does not contain a definition for 'IsShowingEmptyView' and no accessible extension method 'IsShowingEmptyView' accepting a first argument of type 'IMauiRecyclerView' could be found (are you missing a using directive or an assembly reference?)

Check failure on line 127 in src/Core/src/Platform/Android/MauiWindowInsetListener.cs

View check run for this annotation

Azure Pipelines / maui-pr

src/Core/src/Platform/Android/MauiWindowInsetListener.cs#L127

src/Core/src/Platform/Android/MauiWindowInsetListener.cs(127,24): error CS1061: (NETCORE_ENGINEERING_TELEMETRY=Build) 'IMauiRecyclerView' does not contain a definition for 'IsShowingEmptyView' and no accessible extension method 'IsShowingEmptyView' accepting a first argument of type 'IMauiRecyclerView' could be found (are you missing a using directive or an assembly reference?)

Check failure on line 127 in src/Core/src/Platform/Android/MauiWindowInsetListener.cs

View check run for this annotation

Azure Pipelines / maui-pr

src/Core/src/Platform/Android/MauiWindowInsetListener.cs#L127

src/Core/src/Platform/Android/MauiWindowInsetListener.cs(127,24): error CS1061: (NETCORE_ENGINEERING_TELEMETRY=Build) 'IMauiRecyclerView' does not contain a definition for 'IsShowingEmptyView' and no accessible extension method 'IsShowingEmptyView' accepting a first argument of type 'IMauiRecyclerView' could be found (are you missing a using directive or an assembly reference?)
!HasExplicitSafeAreaEdges(view))
{
return null;
}
Expand Down Expand Up @@ -133,6 +154,34 @@
return null;
}

// Per-element-type cache of SafeAreaEdges default values.
// The default value returned by ISafeAreaElement.SafeAreaEdgesDefaultValueCreator() is constant per
// implementing type (e.g. Layout -> Container, Border/ContentView/ContentPage -> None, ScrollView -> Default).
// The cache is therefore append-only and never needs invalidation. Caching by AView would be unsafe
// because RecyclerView rebinds the same AView to different MAUI elements during scrolling; caching
// by Type is independent of any individual instance.
static readonly ConcurrentDictionary<Type, SafeAreaEdges> _safeAreaEdgesDefaults = new();

static bool HasExplicitSafeAreaEdges(AView view)
{
if (view is not ICrossPlatformLayoutBacking { CrossPlatformLayout: ISafeAreaElement safeAreaElement })
{
return false;
}

// Hoist the current value into a local: SafeAreaEdges is a BindableProperty getter that may lazily
// initialize state on first read, so we read it exactly once.
var current = safeAreaElement.SafeAreaEdges;
var defaultEdges = _safeAreaEdgesDefaults.GetOrAdd(
safeAreaElement.GetType(),
static (_, element) => element.SafeAreaEdgesDefaultValueCreator(),
safeAreaElement);

// Use the typed IEquatable<SafeAreaEdges>.Equals to stay independent of operator overloads
// in case SafeAreaEdges evolves (e.g. into a flags-style struct) in the future.
return !current.Equals(defaultEdges);
}

/// <summary>
/// Sets up a view to use this listener for inset handling.
/// This method registers the view and attaches the listener.
Expand Down Expand Up @@ -474,8 +523,9 @@
/// <param name="context">The Android context to get the listener from</param>
public static bool TrySetMauiWindowInsetListener(this View view, Context context)
{
// Check if this view is contained within a registered view first
if (MauiWindowInsetListener.FindListenerForView(view) is MauiWindowInsetListener localListener)
// applyRecyclerViewGate=true: skip RecyclerView data-item views with default SafeAreaEdges
// to prevent recycled views from accumulating stale inset-derived padding (#34634/#34635).
if (MauiWindowInsetListener.FindListenerForView(view, applyRecyclerViewGate: true) is MauiWindowInsetListener localListener)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[major] SafeArea/Android listener lifecycle — Because default RecyclerView item roots are skipped only during TrySetMauiWindowInsetListener, a later change from default SafeAreaEdges to an explicit value is ignored. MapSafeAreaEdges can find/reset the parent listener, but it does not attach this listener and LayoutViewGroup/ContentViewGroup only request insets when _isInsetListenerSet is already true. Concrete scenario: a recycled item root attaches with default edges, then a binding/style sets SafeAreaEdges explicitly; the item still has no listener until detach/reattach.

{
ViewCompat.SetOnApplyWindowInsetsListener(view, localListener);
ViewCompat.SetWindowInsetsAnimationCallback(view, localListener);
Expand Down
Loading