I'm Andrew Hoefling, and I work for FileOnQ as a Lead Software Engineer building mobile technologies for Government, Financial and First Responders using Xamarin. 

 

Xamarin.Forms Fancy Flyout Menu


June 2020 is Xamarin Month Code Snippets Edition, put together by Luis Matos which features a new Xamarin Blog post every day. Be sure to look at the overview blog which contains all the posts for code snippets. 

Xamarin.Forms provides a great set of controls for building cross-platform apps including Flyout Menus. Often the default controls don't meet your design needs and you need to build something custom. Xamarin.Forms provides rich APIs for styling and animating just about anything you can imagine. Let's build a fancy animating Flyout Menu!

Our Fancy Menu will slide to the right and shrink, revealing a hidden menu that is hiding behind it.

Structure

The Fancy Menu utilizes a combination of ContentPage's and ContentView's to create the Flyout control instead of using the built in controls. Our control will be broken into 3 parts

  • Container - Entry point of the Fancy Menu
  • Menu - The hidden menu
  • MainContent - The page that is initially rendered

Once you have the views defined there is some animation code found in the Container class to perform the open and close operations.

Container

The container encapsulates the entire Fancy Menu and includes the Menu, MainContent and animation code. Start by defining the view code structure.

<?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:d="http://xamarin.com/schemas/2014/forms/design"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:views="clr-namespace:FancyMenu"
             mc:Ignorable="d"
             x:Class="FancyMenu.ContainerPage">

    <Grid>
        
        <Grid x:Name="Flyout" BackgroundColor="Transparent" Margin="0" Padding="0">
            <Grid.GestureRecognizers>
                <SwipeGestureRecognizer Direction="Left"
                                        Swiped="FlyoutClose" />
            </Grid.GestureRecognizers>
            <views:MenuPage />
        </Grid>
        
        <Frame x:Name="MainContent" BackgroundColor="Transparent" Margin="0" Padding="0">
            <Frame.GestureRecognizers>
                <SwipeGestureRecognizer Direction="Right"
                                        Swiped="FlyoutOpen" />
            </Frame.GestureRecognizers>
            <views:MainPage ToggleMenu="OnToggleMenu" />
        </Frame>
        
    </Grid>

</ContentPage>

By default the Container view code configures swipe gestures to toggle the flyout.  

The Container.xaml.cs controls all the animation logic.

public partial class ContainerPage : ContentPage
{
    const float FlyoutCornerRadius = 25f;

    bool _isFlyoutOpen = false;
    double _scale;
    uint _flyoutSpeed = 200;
    double _pagePositionX;
    double _flyoutTranslationX;
        
    public ContainerPage()
    {
        InitializeComponent();

        // Set page scale for both content and flyout
        _scale = MainContent.Scale;
            
        _pagePositionX = MainContent.TranslationX;

        // Add event listeners for SizeChanged - Allows us to capture page values after it is rendered
        MainContent.SizeChanged += OnMainContentSizeChanged;
    }

    void OnMainContentSizeChanged(object sender, EventArgs e)
    {
        MainContent.SizeChanged -= OnMainContentSizeChanged;
        _flyoutTranslationX = MainContent.Width * .75;

        if (Flyout.Children.Count == 1 && Flyout.Children[0] is Layout menuPage)
        {
            var flyoutPadding = Flyout.Width - (Flyout.Width * .8);
            (Flyout.Children[0] as Layout).Padding = new Thickness(0, 0, flyoutPadding, 0);
        }
    }

    void OnToggleMenu(object sender, EventArgs e)
    {
        ToggleFlyout();
    }

    void FlyoutClose(object sender, SwipedEventArgs e)
    {
        if (_isFlyoutOpen)
            ToggleFlyout();
    }

    void FlyoutOpen(object sender, SwipedEventArgs e)
    {
        if (!_isFlyoutOpen)
            ToggleFlyout();
    }

    void ToggleFlyout()
    {
        if (_isFlyoutOpen)
        {
            MainContent.ScaleTo(_scale, _flyoutSpeed);
            MainContent.TranslateTo(_pagePositionX, Flyout.TranslationY, _flyoutSpeed);
            MainContent.CornerRadius = 0;
        }
        else
        {
            MainContent.ScaleTo(_scale * .9, _flyoutSpeed);
            MainContent.TranslateTo(Flyout.TranslationX + _flyoutTranslationX, Flyout.TranslationY, _flyoutSpeed);
            MainContent.CornerRadius = FlyoutCornerRadius;
        }

        _isFlyoutOpen = !_isFlyoutOpen;
    }
}

 There are 3 main events in the Container

  • OnToggleMenu - Opens or Closes the flyout
  • FlyoutOpen - Opens the flyout
  • FlyoutClose - Closes the flyout

Menu

The Menu page is a standard ContentView that displays the menu that hides behind the Main Content. Since the Container does all the heavy lifting all you need to do in this file is implement your menu. It will be styled correctly to take up the correct width and not overflow onto the Main Content.

In our example we went ahead an implemented a Menu that has a header, options and a footer.

<?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:d="http://xamarin.com/schemas/2014/forms/design"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             mc:Ignorable="d"
             BackgroundColor="SkyBlue"
             x:Class="FancyMenu.MenuPage">

    <ContentView.Resources>
        <ResourceDictionary>
            <Style TargetType="Label">
                <Setter Property="HorizontalTextAlignment" Value="Center" />
            </Style>
        </ResourceDictionary>
    </ContentView.Resources>
    
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        
        <StackLayout Grid.Row="0" Padding="{OnPlatform Android='50, 25', iOS='50, 50, 50, 25'}">
            <Label Text="Fancy Menu"
                   TextColor="White"
                   FontSize="30"
                   HorizontalOptions="CenterAndExpand"/>
            <BoxView BackgroundColor="White" HeightRequest="1" HorizontalOptions="FillAndExpand" />
        </StackLayout>

        <StackLayout Grid.Row="1" Padding="10, 0, 10, 25">
            <Label Text="This is the fancy menu where youc an customize your control just like any page. I prefer to place header or profile content above the buttons, which is what this space is designed for." />
        </StackLayout>

        <Grid Grid.Row="2" Padding="25, 0, 25, 25" VerticalOptions="FillAndExpand">
            <Grid.RowDefinitions>
                <RowDefinition Height="6*" />
                <RowDefinition Height="*" />
            </Grid.RowDefinitions>
            <StackLayout Grid.Row="0" Spacing="20"
                         VerticalOptions="CenterAndExpand">
                <StackLayout.Resources>
                    <ResourceDictionary>
                        <Style TargetType="Frame">
                            <Setter Property="BackgroundColor" Value="Lavender" />
                            <Setter Property="CornerRadius" Value="10" />
                            <Setter Property="Padding" Value="15, 10" />
                        </Style>
                    </ResourceDictionary>
                </StackLayout.Resources>
                <Frame>
                    <Label Text="Home" />
                </Frame>
                <Frame>
                    <Label Text="Settings" />
                </Frame>
                <Frame>
                    <Label Text="About" />
                </Frame>
                <Frame>
                    <Label Text="GitHub" />
                </Frame>
            </StackLayout>

            <StackLayout Grid.Row="1" VerticalOptions="EndAndExpand">
                <Label Text="Made with ❤ by Andrew Hoefling" />
                <Label Text="Open Source Software" />
            </StackLayout>
        </Grid>
        
    </Grid>
    
</ContentView>

Main Content

The Main Content doesn't use a standard NavigationPage but a custom built one that includes a title and a hamburger button for opening and closing the menu. This is where you may run into problems with the Fancy Menu.

public partial class MainPage : ContentView
{
    public MainPage()
    {
        InitializeComponent();
    }

    public event EventHandler ToggleMenu;

    void OnMenuTapped(object sender, System.EventArgs e)
    {
        ToggleMenu?.Invoke(sender, e);
    }
}

The Main Content view has an event that allows us to create a connection between the Main Content and the Container. This tells the container to invoke the animation logic of opening the flyout menu

  • ToggleMenu

Conclusion

Putting it all together you will have a fancy menu instead of the standard flyout behavior. It should look like animation below.

This code is available for download on GitHub, be sure to check it out as an easy way to get up and running with this.

 

-Happy Coding


Share

Tags

XamarinXamarin.FormsXAMLC#AnimationsXamarin MonthCode Snippets