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();
}
}
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);
}
}
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);
}
}
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
}
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
}
}
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();
}
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:
- Consistent Visual Experience: Android bottom sheets now dim the background like iOS
- Intuitive Interaction: Tap outside to dismiss, just like iOS
- Proper State Management: Only one bottom sheet active at a time
- Memory Efficiency: Proper cleanup prevents memory leaks
- Cross-Platform Compatibility: Works with both Shell and traditional navigation
- 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)