Xamarin.Controls – Xamarin.Forms FloatingActionButton (including iOS!)

You did actually read that title correctly – we have a FloatingActionButton to use in Xamarin.Forms that works in both Android and iOS!

I’ve put the source code up for this here: https://github.com/SuavePirate/Xamarin.Forms.Controls.FloatingActionButton

It’s rudimentary and has room for some more fun properties, but it is fully functional! If you would like to contribute to the repository, see the TODO: list at the bottom of the README and start forking and making pull requests!

To breakdown the steps to create your own Floating Action Button in Xamarin.Forms, you’ll need:

  1. A custom Xamarin.Forms `Element`
  2. An Android Custom renderer to use the native `Android.Compat.Design.Widgets.FloatingActionButton`
  3. An iOS Custom renderer to create a button that looks like a FAB.

So let’s go in that order.

In Xamarin.Forms PCL

FloatingActionButton.xaml.cs

  public partial class FloatingActionButton : Button
    {
        public static BindableProperty ButtonColorProperty = BindableProperty.Create(nameof(ButtonColor), typeof(Color), typeof(FloatingActionButton), Color.Accent);
        public Color ButtonColor
        {
            get
            {
                return (Color)GetValue(ButtonColorProperty);
            }
            set
            {
                SetValue(ButtonColorProperty, value);
            }
        }
        public FloatingActionButton()
        {
            InitializeComponent();
        }
    }

We added a new BindableProperty for the ButtonColor. This is done because setting the BackgroundColor will mess up the Android renderer and apply the background behind the FAB. We want to inherit from Button so that we can utilize some of the already useful properties that come with it – namely the Image property that consumes a FileImageSource. We can use this to set the icon for our FAB.

In Android

FloatingActionButtonRenderer.cs

using FAB = Android.Support.Design.Widget.FloatingActionButton;

[assembly: ExportRenderer(typeof(SuaveControls.Views.FloatingActionButton), typeof(FloatingActionButtonRenderer))]
namespace SuaveControls.FloatingActionButton.Droid.Renderers
{
    public class FloatingActionButtonRenderer : Xamarin.Forms.Platform.Android.AppCompat.ViewRenderer<SuaveControls.Views.FloatingActionButton, FAB>
    {
        protected override void OnElementChanged(ElementChangedEventArgs<SuaveControls.Views.FloatingActionButton> e)
        {
            base.OnElementChanged(e);

            if (e.NewElement == null)
                return;

            var fab = new FAB(Context);
            // set the bg
            fab.BackgroundTintList = ColorStateList.ValueOf(Element.ButtonColor.ToAndroid());

            // set the icon
            var elementImage = Element.Image;
            var imageFile = elementImage?.File;

            if (imageFile != null)
            {
                fab.SetImageDrawable(Context.Resources.GetDrawable(imageFile));
            }
            fab.Click += Fab_Click;
            SetNativeControl(fab);

        }
        protected override void OnLayout(bool changed, int l, int t, int r, int b)
        {
            base.OnLayout(changed, l, t, r, b);
            Control.BringToFront();
        }

        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            var fab = (FAB)Control;
            if (e.PropertyName == nameof(Element.ButtonColor))
            {
                fab.BackgroundTintList = ColorStateList.ValueOf(Element.ButtonColor.ToAndroid());
            }
            if (e.PropertyName == nameof(Element.Image))
            {
                var elementImage = Element.Image;
                var imageFile = elementImage?.File;

                if (imageFile != null)
                {
                    fab.SetImageDrawable(Context.Resources.GetDrawable(imageFile));
                }
            }
            base.OnElementPropertyChanged(sender, e);

        }

        private void Fab_Click(object sender, EventArgs e)
        {
            // proxy the click to the element
            ((IButtonController)Element).SendClicked();
        }
    }
}

A few important things to point out:

  • We add the additional using statement `using FAB = Android.Support.Design.Widget.FloatingActionButton;` to help us distinguish between our Xamarin.Forms element and the built in Android control.
  • We are NOT using a `ButtonRenderer` as our base class, but instead using a basic `ViewRenderer`. This is because the underlying control will not be a native Android `Button`, but the native Android `FloatingActionButton`.
  • Because we replace the `ButtonRenderer`, we need to make sure we still propagate click events up to the Xamarin.Forms element.

Now let’s look at iOS, which can utilize more of the built in pieces from Xamarin.Forms since it supports the BorderRadius property on Buttons.

In iOS

FloatingActionButtonRenderer.cs

[assembly: ExportRenderer(typeof(SuaveControls.Views.FloatingActionButton), typeof(FloatingActionButtonRenderer))]
namespace SuaveControls.FloatingActionButton.iOS.Renderers
{
    [Preserve]
    public class FloatingActionButtonRenderer : ButtonRenderer
    {
        public static void InitRenderer()
        {
        }
        public FloatingActionButtonRenderer()
        {
        }
        protected override void OnElementChanged(ElementChangedEventArgs<Button> e)
        {
            base.OnElementChanged(e);

            if (e.NewElement == null)
                return;

            // remove text from button and set the width/height/radius
            Element.WidthRequest = 50;
            Element.HeightRequest = 50;
            Element.BorderRadius = 25;
            Element.BorderWidth = 0;
            Element.Text = null;

            // set background
            Control.BackgroundColor = ((SuaveControls.Views.FloatingActionButton)Element).ButtonColor.ToUIColor();
        }
        public override void Draw(CGRect rect)
        {
            base.Draw(rect);
            // add shadow
            Layer.ShadowRadius = 2.0f;
            Layer.ShadowColor = UIColor.Black.CGColor;
            Layer.ShadowOffset = new CGSize(1, 1);
            Layer.ShadowOpacity = 0.80f;
            Layer.ShadowPath = UIBezierPath.FromOval(Layer.Bounds).CGPath;
            Layer.MasksToBounds = false;

        }
        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);

            if (e.PropertyName == "ButtonColor")
            {
                Control.BackgroundColor = ((SuaveControls.Views.FloatingActionButton)Element).ButtonColor.ToUIColor();
            }
        }
    }
}

We set an explicit WidthRequest, HeightRequest, and BorderRadius to get ourselves a circle. I’m not a big fan of doing it here, since it’s better suited as a calculation, but for now it works.

Lastly in our Draw override, we set up the drop shadow behind out button, and make sure that our ShadowPath is actually built from an oval so that it rounds off with the Button.
Also note that we take the ButtonColor property and apply it as the BackgroundColor of the UIButton to override the color from Xamarin.Forms. Don’t forget to set Text to null so that we can’t add text to the button and mess it up.

As a side note, iOS might try to link our your custom renderer if you are using it in an iOS Class Library. In order to avoid this, make sure to call a static InitRenderer method in your AppDelegate.cs as it will prevent it from being linked out.

Using the FloatingActionButton

Now that we have our renderers registered for our new Element, we can use it in our XAML or C# of our PCL or Shared Project:

MainPage.xaml

<?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:local="clr-namespace:SuaveControls.FabExample"              xmlns:controls="clr-namespace:SuaveControls.Views;assembly=SuaveControls.FloatingActionButton"              x:Class="SuaveControls.FabExample.MainPage">
    <StackLayout Margin="32">
        <Label Text="This is a Floating Action Button!"             VerticalOptions="Center"             HorizontalOptions="Center"/>

        <controls:FloatingActionButton x:Name="FAB" HorizontalOptions="CenterAndExpand" WidthRequest="50" HeightRequest="50"  VerticalOptions="CenterAndExpand" Image="ic_add_white.png" ButtonColor="#03A9F4" Clicked="Button_Clicked"/>
    </StackLayout>
</ContentPage>

and our code behind:

MainPage.xaml.cs

namespace SuaveControls.FabExample
{
    public partial class MainPage : ContentPage
    {
        public MainPage()
        {
            InitializeComponent();

        }

        private async void Button_Clicked(object sender, EventArgs e)
        {
            await DisplayAlert("FAB Clicked!", "Congrats on creating your FAB!", "Thanks!");
        }
    }
}

Then we get these results in our Android and iOS apps:

Android

Screenshot_1493173400

iOS

2017-04-25_10-38-38-PM

If you want to just pull down the control I built on GitHub, the steps are straight forward:

  1. Clone the repository
  2. Reference the PCL in your PCL/Shared Lib
  3. Reference the PCL and native projects in your respective native project
  4. Pull the namespace into your XAML (or C#)
  5. Start using it!

The repository also contains an example app that references the source libraries.

If you like what you see, don’t forget to follow me on twitter @Suave_Pirate, check out my GitHub, and subscribe to my blog to learn more mobile developer tips and tricks!

Interested in sponsoring developer content? Message @Suave_Pirate on twitter for details.

Advertisements

15 thoughts on “Xamarin.Controls – Xamarin.Forms FloatingActionButton (including iOS!)”

  1. Hi Alex,

    I notice that the shadow in Android looks kind of wierd? The one in iOS looks nice and natural where as the android one finishes abruptly. Is there a way to fix this in android similar to how you override draw in iOS renderer?

    Cheers,
    Mel

    Like

    1. It’s a bug in the native Android support library when there isn’t enough margin around the button and shadow. Try updating the underlying renderer to give it some natural margin.

      Like

      1. Thank you for your fast answer! I have tried to put it in a stacklayout but still nothing gets rendered! However if i remove the fab its ok even without a layout! :/

        Like

      2. Well yeah, if you remove the fab with no layout the label will render. The problem is that in order to have 2 or more controls render, they need to be in a layout. I’ll try to take a second look for other issues.

        Like

  2. Hey Alex, I’ve got a really weird issue. I implemented your FAB a few months ago and it’s been working great. However, I’ve made some changes recently that have been giving me some issues. I am using the FAB to navigate to a different page using PushAsync: await Navigation.PushAsync(new MapPage(), false);

    I originally had the MapPage I was navigating to set up with a navigation bar, but I have since decided to take it off, so I am setting NavigationPage.SetHasNavigationBar(this, false); in the MapPage. After I did that, and am navigating back to the list page: await Navigation.PopAsync(false); the FAB no longer shows up. I am currently using an absolute layout to get it to show up in the middle of the page at the bottom. If I get rid of the Absolute Layout options in the StackLayout below and it by default shows up at the top of the page, it will still show up after navigating back. Below is the bit of XAML from the page that has the FAB on it:

    So, it’s something to do with the AbsoluteLayout and navigating back from a page without a navigation bar, but I can’t figure out what’s going on. I have a pretty major project going on and this is the final issue we are having, so if you need sample code just let me know and I can try to create a test sample app for you. Thanks!

    Like

    1. Well, I guess my XAML was removed. It wasn’t anything special other than to show you that my stack layout i had wrapped around the FAB had these parameters: AbsoluteLayout.LayoutFlags=”PositionProportional” and AbsoluteLayout.LayoutBounds=”0.5,1,-1,-1″

      Like

      1. This seems like a general issue with the Absolute layout and the navigation bar change. Have you tried swapping the FAB out for a regular button and seeing if the same thing happens? I’ve seen this happen before and was a tracked bug that has come back a few times in Xamarin.Forms. What versions are you using for Xamarin.Forms and Xamarin? Also what platforms are you seeing this on?

        Like

  3. Couldn’t reply to your question directly – but yeah, I’m running current versions from the stable channel for everything (VS Mac v7.0.1, Xamarin.Android v7.3.1.2, Xcode v8.3.3, Xamarin.iOS v10.10.0.36) except for the XF nuget package. We are still on 2.3.4.231 because of an issue we were having with our release build with the latest.

    I just tried it with a regular button, and I’m still having the same issues, so I guess it is unrelated to the FAB. Sorry, I didn’t think to try that before. Also, I’m only getting the issue on iOS; Android is fine. I’ll keep digging to see if I can figure something out, but if you have any ideas, please let me know. Thanks!

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s