@@ -62,78 +62,6 @@ public abstract class MauiView : UIView, ICrossPlatformLayoutBacking, IVisualTre
6262 /// </summary>
6363 bool _appliesSafeAreaAdjustments ;
6464
65- /// <summary>
66- /// Safe area override injected by CollectionView cells.
67- /// UICollectionView bypasses MAUI's arrange chain, so cells cannot use <see cref="_safeArea"/>.
68- /// Applied as internal padding by <see cref="CrossPlatformArrange"/> and
69- /// <see cref="CrossPlatformMeasure"/> (#33604, #34635).
70- /// </summary>
71- internal SafeAreaPadding CellSafeAreaOverride { get ; set ; } = SafeAreaPadding . Empty ;
72-
73- /// <summary>
74- /// Computes and applies per-cell safe area insets for a CollectionView cell.
75- /// Called from TemplatedCell/TemplatedCell2 LayoutSubviews before Arrange.
76- /// </summary>
77- internal static void ApplyCellSafeAreaOverride ( UIView cell , IView virtualView , UIView platformView )
78- {
79- if ( virtualView is ISafeAreaView2 safeView && platformView is MauiView mauiView )
80- {
81- var insets = ComputeCellSafeAreaInsets ( cell , safeView ) ;
82- mauiView . CellSafeAreaOverride = insets != UIEdgeInsets . Zero
83- ? insets . ToSafeAreaInsets ( )
84- : SafeAreaPadding . Empty ;
85- }
86- else if ( platformView is MauiView mv && ! mv . CellSafeAreaOverride . IsEmpty )
87- {
88- // Clear stale override from a previous template that implemented ISafeAreaView2.
89- mv . CellSafeAreaOverride = SafeAreaPadding . Empty ;
90- }
91- }
92-
93- /// <summary>
94- /// Computes per-cell safe area insets based on geometric overlap with the window's unsafe regions.
95- /// Returns <see cref="UIEdgeInsets.Zero"/> when all edges share the same region (e.g., default
96- /// Container×4), as the parent layout chain handles uniform safe area (#33604, #34635).
97- /// </summary>
98- static UIEdgeInsets ComputeCellSafeAreaInsets ( UIView cell , ISafeAreaView2 safeView )
99- {
100- var window = cell . Window ;
101- if ( window is null )
102- return UIEdgeInsets . Zero ;
103-
104- var windowSA = window . SafeAreaInsets ;
105- if ( windowSA == UIEdgeInsets . Zero )
106- return UIEdgeInsets . Zero ;
107-
108- var leftRegion = safeView . GetSafeAreaRegionsForEdge ( 0 ) ;
109- var topRegion = safeView . GetSafeAreaRegionsForEdge ( 1 ) ;
110- var rightRegion = safeView . GetSafeAreaRegionsForEdge ( 2 ) ;
111- var bottomRegion = safeView . GetSafeAreaRegionsForEdge ( 3 ) ;
112-
113- // Uniform edges (Container×4, None×4, All×4) are handled by the parent layout chain.
114- bool allSameRegion = leftRegion == topRegion
115- && topRegion == rightRegion
116- && rightRegion == bottomRegion ;
117-
118- if ( allSameRegion )
119- return UIEdgeInsets . Zero ;
120-
121- // Only apply insets for Container edges; SoftInput-only edges are excluded.
122- var cellInWindow = cell . ConvertRectToView ( cell . Bounds , window ) ;
123- var windowBounds = window . Bounds ;
124-
125- nfloat left = SafeAreaEdges . IsContainer ( leftRegion ) && windowSA . Left > 0
126- ? ( nfloat ) Math . Max ( 0 , ( double ) ( windowSA . Left - cellInWindow . X ) ) : 0 ;
127- nfloat top = SafeAreaEdges . IsContainer ( topRegion ) && windowSA . Top > 0
128- ? ( nfloat ) Math . Max ( 0 , ( double ) ( windowSA . Top - cellInWindow . Y ) ) : 0 ;
129- nfloat right = SafeAreaEdges . IsContainer ( rightRegion ) && windowSA . Right > 0
130- ? ( nfloat ) Math . Max ( 0 , ( double ) ( cellInWindow . Right - ( windowBounds . Width - windowSA . Right ) ) ) : 0 ;
131- nfloat bottom = SafeAreaEdges . IsContainer ( bottomRegion ) && windowSA . Bottom > 0
132- ? ( nfloat ) Math . Max ( 0 , ( double ) ( cellInWindow . Bottom - ( windowBounds . Height - windowSA . Bottom ) ) ) : 0 ;
133-
134- return new UIEdgeInsets ( top , left , bottom , right ) ;
135- }
136-
13765 // Indicates whether this view should respond to safe area insets.
13866 // Cached to avoid repeated hierarchy checks.
13967 // True if the view is an ISafeAreaView, does not ignore safe area, and is not inside a UIScrollView;
@@ -144,6 +72,13 @@ static UIEdgeInsets ComputeCellSafeAreaInsets(UIView cell, ISafeAreaView2 safeVi
14472 // Null means not yet determined. Invalidated when view hierarchy changes.
14573 bool ? _parentHandlesSafeArea ;
14674
75+ // Cached UICollectionView parent detection to avoid repeated hierarchy checks.
76+ bool ? _collectionViewDescendant ;
77+
78+ // Cached Window safe area padding for CollectionView children to detect changes.
79+ // Uses SafeAreaPadding with EqualsAtPixelLevel() to absorb sub-pixel animation noise.
80+ SafeAreaPadding _lastWindowSafeAreaPadding ;
81+
14782 // Keyboard tracking
14883 CGRect _keyboardFrame = CGRect . Empty ;
14984 bool _isKeyboardShowing ;
@@ -197,9 +132,18 @@ bool RespondsToSafeArea()
197132 // To prevent this, we ignore safe area calculations on child views when they are inside a scroll view.
198133 // The scrollview itself is responsible for applying the correct insets, and child views should not apply additional safe area logic.
199134 //
135+ // EXCEPTION: CollectionView items must handle their own safe area because UICollectionView (which inherits from UIScrollView)
136+ // does not automatically apply safe area insets to individual cells. Without this exception, CollectionView content
137+ // would render under the notch and home indicator.
138+ //
200139 // For more details and implementation specifics, see MauiScrollView.cs, which contains the logic for safe area management
201140 // within scroll views and explains how this interacts with the overall layout system.
202- _scrollViewDescendant = this . GetParentOfType < UIScrollView > ( ) is not null ;
141+ var scrollViewParent = this . GetParentOfType < UIScrollView > ( ) ;
142+ _scrollViewDescendant = scrollViewParent is not null && scrollViewParent is not UICollectionView ;
143+
144+ // Cache whether this view is inside a UICollectionView for use in CrossPlatformArrange()
145+ _collectionViewDescendant = scrollViewParent is UICollectionView ;
146+
203147 return ! _scrollViewDescendant . Value ;
204148 }
205149
@@ -366,7 +310,12 @@ void OnKeyboardWillHide(NSNotification notification)
366310
367311 SafeAreaPadding GetAdjustedSafeAreaInsets ( )
368312 {
369- var baseSafeArea = SafeAreaInsets . ToSafeAreaInsets ( ) ;
313+ // CollectionView cells don't receive SafeAreaInsetsDidChange notifications, so their SafeAreaInsets
314+ // property may be stale during layout (especially after rotation). Use Window.SafeAreaInsets instead,
315+ // which always reflects the current device orientation and safe area state.
316+ var baseSafeArea = _collectionViewDescendant == true && Window is not null
317+ ? Window . SafeAreaInsets . ToSafeAreaInsets ( )
318+ : SafeAreaInsets . ToSafeAreaInsets ( ) ;
370319
371320 // Check if keyboard-aware safe area adjustments are needed
372321 if ( View is ISafeAreaView2 safeAreaPage && _isKeyboardShowing )
@@ -571,23 +520,19 @@ public ICrossPlatformLayout? CrossPlatformLayout
571520 /// <returns>The desired size of the view</returns>
572521 Size CrossPlatformMeasure ( double widthConstraint , double heightConstraint )
573522 {
574- var effectiveSafeArea = _appliesSafeAreaAdjustments ? _safeArea
575- : ! CellSafeAreaOverride . IsEmpty ? CellSafeAreaOverride
576- : SafeAreaPadding . Empty ;
577-
578- if ( ! effectiveSafeArea . IsEmpty )
523+ if ( _appliesSafeAreaAdjustments )
579524 {
580525 // When responding to safe area, we need to adjust the constraints to account for the safe area.
581- widthConstraint -= effectiveSafeArea . HorizontalThickness ;
582- heightConstraint -= effectiveSafeArea . VerticalThickness ;
526+ widthConstraint -= _safeArea . HorizontalThickness ;
527+ heightConstraint -= _safeArea . VerticalThickness ;
583528 }
584529
585530 var crossPlatformSize = CrossPlatformLayout ? . CrossPlatformMeasure ( widthConstraint , heightConstraint ) ?? Size . Zero ;
586531
587- if ( ! effectiveSafeArea . IsEmpty )
532+ if ( _appliesSafeAreaAdjustments )
588533 {
589534 // If we're responding to the safe area, we need to add the safe area back to the size so the container can allocate the correct space
590- crossPlatformSize = new Size ( crossPlatformSize . Width + effectiveSafeArea . HorizontalThickness , crossPlatformSize . Height + effectiveSafeArea . VerticalThickness ) ;
535+ crossPlatformSize = new Size ( crossPlatformSize . Width + _safeArea . HorizontalThickness , crossPlatformSize . Height + _safeArea . VerticalThickness ) ;
591536 }
592537
593538 return crossPlatformSize ;
@@ -600,13 +545,21 @@ Size CrossPlatformMeasure(double widthConstraint, double heightConstraint)
600545 /// <param name="bounds">The bounds rectangle to arrange within</param>
601546 void CrossPlatformArrange ( CGRect bounds )
602547 {
603- if ( _appliesSafeAreaAdjustments )
548+ // Force safe area revalidation for CollectionView cells when Window safe area changes.
549+ if ( View is ISafeAreaView or ISafeAreaView2 && _collectionViewDescendant == true && Window is not null )
604550 {
605- bounds = AdjustForSafeArea ( bounds ) ;
551+ var currentWindowPadding = Window . SafeAreaInsets . ToSafeAreaInsets ( ) ;
552+ if ( ! currentWindowPadding . EqualsAtPixelLevel ( _lastWindowSafeAreaPadding ) )
553+ {
554+ _lastWindowSafeAreaPadding = currentWindowPadding ;
555+ _safeAreaInvalidated = true ;
556+ ValidateSafeArea ( ) ;
557+ }
606558 }
607- else if ( ! CellSafeAreaOverride . IsEmpty )
559+
560+ if ( _appliesSafeAreaAdjustments )
608561 {
609- bounds = CellSafeAreaOverride . InsetRect ( bounds ) ;
562+ bounds = AdjustForSafeArea ( bounds ) ;
610563 }
611564
612565 CrossPlatformLayout ? . CrossPlatformArrange ( bounds . ToRectangle ( ) ) ;
@@ -848,6 +801,8 @@ public override void MovedToWindow()
848801
849802 _scrollViewDescendant = null ;
850803 _parentHandlesSafeArea = null ;
804+ _collectionViewDescendant = null ;
805+ _lastWindowSafeAreaPadding = SafeAreaPadding . Empty ;
851806
852807 // Notify any subscribers that this view has been moved to a window
853808 _movedToWindow ? . Invoke ( this , EventArgs . Empty ) ;
0 commit comments