Skip to content
Merged
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
130 changes: 79 additions & 51 deletions src/Core/src/Platform/Android/SafeAreaExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,23 +63,24 @@ internal static SafeAreaRegions GetSafeAreaRegionForEdge(int edge, ICrossPlatfor
var bottom = GetSafeAreaForEdge(GetSafeAreaRegionForEdge(3, layout), baseSafeArea.Bottom, 3, isKeyboardShowing, keyboardInsets);

var globalWindowInsetsListener = MauiWindowInsetListener.FindListenerForView(view);
bool hasTrackedViews = globalWindowInsetsListener?.HasTrackedView == true;

// If this view has no safe area padding to apply, pass insets through to children
// instead of consuming them. This allows child views with SafeAreaEdges set
// to properly handle the insets even when the parent has SafeAreaEdges.None
// However, if this view was previously tracked (had padding before), we need to
// continue processing to reset the padding to 0
if (left == 0 && right == 0 && top == 0 && bottom == 0)
{
// Only pass through if this view hasn't been tracked yet
// If it was tracked, we need to reset its padding
if (globalWindowInsetsListener?.IsViewTracked(view) != true)
{
// Don't consume insets - pass them through for potential child views to handle
return windowInsets;
}
}
bool hasTrackedViews = globalWindowInsetsListener?.HasTrackedView == true;

// If this view has no safe area padding to apply, pass insets through to children
// instead of consuming them. This allows child views with SafeAreaEdges set
// to properly handle the insets even when the parent has SafeAreaEdges.None
// However, if this view was previously tracked (had padding before), we need to
// continue processing to reset the padding to 0
if (left == 0 && right == 0 && top == 0 && bottom == 0)
{
// Only pass through if this view hasn't been tracked yet
// If it was tracked, we need to reset its padding
bool isViewTracked = globalWindowInsetsListener?.IsViewTracked(view) == true;
if (!isViewTracked)
{
// Don't consume insets - pass them through for potential child views to handle
return windowInsets;
}
}


if (isKeyboardShowing &&
Expand Down Expand Up @@ -142,35 +143,43 @@ internal static SafeAreaRegions GetSafeAreaRegionForEdge(int edge, ICrossPlatfor
var screenWidth = realMetrics.WidthPixels;
var screenHeight = realMetrics.HeightPixels;

// Check if view extends beyond screen bounds - this indicates the view
// is still being positioned (e.g., during Shell fragment transitions).
// In this case, consume all insets to prevent children from processing
// invalid data, and request a re-apply after the view settles.
bool viewExtendsBeyondScreen = viewRight > screenWidth || viewBottom > screenHeight ||
viewLeft < 0 || viewTop < 0;

if (viewExtendsBeyondScreen)
{
// Request insets to be reapplied after the next layout pass
// when the view should be properly positioned.
// Don't return early - let processing continue with current insets
// to avoid visual popping, the re-apply will correct any issues.
view.Post(() => ViewCompat.RequestApplyInsets(view));
}

// Calculate actual overlap for each edge
// Top: how much the view extends into the top safe area
// If the viewTop is < 0 that means that it's most likely
// panned off the top of the screen so we don't want to apply any top inset
//
// Special case: During Shell navigation animations, the view may be positioned
// beyond the status bar area (e.g., Y=126 when status bar is 63px) and also
// extend beyond the screen bottom. This happens because the fragment animation
// slides the view in from off-screen. We detect this animating state by checking:
// 1. viewTop > top (view is below the status bar area - normal case would be viewTop <= top)
// 2. viewBottom > screenHeight (view extends beyond screen - confirms it's not just a small view)
// 3. viewTop > 0 (view is not at origin)
//
// This is DIFFERENT from ScrollView where:
// - viewTop = 0 (view is at origin, not animating)
// - Content extends beyond screen (but view position is stable)
//
// When we detect animation state, apply the full top inset since the view
// will eventually settle at Y=0.
var viewIsAnimating = viewTop > top && viewTop > 0 && viewBottom > screenHeight;

if (top > 0 && viewTop < top && viewTop >= 0)
{
// Calculate the actual overlap amount
top = Math.Min(top - viewTop, top);
}
else if (top > 0 && viewIsAnimating)
{
// View is animating - positioned beyond status bar but extends off-screen
// Apply full top inset since view will settle at Y=0
}
else
{
if (viewHeight > 0 || hasTrackedViews)
{
top = 0;
}
}

// Bottom: how much the view extends into the bottom safe area
Expand All @@ -185,19 +194,36 @@ internal static SafeAreaRegions GetSafeAreaRegionForEdge(int edge, ICrossPlatfor
// if the view height is zero because it hasn't done the first pass
// and we don't have any tracked views yet then we will apply the bottom inset
if (viewHeight > 0 || hasTrackedViews)
{
bottom = 0;
}
}

// Left: how much the view extends into the left safe area
// Similar to top, during animation the view may be shifted right (viewLeft > 0)
// but will settle at X=0. Detect animation by checking if view extends beyond screen.
// Note: We also check viewBottom > screenHeight because in landscape orientation,
// Shell navigation transitions can slide views vertically while affecting left safe area.
// Without this check, the left inset would be incorrectly set to 0 during these animations.
var viewIsAnimatingHorizontally = viewLeft > 0 && (viewRight > screenWidth || viewBottom > screenHeight);

if (left > 0 && viewLeft < left)
{
// Calculate the actual overlap amount
left = Math.Min(left - viewLeft, left);
}
else if (left > 0 && viewIsAnimatingHorizontally && viewLeft > left)
{
// View is animating and has been shifted beyond the left safe area edge (viewLeft > left).
// This happens during Shell navigation when the view slides in from the right.
// Keep the full left inset since the view will eventually settle at X=0.
}
else
{
if (viewWidth > 0 || hasTrackedViews)
{
left = 0;
}
}

// Right: how much the view extends into the right safe area
Expand All @@ -210,7 +236,9 @@ internal static SafeAreaRegions GetSafeAreaRegionForEdge(int edge, ICrossPlatfor
else
{
if (viewWidth > 0 || hasTrackedViews)
{
right = 0;
}
}
}

Expand Down Expand Up @@ -287,24 +315,24 @@ internal static double GetSafeAreaForEdge(SafeAreaRegions safeAreaRegion, double

// Handle SoftInput specifically - only apply keyboard insets for bottom edge when keyboard is showing
if (edge == 3)
{
if (SafeAreaEdges.IsOnlySoftInput(safeAreaRegion))
{
// SoftInput only applies padding when keyboard is showing
return isKeyboardShowing ? keyBoardInsets.Bottom : 0;
}

if (isKeyboardShowing)
{
// Return keyboard insets for any region that includes SoftInput
if (SafeAreaEdges.IsSoftInput(safeAreaRegion))
return keyBoardInsets.Bottom;

// if the keyboard is showing then we will just return 0 for the bottom inset
// because that part of the view is covered by the keyboard so we don't want to pad the view
return 0;
}
}
{
if (SafeAreaEdges.IsOnlySoftInput(safeAreaRegion))
{
// SoftInput only applies padding when keyboard is showing
return isKeyboardShowing ? keyBoardInsets.Bottom : 0;
}

if (isKeyboardShowing)
{
// Return keyboard insets for any region that includes SoftInput
if (SafeAreaEdges.IsSoftInput(safeAreaRegion))
return keyBoardInsets.Bottom;

// if the keyboard is showing then we will just return 0 for the bottom inset
// because that part of the view is covered by the keyboard so we don't want to pad the view
return 0;
}
}

// All other regions respect safe area in some form
// This includes:
Expand Down
Loading