Skip to content
Closed
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
63 changes: 63 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,29 @@ For .NET installation on Linux, follow the official Microsoft documentation:

## Development Workflow

### Pre-Commit Validation Requirements

**MANDATORY: All platform-specific changes must be validated before committing.**

For Android platform code changes:
1. **Compile Android TFM**: Always test `net10.0-android36.0` and similar Android target frameworks
2. **Verify Android references**: Ensure `using Android.*` statements compile correctly
3. **Test platform APIs**: Confirm Android-specific extension methods work as expected

Example validation commands:
```bash
# Core Android TFM validation
dotnet build src/Core/src/Core.csproj --framework net10.0-android36.0

# Controls Android TFM validation
dotnet build src/Controls/src/Core/Controls.Core.csproj --framework net10.0-android36.0

# Full BuildTasks validation
dotnet build ./Microsoft.Maui.BuildTasks.slnf
```

**Do not skip Android TFM compilation testing** - Android workloads are available and functional in this environment.

### Building

#### Using Cake (Recommended)
Expand All @@ -99,6 +122,29 @@ dotnet cake
dotnet cake --target=dotnet-pack
```

#### Android Target Framework Validation

**CRITICAL: Always validate Android TFM builds before committing changes.**

The Android workloads are properly installed and available in this environment. When making code changes that affect Android platform code, always verify that Android target frameworks compile successfully:

```bash
# Verify Android TFM compilation for specific projects
dotnet build src/Core/src/Core.csproj --framework net10.0-android36.0
dotnet build src/Controls/src/Core/Controls.Core.csproj --framework net10.0-android36.0

# Build BuildTasks to ensure foundational compilation works
dotnet build ./Microsoft.Maui.BuildTasks.slnf
```

**Required Android TFM Build Verification:**
- Test Android-specific target frameworks (e.g., `net10.0-android36.0`) when modifying Android platform code
- Verify that `using Android.*` statements compile correctly
- Ensure Android-specific extension methods and APIs are properly referenced
- Confirm that Android workload dependencies are resolved

**Do not commit Android platform changes without verifying Android TFM compilation success.**

### Testing and Debugging

#### Testing Guidelines
Expand Down Expand Up @@ -184,6 +230,23 @@ For compatibility with specific branches:
- Install missing Android SDKs via [Android SDK Manager](https://learn.microsoft.com/xamarin/android/get-started/installation/android-sdk)
- Android SDK Manager available via: `android` command (after dotnet tool restore)

#### Android TFM Build Requirements
**Android workloads are installed and functional.** When working on Android platform code:

- **Always test Android TFM compilation** before committing changes
- **Verify Android-specific target frameworks** like `net10.0-android36.0` compile successfully
- **Test Android platform references** including `using Android.*` namespaces
- **Validate Android extension methods** and platform-specific APIs work correctly

Build verification commands:
```bash
# Test Android TFM for Core components
dotnet build src/Core/src/Core.csproj --framework net10.0-android36.0

# Test Android TFM for Controls
dotnet build src/Controls/src/Core/Controls.Core.csproj --framework net10.0-android36.0
```

### iOS (requires macOS)
- Requires current stable Xcode installation from [App Store](https://apps.apple.com/us/app/xcode/id497799835?mt=12) or [Apple Developer portal](https://developer.apple.com/download/more/?name=Xcode)
- Pair to Mac required when developing on Windows
Expand Down
118 changes: 118 additions & 0 deletions docs/SafeAreaEdges-Android.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# SafeAreaEdges Android Implementation

This document describes the Android implementation of SafeAreaEdges functionality for .NET MAUI, which provides per-edge safe area control matching the functionality introduced for iOS in PR #30337.

## Overview

The Android SafeAreaEdges implementation allows developers to control how content respects system UI elements like status bars, navigation bars, display cutouts, and the keyboard on a per-edge basis.

## Implementation Details

### Core Components

1. **SafeAreaPadding.cs** - Android equivalent of iOS SafeAreaPadding, handles inset calculations
2. **ContentViewGroup.cs** - Updated to apply SafeAreaEdges logic during layout
3. **LayoutViewGroup.cs** - Updated to apply SafeAreaEdges logic during layout
4. **WindowInsetsExtensions** - Helper methods to convert Android WindowInsets to SafeAreaPadding

### SafeAreaRegions Behavior on Android

- **None**: Content goes edge-to-edge, ignoring all system UI elements
- **All**: Content respects all safe area insets (status bar, navigation bar, display cutouts, keyboard)
- **Container**: Content flows under keyboard but stays out of status/navigation bars and display cutouts
- **SoftInput**: Always pad to avoid keyboard overlap
- **Default**: Platform default behavior (currently same as Container)

### Android-Specific Considerations

#### WindowInsets Integration
- Uses AndroidX.Core.View.WindowInsetsCompat for consistent API across Android versions
- Supports WindowInsetsCompat.Type.SystemBars() for status/navigation bars
- Supports WindowInsetsCompat.Type.DisplayCutout() for display cutouts (API 28+)
- Supports WindowInsetsCompat.Type.Ime() for keyboard insets (API 30+)

#### Pixel Density Handling
- All insets are converted from Android pixels to device-independent units using Context.GetDisplayDensity()
- This ensures consistent behavior across different screen densities

#### WindowInsets Listener
- Each view group sets up a WindowInsetsListener to receive inset changes
- When insets change, the view invalidates and triggers a layout update
- This ensures dynamic updates when keyboard appears/disappears or device orientation changes

### Edge-Specific Logic

The implementation processes each edge (Left, Top, Right, Bottom) independently:

1. **SafeAreaRegions.None**: Returns 0 padding (edge-to-edge)
2. **SafeAreaRegions.SoftInput**: Returns max of original safe area and keyboard inset
3. **SafeAreaRegions.Container**: Returns original safe area (ignores keyboard)
4. **SafeAreaRegions.All/Default**: Returns original safe area

### Layout Integration

Both ContentViewGroup and LayoutViewGroup apply SafeAreaEdges in their OnLayout methods:

1. Check if the cross-platform layout implements ISafeAreaView2
2. Calculate adjusted safe area insets based on SafeAreaEdges configuration
3. Apply insets to the layout bounds before arranging child content

## Usage Examples

### Edge-to-Edge Content
```xml
<Grid SafeAreaEdges="None">
<!-- Content goes under status bar and navigation bar -->
</Grid>
```

### Respect All Safe Areas
```xml
<Grid SafeAreaEdges="All">
<!-- Content respects status bar, navigation bar, cutouts, and keyboard -->
</Grid>
```

### Per-Edge Control
```xml
<Grid SafeAreaEdges="{x:Static SafeAreaEdges.None}">
<!-- Programmatic per-edge control -->
</Grid>
```

```csharp
// Set bottom edge to handle keyboard, keep other edges edge-to-edge
myGrid.SafeAreaEdges = new SafeAreaEdges(
SafeAreaRegions.None, // Left
SafeAreaRegions.None, // Top
SafeAreaRegions.None, // Right
SafeAreaRegions.SoftInput // Bottom
);
```

## Testing

### Unit Tests
- Issue28986Android.cs provides Android-specific UI tests
- Tests verify edge-to-edge vs safe area positioning
- Tests verify keyboard interaction with SoftInput regions

### Manual Testing
- SafeAreaAndroidTest.xaml provides a comprehensive test page
- Includes buttons to test all SafeAreaRegions combinations
- Provides visual feedback for safe area behavior
- Entry field for testing keyboard/SoftInput behavior

## Platform Differences from iOS

1. **System UI Elements**: Android has status bar and navigation bar instead of iOS notch/home indicator
2. **Keyboard Behavior**: Android WindowInsets provide more granular keyboard information
3. **Display Cutouts**: Android supports various cutout shapes beyond iOS notch
4. **Edge Cases**: Android handles orientation changes and foldable devices differently

## Future Enhancements

1. **WindowInsetsAnimation**: Could be integrated for smooth keyboard animations
2. **Navigation Bar Behavior**: Could distinguish between gesture navigation and button navigation
3. **Foldable Support**: Could handle foldable device specific insets
4. **Performance**: Could cache inset calculations for better performance
Copy link
Member

Choose a reason for hiding this comment

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

@copilot can you apply this same logic to the NavigationRootManager?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Applied the same safe area logic to NavigationRootManager. The implementation:

  • Finds the AppBarLayout from the inflated navigationlayout resource
  • Sets up WindowInsets listeners to apply safe area insets as top padding
  • Includes edge-to-edge configuration for proper display cutout detection
  • Uses the same pattern as ShellContentFragment and ViewHandler for consistency

The NavigationRootManager now applies safe area insets directly to its AppBarLayout component, ensuring Shell navigation headers properly avoid camera cutouts and notches.

Commit: 5970479

Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
#nullable disable
using System;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Views.Animations;
using AndroidX.AppCompat.Widget;
using AndroidX.CoordinatorLayout.Widget;
using AndroidX.Core.View;
using AndroidX.Fragment.App;
using Google.Android.Material.AppBar;
using Microsoft.Maui.Platform;
using AndroidAnimation = Android.Views.Animations.Animation;
using AnimationSet = Android.Views.Animations.AnimationSet;
using AToolbar = AndroidX.AppCompat.Widget.Toolbar;
Expand Down Expand Up @@ -142,6 +145,9 @@ public override AView OnCreateView(LayoutInflater inflater, ViewGroup container,

var appBar = _root.FindViewById<AppBarLayout>(Resource.Id.shellcontent_appbar);
appBar.AddView(_toolbar);

// Apply safe area insets to AppBarLayout to prevent content from going behind cutouts/notch
SetupAppBarSafeAreaHandling(appBar);
_viewhandler = _page.ToHandler(shellContentMauiContext);

_shellPageContainer = new ShellPageContainer(Context, _viewhandler);
Expand All @@ -165,6 +171,83 @@ public override AView OnCreateView(LayoutInflater inflater, ViewGroup container,
return _root;
}

void SetupAppBarSafeAreaHandling(AppBarLayout appBar)
{
if (appBar == null || Context == null)
return;

// Ensure edge-to-edge configuration for proper cutout detection
EnsureEdgeToEdgeConfiguration();

// Set up WindowInsets listener for the AppBarLayout
ViewCompat.SetOnApplyWindowInsetsListener(appBar, (view, insets) =>
{
ApplySafeAreaToAppBar(appBar, insets);
// Don't consume insets here - let them propagate to child views
return insets;
});

// Initial application if insets are already available
var rootView = appBar.RootView;
if (rootView != null)
{
var windowInsets = ViewCompat.GetRootWindowInsets(rootView);
if (windowInsets != null)
{
ApplySafeAreaToAppBar(appBar, windowInsets);
}
}
}

void EnsureEdgeToEdgeConfiguration()
{
try
{
var activity = Context.GetActivity();
if (activity?.Window != null && OperatingSystem.IsAndroidVersionAtLeast(30))
{
// For API 30+, ensure edge-to-edge configuration for proper cutout detection
AndroidX.Core.View.WindowCompat.SetDecorFitsSystemWindows(activity.Window, false);
}
}
catch (Exception ex)
{
// Log but don't crash if we can't configure the window
System.Diagnostics.Debug.WriteLine($"SafeArea: Failed to configure edge-to-edge mode: {ex.Message}");
}
}

void ApplySafeAreaToAppBar(AppBarLayout appBar, WindowInsetsCompat insets)
{
if (appBar == null || Context == null)
return;

try
{
// Get safe area insets including display cutouts
var safeArea = insets.ToSafeAreaInsets(Context);

// Apply top safe area inset as padding to push content down from notch/cutout
// Convert to pixels for Android view padding
var topPaddingPx = (int)(safeArea.Top * Context.GetDisplayDensity());

// Apply padding to the AppBarLayout to avoid cutout areas
// Preserve existing left/right/bottom padding if any
appBar.SetPadding(
appBar.PaddingLeft,
topPaddingPx,
appBar.PaddingRight,
appBar.PaddingBottom
);

System.Diagnostics.Debug.WriteLine($"SafeArea: Applied AppBar top padding: {topPaddingPx}px (from {safeArea.Top} dip)");
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"SafeArea: Failed to apply safe area to AppBar: {ex.Message}");
}
}

void Destroy()
{
// If the user taps very quickly on back button multiple times to pop a page,
Expand Down
Loading