Xamarin.Tip – Adding Dynamic Elevation to Your Xamarin.Forms Buttons

Before Reading

In a previous post, I talked about bringing Material Design to your iOS applications in Xamarin.Forms and adding drop shadows to them. You might want to read that here first: Xamarin.Tips – Creating a Material Design Button in iOS

In another post, we learned how to override the Android Button Elevations. We will be doing this in this post in order to set a dynamic elevation. You can read that here: Xamarin.Tips – Overriding Android Button Shadows/Elevation

Now in this post, we will combine these two concepts with a new custom Xamarin.Forms component called MaterialButton that will have a new Elevation property to control the elevation and shadow of the underlying button control.

The source code and an example can be found here: https://github.com/SuavePirate/MaterialButton

Using the existing code

You can of course use the code I wrote and put on GitHub for this. In order to use it, simply:

  1. Clone the repository at  https://github.com/SuavePirate/MaterialButton
  2. Include all 3 `src` projects in your Solution
  3. Reference the Shared project in all Xamarin.Forms and platform projects
  4. Reference the Android project in your Android projects
  5. Reference the iOS project in your iOS projects
  6. Use the control as below (see example projects to demo)

Reference the control in your XAML:

MainPage.xaml

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"              xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"              xmlns:local="clr-namespace:MaterialButtonExample"              xmlns:suave="clr-namespace:SuaveControls.MaterialButton.Shared;assembly=SuaveControls.MaterialButton.Shared"              x:Class="MaterialButtonExample.MainPage">

	<suave:MaterialButton x:Name="MyButton"                            BackgroundColor="#03A9F4"                            TextColor="White"                            Text="Click to raise elevation"                            Elevation="1"                            VerticalOptions="Center"                            HorizontalOptions="Center"                           WidthRequest="300"                           Clicked="MyButton_Clicked"/>

</ContentPage>

MainPage.xaml.cs

namespace MaterialButtonExample
{
    public partial class MainPage : ContentPage
    {
        public MainPage()
        {
            InitializeComponent();
        }

        private void MyButton_Clicked(object sender, EventArgs e)
        {
            MyButton.Elevation++;
        }
    }
}

In your iOS AppDelegate you’ll also need to call the Initialize method to ensure that the Custom renderer does not get excluded during linking:

AppDelegate.cs

namespace MaterialButtonExample.iOS
{
    [Register("AppDelegate")]
    public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate
    {

        public override bool FinishedLaunching(UIApplication app, NSDictionary options)
        {
            global::Xamarin.Forms.Forms.Init();
            MaterialButtonRenderer.Initialize();
            LoadApplication(new App());

            return base.FinishedLaunching(app, options);
        }
    }
}

Now you can see your results!

Creating Your Own Material Design Button

First things first, let’s create our new Xamarin.Forms control before we implement our custom renderers:

MaterialButton.cs

namespace SuaveControls.MaterialButton.Shared
{
    public class MaterialButton : Button
    {
        public static BindableProperty ElevationProperty = BindableProperty.Create(nameof(Elevation), typeof(float), typeof(MaterialButton), 4.0f);

        public float Elevation
        {
            get
            {
                return (float)GetValue(ElevationProperty);
            }
            set
            {
                SetValue(ElevationProperty, value);
            }
        }
    }
}

Now let’s implement our iOS and Android custom renderers.

iOS:
MaterialButtonRenderer


[assembly: ExportRenderer(typeof(MaterialButton), typeof(MaterialButtonRenderer))]
namespace SuaveControls.MaterialButton.iOS
{
    public class MaterialButtonRenderer : ButtonRenderer
    {
        public static void Initialize()
        {
            // empty, but used for beating the linker
        }
        protected override void OnElementChanged(ElementChangedEventArgs<Button> e)
        {
            base.OnElementChanged(e);

            if (e.NewElement == null)
                return;

        }

        public override void Draw(CGRect rect)
        {
            base.Draw(rect);
            UpdateShadow();
        }

        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);
            if(e.PropertyName == "Elevation")
            {
                UpdateShadow();
            }
        }

        private void UpdateShadow()
        {

            var materialButton = (Shared.MaterialButton)Element;

            // Update shadow to match better material design standards of elevation
            Layer.ShadowRadius = materialButton.Elevation;
            Layer.ShadowColor = UIColor.Gray.CGColor;
            Layer.ShadowOffset = new CGSize(2, 2);
            Layer.ShadowOpacity = 0.80f;
            Layer.ShadowPath = UIBezierPath.FromRect(Layer.Bounds).CGPath;
            Layer.MasksToBounds = false;

        }
    }
}

Notice how we use the UpdateShadow method to use the Elevation property to set the ShadowRadius of our UIButton's Layer.

It’s important to also make the UpdateShadow call in the OnElementPropertyChanged so that we can dynamically change the Elevation property in our Xamarin.Forms control and see it reflected in the underlying UIButton.

Now let’s do it on Android:

MaterialButtonRenderer

[assembly: ExportRenderer(typeof(MaterialButton), typeof(MaterialButtonRenderer))]
namespace SuaveControls.MaterialButton.Droid
{
    public class MaterialButtonRenderer : Xamarin.Forms.Platform.Android.AppCompat.ButtonRenderer
    {
        ///
<summary>
        /// Set up the elevation from load
        /// </summary>

        /// <param name="e"></param>
        protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.Button> e)
        {
            base.OnElementChanged(e);
            if (e.NewElement == null)
                return;

            var materialButton = (Shared.MaterialButton)Element;

            // we need to reset the StateListAnimator to override the setting of Elevation on touch down and release.
            Control.StateListAnimator = new Android.Animation.StateListAnimator();

            // set the elevation manually
            ViewCompat.SetElevation(this, materialButton.Elevation);
            ViewCompat.SetElevation(Control, materialButton.Elevation);
        }

        public override void Draw(Canvas canvas)
        {
            var materialButton = (Shared.MaterialButton)Element;
            Control.Elevation = materialButton.Elevation;
            base.Draw(canvas);
        }

        ///
<summary>
        /// Update the elevation when updated from Xamarin.Forms
        /// </summary>

        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);
            if(e.PropertyName == "Elevation")
            {
                var materialButton = (Shared.MaterialButton)Element;
                ViewCompat.SetElevation(this, materialButton.Elevation);
                ViewCompat.SetElevation(Control, materialButton.Elevation);
                UpdateLayout();
            }
        }
    }
}

Just as mentioned in the iOS implementation, we need to make sure that we implement our changes in both the OnElementChanged method as well as the OnElementPropertyChanged event to ensure we are able to update our Elevation with MVVM.

Now we can use our control in our pages!

MainPage.xaml

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"              xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"              xmlns:local="clr-namespace:MaterialButtonExample"              xmlns:suave="clr-namespace:SuaveControls.MaterialButton.Shared;assembly=SuaveControls.MaterialButton.Shared"              x:Class="MaterialButtonExample.MainPage">

	<suave:MaterialButton x:Name="MyButton"                            BackgroundColor="#03A9F4"                            TextColor="White"                            Text="Click to raise elevation"                            Elevation="1"                            VerticalOptions="Center"                            HorizontalOptions="Center"                           WidthRequest="300"                           Clicked="MyButton_Clicked"/>

</ContentPage>

MainPage.xaml.cs

namespace MaterialButtonExample
{
    public partial class MainPage : ContentPage
    {
        public MainPage()
        {
            InitializeComponent();
        }

        private void MyButton_Clicked(object sender, EventArgs e)
        {
            MyButton.Elevation++;
        }
    }
}

In your iOS AppDelegate you’ll also need to call the Initialize method to ensure that the Custom renderer does not get excluded during linking:

AppDelegate.cs

namespace MaterialButtonExample.iOS
{
    [Register("AppDelegate")]
    public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate
    {

        public override bool FinishedLaunching(UIApplication app, NSDictionary options)
        {
            global::Xamarin.Forms.Forms.Init();
            MaterialButtonRenderer.Initialize();
            LoadApplication(new App());

            return base.FinishedLaunching(app, options);
        }
    }
}

Now you can see your results!

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.

2 thoughts on “Xamarin.Tip – Adding Dynamic Elevation to Your Xamarin.Forms Buttons”

Leave a comment