DEV Community

Amina Tariq
Amina Tariq

Posted on

Making Android Bottom Sheets Look and Behave More Like iOS in .NET MAUI

Bottom sheets are a popular UI pattern for displaying contextual content without navigating away from the current screen. However, the default implementations on Android and iOS can look and feel quite different, leading to inconsistent user experiences across platforms. In this post, we'll explore how to create a custom bottom sheet implementation that makes Android bottom sheets behave more like their iOS counterparts.

The Problem: Platform Inconsistencies

By default, Android and iOS handle bottom sheets differently:

Android Bottom Sheets:

  • Often appear without affecting the background content
  • May not have consistent backdrop behavior
  • Can have different animation styles
  • Touch handling varies between implementations

iOS Bottom Sheets:

  • Consistently dim the background content
  • Have smooth, predictable animations
  • Provide clear visual separation between the sheet and background
  • Offer intuitive gesture-based interactions
  • Can be dismissed by tapping outside the sheet area

The Solution: Custom Bottom Sheet Implementation

Let's walk through creating a custom bottom sheet that brings iOS-like behavior to Android while maintaining native performance. This implementation is available as a complete project on GitHub: CustomBottomSheet.

The solution provides:

  • Dimmed background for better focus
  • Proper cleanup of active bottom sheets
  • Cancelable by tapping outside
  • Only one bottom sheet active at a time
  • Consistent API for use in views and view models

Step 1: Setting Up the Base Class

We'll extend the existing BottomSheet from The49.Maui.BottomSheet package to add our custom behavior:

using The49.Maui.BottomSheet;
using System;
using Microsoft.Maui.Controls;

namespace CustomBottomSheet.Custom;

public class CustomBottomsheet : BottomSheet
{
    public static CustomBottomsheet CurrentBottomSheet { get; private set; }
    private bool isInitialized = false;
    private bool isDisposed = false;

    public CustomBottomsheet()
    {
        InitializeBottomSheet();
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Managing Bottom Sheet Lifecycle

The key to iOS-like behavior is proper lifecycle management. We need to ensure only one bottom sheet is active at a time and handle the background dimming correctly:

private void InitializeBottomSheet()
{
    if (isInitialized || isDisposed) return;

    this.Showing += OnBottomSheetShowing;
    this.Shown += OnBottomSheetShown;
    this.Dismissed += OnBottomSheetDismissed;

    isInitialized = true;
}

private void OnBottomSheetShowing(object sender, EventArgs e)
{
    if (isDisposed) return;

    // Ensure only one bottom sheet is active
    if (CurrentBottomSheet != null && CurrentBottomSheet != this)
    {
        CurrentBottomSheet.CleanupBottomSheet();
    }

    CurrentBottomSheet = this;

    // Apply iOS-like behavior on Android
    if (DeviceInfo.Platform == DevicePlatform.Android)
    {
        this.IsCancelable = true;  // Enable tap-outside-to-dismiss
        this.HasBackdrop = true;   // Show backdrop for tap detection
        HandleAndroidOpacityForBottomSheet(0.8);
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Enabling iOS-Like Tap-Outside-to-Dismiss

One of the most important iOS behaviors is the ability to dismiss bottom sheets by tapping outside the content area. This provides intuitive interaction that users expect:

private void OnBottomSheetShowing(object sender, EventArgs e)
{
    if (isDisposed) return;

    // Ensure only one bottom sheet is active
    if (CurrentBottomSheet != null && CurrentBottomSheet != this)
    {
        CurrentBottomSheet.CleanupBottomSheet();
    }

    CurrentBottomSheet = this;

    // Apply iOS-like behavior on Android
    if (DeviceInfo.Platform == DevicePlatform.Android)
    {
        this.IsCancelable = true;  // Enable tap-outside-to-dismiss
        this.HasBackdrop = true;   // Show backdrop for tap detection
        HandleAndroidOpacityForBottomSheet(0.8);
    }
}
Enter fullscreen mode Exit fullscreen mode

Key Properties for iOS-Like Behavior:

  • IsCancelable = true: Allows the bottom sheet to be dismissed by tapping outside
  • HasBackdrop = true: Creates a backdrop area that can detect tap gestures
  • Background dimming: Visual feedback that the sheet is modal and can be dismissed

Step 4: The Magic - Background Dimming

The most noticeable difference between platforms is how the background is handled. iOS consistently dims the background content, creating a clear focus on the bottom sheet. Here's how we achieve this on Android:

public static void HandleAndroidOpacityForBottomSheet(double opacity)
{
#if ANDROID
    var currentPageView = Shell.Current?.CurrentPage;
    if (currentPageView != null)
    {
        currentPageView.Opacity = opacity;
    }
    else
    {
        // Fallback for non-Shell applications
        var currentPageOutsideShell = Application.Current.MainPage?.Navigation?.NavigationStack.LastOrDefault();
        if (currentPageOutsideShell != null)
        {
            currentPageOutsideShell.Opacity = opacity;
        }
    }
#endif
}
Enter fullscreen mode Exit fullscreen mode

This method:

  • Dims the background by reducing the current page's opacity to 0.8 (you can adjust this value)
  • Works with Shell and non-Shell applications by checking both navigation patterns
  • Only affects Android using conditional compilation
  • Creates visual hierarchy similar to iOS modal presentations

Step 5: Proper Cleanup and State Management

To prevent memory leaks and ensure smooth transitions, we need comprehensive cleanup:

private void OnBottomSheetDismissed(object sender, DismissOrigin dismissOrigin)
{
    CleanupBottomSheet();
}

private void CleanupBottomSheet()
{
    if (isDisposed) return;

    // Restore background opacity
    if (DeviceInfo.Platform == DevicePlatform.Android)
    {
        HandleAndroidOpacityForBottomSheet(1.0);
    }

    // Clear current reference
    if (CurrentBottomSheet == this)
    {
        CurrentBottomSheet = null;
    }

    ResetBottomSheetState();
}

private void ResetBottomSheetState()
{
    if (!isDisposed && DeviceInfo.Platform == DevicePlatform.Android)
    {
        this.IsCancelable = true;   // Maintain tap-outside-to-dismiss
        this.HasBackdrop = true;    // Keep backdrop for consistent behavior
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Memory Management

Implement proper disposal to prevent memory leaks:

public void ManualCleanup()
{
    if (isDisposed) return;
    CleanupBottomSheet();
    DisposeBottomSheet();
}

private void DisposeBottomSheet()
{
    if (isDisposed) return;

    if (isInitialized)
    {
        this.Showing -= OnBottomSheetShowing;
        this.Shown -= OnBottomSheetShown;
        this.Dismissed -= OnBottomSheetDismissed;
    }

    CleanupBottomSheet();
    isDisposed = true;
    isInitialized = false;
}

~CustomBottomsheet()
{
    DisposeBottomSheet();
}
Enter fullscreen mode Exit fullscreen mode

Getting Started

You can find the complete implementation in the CustomBottomSheet GitHub repository. The repository includes:

  • Complete source code with detailed comments
  • Example usage scenarios
  • Setup instructions for .NET MAUI projects
  • Integration with The49.Maui.BottomSheet package

Prerequisites

Before implementing this solution, ensure you have:

  • The49.Maui.BottomSheet NuGet package installed
  • .NET MAUI project setup
  • Understanding of MAUI lifecycle management

Key Benefits

This implementation provides several advantages:

  1. Consistent Visual Experience: Android bottom sheets now dim the background like iOS
  2. Intuitive Interaction: Tap outside to dismiss, just like iOS
  3. Proper State Management: Only one bottom sheet active at a time
  4. Memory Efficiency: Proper cleanup prevents memory leaks
  5. Cross-Platform Compatibility: Works with both Shell and traditional navigation
  6. Smooth Animations: Background opacity changes create smooth transitions

Best Practices

When implementing this solution:

  • Test on both platforms to ensure the behavior feels natural
  • Adjust opacity values based on your app's design requirements
  • Consider accessibility when dimming background content
  • Handle edge cases like rapid successive bottom sheet presentations
  • Monitor memory usage in long-running applications

Conclusion

By implementing this custom bottom sheet solution, you can achieve iOS-like consistency across both platforms while maintaining the native performance and feel that users expect. The background dimming effect, combined with proper lifecycle management and tap-outside-to-dismiss functionality, creates a polished user experience that feels cohesive regardless of the platform.

The key insight is that small visual details like background dimming and intuitive interactions can significantly impact the perceived quality and consistency of your cross-platform application. With this implementation, your Android users will enjoy the same polished bottom sheet experience as your iOS users.

Source Code

The complete implementation is available on GitHub: amina-taariq/CustomBottomSheet

This repository provides a reusable solution that you can easily integrate into your .NET MAUI projects to achieve consistent cross-platform bottom sheet behavior.


Want to learn more about cross-platform UI consistency in .NET MAUI? Follow along for more tips and techniques for creating seamless user experiences across Android and iOS.

Top comments (0)