7

Xamarin.Forms lightweight bottom sheet modal

 2 years ago
source link: https://xamarinhowto.com/xamarin-forms-lightweight-bottom-sheet-modal/?utm_campaign=xamarin-forms-lightweight-bottom-sheet-modal
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Xamarin.Forms lightweight bottom sheet modal

Bottom sheets are becoming increasingly more popular in modern apps including Facebook, Instagram and Microsoft Teams.  A bottom sheet comes in three main forms including:

  • Standard – the sheet remains visible while users can interact with the rest of the content on the screen.
  • Modal – the sheet must be dismissed to interact with the rest of the content on the screen.
  • Expanding – the sheet is able to be resized with ‘snap’ points often a low, middle and fullscreen view.  The underlying content is also able to be interacted with by the user.

In this Xamarin how-to post, I will show you how you can create a control to display a modal bottom sheet.

The criteria:

  • Pure Xamarin.Forms (no custom renderers)
  • Change sheet content through binding
  • Open sheet via button
  • Close sheet by either swiping down or tapping outside of the sheet’s content
  • Smooth + efficient

Start off by creating a blank ContentView class. We create the PanGesture on the ContentView rather than the frame itself. This is to stop jerkiness on Android due to the way that the OS handles translations.

using System;
using Xamarin.Forms;
namespace BottomSheet.Controls
    public class PanContainer : ContentView
        public PanContainer()

The Control’s .XAML

<?xml version="1.0" encoding="UTF-8"?>
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:controls="clr-namespace:BottomSheet.Controls"
             x:Class="BottomSheet.Controls.BottomSheetControl"
             x:Name="BottomSheetRef"
             InputTransparent="True">
    <ContentView.Content>
        <Grid>
            <BoxView Color="Black"
                 Opacity="0"
                 InputTransparent="True"
                 x:Name="Backdrop">
                <BoxView.GestureRecognizers>
                    <TapGestureRecognizer Tapped="TapGestureRecognizer_Tapped"/>
                </BoxView.GestureRecognizers>
            </BoxView>
            <controls:PanContainer x:Name="PanContainerRef"
                                   HeightRequest="{Binding SheetHeight, Source={x:Reference BottomSheetRef}}"
                                   VerticalOptions="End">
                <controls:PanContainer.GestureRecognizers>
                    <PanGestureRecognizer PanUpdated="PanGestureRecognizer_PanUpdated"/>
                </controls:PanContainer.GestureRecognizers>
                <Frame x:Name="Sheet"
                   Content="{Binding SheetContent ,Source={x:Reference BottomSheetRef}}"
                   HeightRequest="{Binding SheetHeight, Source={x:Reference BottomSheetRef}}"
                   VerticalOptions="End"
                   BackgroundColor="White"
                   CornerRadius="5"
                   HasShadow="False">
                </Frame>
            </controls:PanContainer>
        </Grid>
    </ContentView.Content>
</ContentView>

The Control’s Code Behind

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using DarkIce.Toolkit.Core.Utilities;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace BottomSheet.Controls
    public partial class BottomSheetControl : ContentView
        #region Constructors & initialisation
        public BottomSheetControl()
            InitializeComponent();
        protected override void OnBindingContextChanged()
                base.OnBindingContextChanged();
                PanContainerRef.Content.TranslationY = SheetHeight + 60;
            catch (Exception ex)
                ex.Log();
        #endregion
        #region Properties
        public static BindableProperty SheetHeightProperty = BindableProperty.Create(
            nameof(SheetHeight),
            typeof(double),
            typeof(BottomSheetControl),
            defaultValue: default(double),
            defaultBindingMode: BindingMode.TwoWay);
        public double SheetHeight
            get { return (double)GetValue(SheetHeightProperty); }
            set { SetValue(SheetHeightProperty, value); OnPropertyChanged(); }
        public static BindableProperty SheetContentProperty = BindableProperty.Create(
            nameof(SheetContent),
            typeof(View),
            typeof(BottomSheetControl),
            defaultValue: default(View),
            defaultBindingMode: BindingMode.TwoWay);
        public View SheetContent
            get { return (View)GetValue(SheetContentProperty); }
            set { SetValue(SheetContentProperty, value); OnPropertyChanged(); }
        #endregion
        uint duration = 250;
        double openPosition = (DeviceInfo.Platform == DevicePlatform.Android) ? 20 : 60;
        double currentPosition = 0;
        public async void PanGestureRecognizer_PanUpdated(object sender, PanUpdatedEventArgs e)
                if (e.StatusType == GestureStatus.Running)
                    currentPosition = e.TotalY;
                    if (e.TotalY > 0)
                        PanContainerRef.Content.TranslationY = openPosition + e.TotalY;
                else if (e.StatusType == GestureStatus.Completed)
                    var threshold = SheetHeight * 0.55;
                    if (currentPosition < threshold)
                        await OpenSheet();
                        await CloseSheet();
            catch (Exception ex)
                ex.Log();
        public async Task OpenSheet()
                await Task.WhenAll
                    Backdrop.FadeTo(0.4, length: duration),
                    Sheet.TranslateTo(0, openPosition, length: duration, easing: Easing.SinIn)
                BottomSheetRef.InputTransparent = Backdrop.InputTransparent = false;
            catch (Exception ex)
                ex.Log();
        public async Task CloseSheet()
                await Task.WhenAll
                    Backdrop.FadeTo(0, length: duration),
                    PanContainerRef.Content.TranslateTo(x: 0, y: SheetHeight + 60, length: duration, easing: Easing.SinIn)
                BottomSheetRef.InputTransparent = Backdrop.InputTransparent = true;
            catch (Exception ex)
                ex.Log();
        private async void TapGestureRecognizer_Tapped(System.Object sender, System.EventArgs e)
                await CloseSheet();
            catch (Exception ex)
                ex.Log();

Consuming the Control

<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:controls="clr-namespace:BottomSheet.Controls"
             xmlns:viewmodel="clr-namespace:BottomSheet"
             x:Class="BottomSheet.MainPage">
    <ContentPage.BindingContext>
        <viewmodel:MainViewModel/>
    </ContentPage.BindingContext>
    <ContentPage.Content>
        <Grid>
            <Grid HorizontalOptions="Center">
                <Button Text="Tap Me!" Clicked="Button_Clicked" VerticalOptions="Center"/>
            </Grid>
            <controls:BottomSheetControl x:Name="Sheet"
                                         SheetHeight="300">
                <controls:BottomSheetControl.SheetContent>
                    <StackLayout>
                        <OnPlatform x:TypeArguments="View">
                            <On Platform="iOS">
                                <Grid>
                                    <BoxView CornerRadius="2.5"
                                     BackgroundColor="Gray"
                                     HorizontalOptions="Center"
                                     VerticalOptions="Start"
                                     HeightRequest="5"
                                     WidthRequest="40"/>
                                </Grid>
                            </On>
                            <On Platform="Android">
                                <Grid>
                                    <Image Source="chevrondown"
                                    HorizontalOptions="Center"
                                    VerticalOptions="Start"
                                    WidthRequest="40"/>
                                </Grid>
                            </On>
                        </OnPlatform>
                        <Label HorizontalTextAlignment="Center"
                           Margin="0,20"
                           Text="Welcome to Xamarin how to"
                           FontSize="18"/>
                        <Image Source="xhtlogo" HorizontalOptions="Center"/>
                    </StackLayout>
                </controls:BottomSheetControl.SheetContent>
            </controls:BottomSheetControl>
        </Grid>
    </ContentPage.Content>
</ContentPage>
using System;
using System.Collections.Generic;
using DarkIce.Toolkit.Core.Utilities;
using Xamarin.Forms;
namespace BottomSheet
    public partial class MainPage : ContentPage
        public MainPage()
            InitializeComponent();
        private async void Button_Clicked(System.Object sender, System.EventArgs e)
                await Sheet.OpenSheet();
            catch (Exception ex)
                ex.Log();

I know this looks like a lot of code but it is quite simple.  The bottom sheet control uses a pan gesture recognizer and a couple simple animations (FadeTo and TranslateTo) to give the desired look and feel.  This bottom sheet is very lightweight and takes a little performance hit.  The content inside the frame could very easily be changed to collection view to be used as a toolbar / menu with the bindings set up accordingly. I hope this control can be used in your next project!

Full source code is available here


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK