Animated Menus (satellite, drop down, circle...)

Hi everyone.

I happened to struggle with the Satellite Menu integration in Xamarin Forms via custom renderers, just to find out it didn't quite suited my needs.
So, I created some animated menus that will fit better in my apps and I thought it would be useful to some of you.

Xamarin Animated Menu

It is full C#, only code, no XAML, fit in a ContentPage and is fully editable. It is really messy, badly commented, the math functions are what they are but I plan on improving only the parts I use in further projects, so it will stay that way. If you want to modify it, reap it or use it, just do what you want:

`// Animated Menus for Xamarin Forms
// by Julien Chateau
// Open source and editable, no license
// As it is, realy messy, can't be held responsible for any use of this code
// and its consequences.

using System;
using Xamarin.Forms;
using System.Diagnostics;

namespace DynamicMenus
{
class DynamicMenus : ContentPage
{
Button[] Buttons;
Button menubutton;

    Label texte;
    Label titre;

    DateTime beginTime;
    AbsoluteLayout absoluteLayout;

    bool isCurrentPage;
    bool okMove = false;
    float each = 0.0f;
    float eachinv = 0.0f;
    double seconds = 0;
    double offset = 0;
    double RatioScreen;
    double delta = frac;
    static double frac = 0.25;      // Fraction de seconde to expand.

    string TypeMenu = "Dropdown";       // DropDown, Arc, Side ...

    public DynamicMenu_DropDown ()
    {
        NavigationPage.SetHasNavigationBar (this, false);

        Buttons = new Button[] {
            new Button{ Text = "",   Image = "MenuDeroulant/icon1.png", },
            new Button{ Text = "",   Image = "MenuDeroulant/icon2.png", },
            new Button{ Text = "",   Image = "MenuDeroulant/icon3.png", },
            new Button{ Text = "",   Image = "MenuDeroulant/icon4.png", },
            new Button{ Text = "",   Image = "MenuDeroulant/icon5.png", }
        };
        //*  Each button click refers to OnButtonClicked *//s
        for (int i = 0; i < Buttons.Length; i++) {
            Buttons [i].Clicked += OnButtonClicked;
        }


        texte = new Label {
            Text = "Dropdown",
            Font = Font.SystemFontOfSize (NamedSize.Large),
            TextColor = Color.Maroon,
            HorizontalOptions = LayoutOptions.Center,
            VerticalOptions = LayoutOptions.CenterAndExpand,
            HeightRequest = 200
        };

        titre = new Label {
            Text = "Animated Buttons",
            Font = Font.SystemFontOfSize (NamedSize.Large),
            HorizontalOptions = LayoutOptions.Center,
            TextColor = Color.Maroon,
            VerticalOptions = LayoutOptions.Start,
            HeightRequest = 30
        };

        menubutton = new Button {
            Text = "",
            Font = Font.SystemFontOfSize (NamedSize.Medium),
            FontAttributes = FontAttributes.Bold,
            BackgroundColor = Color.Transparent,
            //BorderWidth = 2,
            //BorderColor = Color.White,
            TextColor = Color.White,
            Image = "MenuDeroulant/menu.png",
            HorizontalOptions = LayoutOptions.Start,
            VerticalOptions = LayoutOptions.Start,
            Opacity = 0.95,
        };
        menubutton.Clicked += OnButtonClicked;

        absoluteLayout = new AbsoluteLayout {
            BackgroundColor = Color.Blue.WithLuminosity (0.8),
            VerticalOptions = LayoutOptions.FillAndExpand
        };

        foreach (Button but in Buttons) {
            absoluteLayout.Children.Add (but);
            AbsoluteLayout.SetLayoutFlags (but,
                AbsoluteLayoutFlags.PositionProportional);
        }

        absoluteLayout.Children.Add (menubutton);
        AbsoluteLayout.SetLayoutFlags (menubutton,
            AbsoluteLayoutFlags.PositionProportional);

        absoluteLayout.Children.Add (texte);
        AbsoluteLayout.SetLayoutFlags (texte,
            AbsoluteLayoutFlags.PositionProportional);
        AbsoluteLayout.SetLayoutBounds (texte,
            new Rectangle (0.5, 0.5, 
                AbsoluteLayout.AutoSize, AbsoluteLayout.AutoSize));


        this.Content = new StackLayout {
            Children = {
                titre,
                absoluteLayout
            }
        };
    }

    void OnButtonClicked (object sender, EventArgs e)
    {
        isCurrentPage = true;
        //* Button Menu *//

        if (sender == menubutton) {             
            //* Sub buttons of the Menu *//
            //Just an example of action
        } else if (sender == Buttons [4]) {
            delta = frac;
            menubutton.Text = "";
            TypeMenu = "Dropdown";
            AbsoluteLayout.SetLayoutBounds (menubutton,
                new Rectangle (1, 0, 
                    AbsoluteLayout.AutoSize, AbsoluteLayout.AutoSize));
        } else if (sender == Buttons [3]) {
            delta = frac;
            menubutton.Text = "";
            TypeMenu = "Side";
            AbsoluteLayout.SetLayoutBounds (menubutton,
                new Rectangle (1, 0, 
                    AbsoluteLayout.AutoSize, AbsoluteLayout.AutoSize));
        } else if (sender == Buttons [2]) {
            delta = frac;
            menubutton.Text = "";
            TypeMenu = "Arc";
            AbsoluteLayout.SetLayoutBounds (menubutton,
                new Rectangle (1, 0, 
                    AbsoluteLayout.AutoSize, AbsoluteLayout.AutoSize));
        } else if (sender == Buttons [1]) {
            delta = frac;
            menubutton.Text = "";
            TypeMenu = "DemiCircle";
            AbsoluteLayout.SetLayoutBounds (menubutton,
                new Rectangle (0.5, 1, 
                    AbsoluteLayout.AutoSize, AbsoluteLayout.AutoSize));
        } else if (sender == Buttons [0]) {
            delta = frac;
            menubutton.Text = "";
            TypeMenu = "FlatBottom";
            AbsoluteLayout.SetLayoutBounds (menubutton,
                new Rectangle (0.5, 1, 
                    AbsoluteLayout.AutoSize, AbsoluteLayout.AutoSize));
        }
        texte.Text = TypeMenu;
        ExpandContractMenu ();
    }

    void ExpandContractMenu ()
    {
        RatioScreen = absoluteLayout.Height / absoluteLayout.Width;
        Debug.WriteLine ("\nH : " + absoluteLayout.Height +"\nW : " + absoluteLayout.Width + "\nRatio :  " + RatioScreen);
        if (!okMove) {
            okMove = true;
            //* delta start value (here 0,25) is half the time required to expand or contract *//
            if (delta == 0) {
                delta = frac;
            } else {
                delta = 0;
            }

            seconds = 0;
            offset = 0;
            beginTime = DateTime.Now;
            Device.StartTimer (TimeSpan.FromSeconds (1.0 / 60), () => {

                if (okMove && seconds < frac) { 
                    if (TypeMenu == "Dropdown") {
                        seconds = Math.Min ((DateTime.Now - beginTime).TotalSeconds, frac);
                        offset = seconds + delta;
                        //* convert linear transition to sinusoid *//
                        //* Multiply PI by twice the time required to expand or contract *//
                        //* substract half a delta from offset *//
                        //* divide the whole thing by twice the screen height division you want (2 for the whole screen, 4 for half, 8 for a quarter...) *//
                        //  http://www.abhortsoft.hu/functionvisualizer/functionvisualizer.html
                        double offsetB = (Math.Sin (4 * Math.PI * (offset - 0.125)) + 1) / 4;

                        for (int i = 0; i < Buttons.Length; i++) {
                            float div = 1 / (float)Buttons.Length; // Gives a fraction of distance.
                            each = (i * div); // Assign that fraction multiplied by each place in the collection
                            eachinv = Math.Abs (each - Buttons.Length); // reversed direction
                            AbsoluteLayout.SetLayoutBounds (Buttons [i],
                                new Rectangle (1, offsetB - ((double)each * offsetB), 
                                    AbsoluteLayout.AutoSize, AbsoluteLayout.AutoSize));
                        }
                    } else if (TypeMenu == "Side") {
                        seconds = Math.Min ((DateTime.Now - beginTime).TotalSeconds, frac);
                        offset = seconds + delta;
                        //* convert linear transition to sinusoid *//
                        double offsetB = (Math.Sin (4 * Math.PI * (offset - 0.125)) + 1) / 4;

                        for (int i = 0; i < Buttons.Length; i++) {
                            float div = 1 / (float)Buttons.Length;
                            each = (i * div);
                            eachinv = Math.Abs (each - Buttons.Length);
                            AbsoluteLayout.SetLayoutBounds (Buttons [i],
                                new Rectangle (1 - (2 * (offsetB - ((double)each * offsetB))), 0, 
                                    AbsoluteLayout.AutoSize, AbsoluteLayout.AutoSize));
                        }
                    } else if (TypeMenu == "Arc") {
                        seconds = Math.Min ((DateTime.Now - beginTime).TotalSeconds, frac);
                        offset = seconds + delta;
                        double arkmult = 0.0;
                        if (delta == frac)
                            arkmult = Math.Abs (frac - seconds); // arkmult multiply the arker, so when 
                        if (delta == 0)
                            arkmult = seconds;
                        //* convert linear transition to sinusoid *//
                        double offsetB = (Math.Sin (4 * Math.PI * (offset - 0.125)) + 1) / 8;

                        for (int i = 0; i < Buttons.Length; i++) {
                            float j = ((float)Math.Sin (0.75f * ((float)i - 2.0f)) + 1.0f) * 2.0f; // really fake radiality of the linear  0 1 2 3 4
                            double arker = (0.3 * (Math.Sin (Math.Acos ((0.5 * j) - 1))));  // Draws an Arc for values 0 to 4 (adjust the 0,5 double to fit your collection size


                            float div = 1 / (float)Buttons.Length;
                            each = ((j + 2 * i) / 3 * div) + (1 / (float)Buttons.Length);
                            eachinv = Math.Abs (((j + 2 * i) / 3 * div) - 1);
                            AbsoluteLayout.SetLayoutBounds (Buttons [i],
                                new Rectangle ((1 - (RatioScreen * (offsetB - ((double)each * offsetB)))) - ((arker) * arkmult), (offsetB - ((double)eachinv * offsetB)) + ((arker / RatioScreen) * arkmult), 
                                    AbsoluteLayout.AutoSize, AbsoluteLayout.AutoSize));
                        }
                    } else if (TypeMenu == "DemiCircle") {
                        seconds = Math.Min ((DateTime.Now - beginTime).TotalSeconds, frac);
                        offset = seconds + delta;
                        double arkmult = 0.0;
                        if (delta == frac)
                            arkmult = Math.Abs (frac - seconds); // arkmult multiply the arker, so when 
                        if (delta == 0)
                            arkmult = seconds;
                        //* convert linear transition to sinusoid *//
                        double offsetA = (Math.Sin (4 * Math.PI * (offset - 0.125)) + 1) / 8;

                        for (int i = 0; i < Buttons.Length; i++) {
                            float j = ((float)Math.Sin (0.75f * ((float)i - 2.0f)) + 1.0f) * 2.0f; // really fake radiality of the linear  0 1 2 3 4
                            double arkerY = 0.14 * Math.Sin (Math.Acos ((j * 0.5 * (arkmult / frac)) - 1)); // Demi circle equation
                            float lifter = (float)Math.Min (0.0f, 5.0f * (float)Math.Pow ((5.0f * (float)arkmult - 0.5f), 5.0f));
                            float div = 1.0f / (float)Buttons.Length;
                            each = ((j + i) / 2 * div);
                            eachinv = Math.Abs (((j + i) / 2 * div) - 1);
                            AbsoluteLayout.SetLayoutBounds (Buttons [i],
                                new Rectangle (div + (3 * (offsetA - ((double)eachinv * offsetA))), 1 - (arkerY) - (double)lifter,    // the 0,5 in X is to center horizontally
                                    AbsoluteLayout.AutoSize, AbsoluteLayout.AutoSize));
                        }

                    } else if (TypeMenu == "FlatBottom") { // Button 3 stays behind the menu button
                        seconds = Math.Min ((DateTime.Now - beginTime).TotalSeconds, frac);
                        offset = seconds + delta;
                        double arkmult = 0.0;
                        if (delta == frac)
                            arkmult = Math.Abs (frac - seconds); // arkmult multiply the arker, so when 
                        if (delta == 0)
                            arkmult = seconds;

                        for (int i = 0; i < Buttons.Length; i++) {
                            float deltX = (Buttons.Length - 1.0f) / 5.0f;  // really not sure about that, it works at 5 items but would explode with any other number
                            double arkerX = (deltX * i) - (2 * deltX); // moves the buttons on x

                            Debug.WriteLine (i + " : " + arkerX);
                            float div = 0.5f / (float)Buttons.Length;
                            each = (i * div);
                            eachinv = Math.Abs ((i * div) - 1);
                            AbsoluteLayout.SetLayoutBounds (Buttons [i],
                                new Rectangle (0.5 + (arkerX * arkmult), 1,    // the 0,5 in X is to center horizontally
                                    AbsoluteLayout.AutoSize, AbsoluteLayout.AutoSize));
                        }
                    }

                    return isCurrentPage;
                } else {
                    okMove = false;

                    isCurrentPage = false;
                    return isCurrentPage;
                }



            });
        }
    }

    protected override void OnAppearing ()
    {
        base.OnAppearing ();
        isCurrentPage = true;
        beginTime = DateTime.Now;

        for (int i = 0; i < Buttons.Length; i++) {
            AbsoluteLayout.SetLayoutBounds (Buttons [i],
                new Rectangle (1, -1, 
                    AbsoluteLayout.AutoSize, AbsoluteLayout.AutoSize));
        }
        AbsoluteLayout.SetLayoutBounds (menubutton,
            new Rectangle (1, 0, 
                AbsoluteLayout.AutoSize, AbsoluteLayout.AutoSize));
        if (TypeMenu == "DemiCircle" || TypeMenu == "Flatbottom") {
            AbsoluteLayout.SetLayoutBounds (menubutton,
                new Rectangle (0.5, 1, 
                    AbsoluteLayout.AutoSize, AbsoluteLayout.AutoSize));
        }


    }

    protected override void OnDisappearing ()
    {
        base.OnDisappearing ();
        isCurrentPage = false;
    }
}

}
`

Posts

  • AlessandroCaliaroAlessandroCaliaro ITMember ✭✭✭✭✭

    @JulienChateau good work and thanks to share your code. Would be nice if you can put this project to a github repository so the community could help you to maintain it

  • JulienChateauJulienChateau FRMember ✭✭

    @AlessandroCaliaro Yeah, why not. I'll do that tomorrow. It would be nice if people improve it, add animations or add implementations. Good idea.
    Ju

  • JonNowickJonNowick USMember

    @JulienChateau Fantastic work! Have you put this on Git yet? I have been looking for something like this. I started looking at your code and I was wondering if you could help me with the DemiCircle menu. Is there a way to change it so it will work on different numbers of buttons?

  • JulienChateauJulienChateau FRMember ✭✭

    Thx @JonNowick . No, it is still not on GitHub, but everything (except pngs, which you can find in the satellite menu component samples) is there. Demi Circle and Flat Bottom are the hardest to tweak and I didn't test with more items but you definetly can add more. I got too much on my plate at the moment to help you with that, but I'll try as soon as I can.
    Julien

  • JulienChateauJulienChateau FRMember ✭✭

    @JonNowick Hey, sorry, still no GitHub, but I took some time to find the right equations for 6 and 7 items. You can tweak it easily if you use a math function visualizer (ie. abhortsoft.hu). You'll find the 5, 6 and 7 items solution in the following comments; there are 2 lines to change (and 3 values). It is very empiric, but as long as it looks right...

    `} else if (TypeMenu == "DemiCircle") {
    seconds = Math.Min ((DateTime.Now - beginTime).TotalSeconds, frac);
    offset = seconds + delta;
    double arkmult = 0.0;
    if (delta == frac)
    arkmult = Math.Abs (frac - seconds); // arkmult multiply the arker, so when
    if (delta == 0)
    arkmult = seconds;
    //* convert linear transition to sinusoid *//
    double offsetA = (Math.Sin (4 * Math.PI * (offset - 0.125)) + 1) / 8;

                            for (int i = 0; i < Buttons.Length; i++) {
                                float j = ((float)Math.Sin (0.52f * ((float)i - 2.95f)) + 1.0f) * 2.0f; // really fake radiality of the linear  0 1 2 3 4...  ; (0.75f*(i-2.0f)) for 5 items ; (0.62f*(i-2.5f)) for 6 items ; (0.52f*(i-2.95f)) for 6 items
                                double arkerY = 0.14 * Math.Sin (Math.Acos ((j * 0.5 * (arkmult / frac)) - 1)); // Demi circle equation
                                float lifter = (float)Math.Min (0.0f, 5.0f * (float)Math.Pow ((5.0f * (float)arkmult - 0.5f), 5.0f));
                                float div = 1.0f / (float)Buttons.Length;
                                each = ((j + i) / 2 * div);
                                eachinv = Math.Abs (((j + i) / 2 * div) - 1);
                                AbsoluteLayout.SetLayoutBounds (Buttons [i],
                                    new Rectangle (div + (4.0 * (offsetA - ((double)eachinv * offsetA))), 1 - (arkerY) - (double)lifter,    // the 0,5 in X is to center horizontally   ; (3.0* for 5 items ;  (3.5* for 6 items ;  (3.0* for 5 items ;  (3.5* for 7 items ;
                                        AbsoluteLayout.AutoSize, AbsoluteLayout.AutoSize));
                            }`
    
  • JonNowickJonNowick USMember

    @JulienChateau Thanks for the reply. I am still tweaking your examples to fit my needs but I have to say your mathfu is much greater than mine. I have been attempting to fit your animations to fit anywhere from 3 buttons to 8 buttons. I have also been trying to adjust both the x and y starting positions to make them fit in different views. I have been successful with more buttons but not with less.

  • JulienChateauJulienChateau FRMember ✭✭

    @JonNowik Ah ah, and still, my math-fu is some "caveman playing with his feces" level compared to some of my coworker's...
    It is really empiric, I go there: http://www.abhortsoft.hu/functionvisualizer/functionvisualizer.html and then I tweak the functions to look like what I need. If you want to modify my functions, copy them in the visualizer and adjust the values so the graphic matches the number you're looking for. If you have a particuliar request, hit me, I'll manage to find the time to try something.

  • PaulHoetsPaulHoets USMember ✭✭

    Wow. This is really impressive.

    If you put it on Github, people would come from far and wide for it.
    You've really made something valuable, but its extremely generous of you to share it - thank you!

  • PeterBaumanPeterBauman USMember

    @JonNowik, please share your code too!, this code is great. Thanks @JonNowick !!

  • PeterBaumanPeterBauman USMember

    Hi everyone,
    Y really need to make a circle menu like this code show, but a complete circle, as myndly app. You can see it in this image, one in the center, and the others around it.
    Can you please help me?.
    Thanks!

  • RAM502RAM502 INMember

    HI Peter Bauman,
    Are You able to Create the Circle Menu as shown above.Can You share the Logic.

  • RavinderJangraRavinderJangra USMember ✭✭✭

    @PeterBauman did you find any solution? please send if any

Sign In or Register to comment.