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
91 changes: 47 additions & 44 deletions src/Core/src/Platform/Android/SafeAreaExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,21 +120,9 @@ internal static SafeAreaRegions GetSafeAreaRegionForEdge(int edge, ICrossPlatfor
var viewRight = viewLeft + viewWidth;
var viewBottom = viewTop + viewHeight;

// Adjust for view's position relative to parent (including margins) to calculate
// safe area insets relative to the parent's position, not the view's visual position.
// This ensures margins and safe area insets are additive rather than overlapping.
// For example: 20px margin + 30px safe area = 50px total offset
// We only take the margins into account if the Width and Height are set
// If the Width and Height aren't set it means the layout pass hasn't happen yet
if (view.Width > 0 && view.Height > 0)
{
viewTop = Math.Max(0, viewTop - (int)context.ToPixels(margins.Top));
viewLeft = Math.Max(0, viewLeft - (int)context.ToPixels(margins.Left));
viewRight += (int)context.ToPixels(margins.Right);
viewBottom += (int)context.ToPixels(margins.Bottom);
}

// Get actual screen dimensions (including system UI)
// This must be done BEFORE margin adjustment so we can detect
// off-screen animation state from raw position values.
var windowManager = context.GetSystemService(Context.WindowService) as IWindowManager;
if (windowManager?.DefaultDisplay is not null)
{
Expand All @@ -143,33 +131,48 @@ internal static SafeAreaRegions GetSafeAreaRegionForEdge(int edge, ICrossPlatfor
var screenWidth = realMetrics.WidthPixels;
var screenHeight = realMetrics.HeightPixels;

// 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
// Detect if view is off-screen BEFORE margin adjustment clamps negative positions
// to zero via Math.Max, destroying the animation signal.
// Horizontal: during Shell tab animation, viewLeft=-1 gets clamped to 0,
// making it impossible to detect animation for the RIGHT edge afterward.
var viewIsAnimatingHorizontally = viewLeft < 0 || viewRight > screenWidth;

// Vertical: 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;
// This is DIFFERENT from ScrollView where viewTop = 0 (at origin, not animating).
// When we detect animation state, apply the full top inset since view will settle at Y=0.
var viewIsAnimatingVertically = viewTop > top && viewTop > 0 && viewBottom > screenHeight;

// Adjust for view's position relative to parent (including margins) to calculate
// safe area insets relative to the parent's position, not the view's visual position.
// This ensures margins and safe area insets are additive rather than overlapping.
// For example: 20px margin + 30px safe area = 50px total offset
// We only take the margins into account if the Width and Height are set
// If the Width and Height aren't set it means the layout pass hasn't happened yet
if (view.Width > 0 && view.Height > 0)
{
viewTop = Math.Max(0, viewTop - (int)context.ToPixels(margins.Top));
viewLeft = Math.Max(0, viewLeft - (int)context.ToPixels(margins.Left));
viewRight += (int)context.ToPixels(margins.Right);
viewBottom += (int)context.ToPixels(margins.Bottom);
}

// 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

if (top > 0 && viewTop < top && viewTop >= 0)
{
// Calculate the actual overlap amount
top = Math.Min(top - viewTop, top);
}
else if (top > 0 && viewIsAnimating)
else if (top > 0 && viewIsAnimatingVertically)
{
// View is animating - positioned beyond status bar but extends off-screen
// Apply full top inset since view will settle at Y=0
Expand Down Expand Up @@ -200,23 +203,17 @@ internal static SafeAreaRegions GetSafeAreaRegionForEdge(int edge, ICrossPlatfor
}

// 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)
// During Shell navigation animations, the view slides in from off-screen.
// We must check animation FIRST because near the end of animation
// (e.g., viewLeft=1), the overlap check would incorrectly reduce the inset.
if (left > 0 && viewIsAnimatingHorizontally && viewLeft > 0)
{
// Calculate the actual overlap amount
left = Math.Min(left - viewLeft, left);
// View is animating - keep full inset since view will settle at X=0
}
else if (left > 0 && viewIsAnimatingHorizontally && viewLeft > left)
else if (left > 0 && 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.
// Calculate the actual overlap amount
left = Math.Min(left - viewLeft, left);
}
else
{
Expand All @@ -227,7 +224,13 @@ internal static SafeAreaRegions GetSafeAreaRegionForEdge(int edge, ICrossPlatfor
}

// Right: how much the view extends into the right safe area
if (right > 0 && viewRight > (screenWidth - right))
// During animation, viewRight may be near screenWidth (e.g., 2991 vs 2992)
// causing incorrect partial overlap. Check animation before overlap.
if (right > 0 && viewIsAnimatingHorizontally)
{
// View is animating - keep full inset
}
else if (right > 0 && viewRight > (screenWidth - right))
{
// Calculate the actual overlap amount
var rightEdge = screenWidth - right;
Expand Down
Loading