May 29, 2019

Extending Visual Material in Xamarin.Forms

Xamarin.Forms Visual is out along with the Material Renderers and they are awesome. But I want more...

Extending Visual Material in Xamarin.Forms

Xamarin.Forms Visual is out along with the Material Renderers and they are awesome.  They make life much easier for developers wanting to offer consistent appearance of apps on Android and iOS.

The first offering of Visual is Material which lets you make your apps fit Google's Material Design on iOS and Android. There'll be more coming from Xamarin and you can write your own. In this post I'll be focusing on creating your own Visual by extending Material. If you want an overview of Visual and how to get started I recommend David Ortinau's Beautiful Material Design for Android & iOS.

For this post I've created a ContentPage with all the controls that have Material Renderers. I've tried to keep it as simple as possible but still show the controls in a close to real world situation.  All the code is open source on GitHub at https://github.com/lachlanwgordon/ExtendedVisualSample.  So that I can easily swap between Visuals I've put the same page in a Shell (another awesome new feature) three times with Tab Navigation to switch between them.

If you're playing along at home you'll need to install Xamarin.Forms.Material.Visual in your Android and iOS projects and initialise them with a call to FormsMaterial.Init(); in your AppDelegate and a FormsMaterial.Init(this, savedInstanceState); in your MainActivity. Make sure the version matches the Xamarin.Forms you're running and that they're all 3.6 or higher.

Here's my ugly page with minimal styling that looks wildly different between iOS and Android.

Bleurghhh

Now here's the same page with one little Visual="Material" on the ContentPage

Ahhh, a sigh of relief

Everything looks better, or at least a lot more consistent. Highlights for me are:

  • The cute little hint that gets smaller when you start typing in an entry
  • Buttons have the same default transparency
  • Button shadows
  • Frame shadows

What's with the BUTTONS? Time to extend

After setting Visual to Material, we are now using the renderers provided by Google as part of Material design. For the most part I love them but:

  • I don't want ALL CAPS on my buttons
  • The picker doesn't have a title/placeholder
  • I want the title on the picker to do the funky little thing that the entry does when you have something selected

Step 1. Create a Visual

Create a new class somewhere in your .Net Standard library. I'm calling mine CustomVisual and putting it in the root of the project. The class name should end with Visual and extend the IVisual Class.

public class CustomVisual : IVisual
{
}

This is a very simple class that we don't need to add anything more to. It's just a marker so that our renderers can say "Hey, I'm part of CustomVisual" and our views can say "Give me the ButtonRenderer for CustomVisual" and everyone can agree that CustomVisual means something in a type safe way.

Step 2. Extend the ButtonRenderer

Inside your iOS project create a new C# file for the renderer, I'm calling mine AnyCaseButtonRenderer but you can call it anything. I like to give my renderers a name that communicates why I have created my own renderer rather than calling it ExtendedButtonRenderer, this stops making sense if you're customising multiple aspects of it.

Inside the renderer you need to:

  • Subclass the MaterialButtonRenderer
  • Make any changes to renderer(Usually in OnElementChanged)
  • Export the renderer - This line will look familiar but is a little different when working with Visual.

The final renderer looks like:

using VisualExtension;
using VisualExtension.iOS.Renderers;
using Xamarin.Forms;
using Xamarin.Forms.Material.iOS;
using Xamarin.Forms.Platform.iOS;

[assembly: ExportRenderer(typeof(Button), typeof(AnyCaseButtonRenderer), new[] {typeof(CustomVisual) } ) ]
namespace VisualExtension.iOS.Renderers
{
    public class AnyCaseButtonRenderer : MaterialButtonRenderer
    {
        protected override void OnElementChanged(ElementChangedEventArgs<Button> e)
        {
            base.OnElementChanged(e);
            if(Control != null)
            {
                Control.UppercaseTitle = false;
            }
        }
    }
}

The export statement takes three arguments:

  • A handler e.g. typeof(Button). This is saying what Xamarin.Forms type you will use in your UI code to get this new button. By using Button I won't need a subclass of CustomButton in my UI project and I won't have to replace every instance of Button throughout the code.
  • Target e.g. AnyCaseButtonRenderer. This is the Renderer that is doing all the work. This should always be the Renderer defined in the file you're working in unless you hate your coworkers and want them to go on a wild goose chase.
  • Supported Visuals e.g. new[] {typeof(CustomVisual) }. This is the new argument that declares what Visuals the renderer is part of. You can put multiple Visuals in here but I'll stick to one for now.

All up this line says "Hey, any time the UI asks for a Button with Visual="Custom", give them an AnyCaseButtonRenderer".

The Android Renderer is very similar.

using System;
using Android.Content;
using VisualExtension;
using VisualExtension.Droid.Renderers;
using Xamarin.Forms;
using Xamarin.Forms.Material.Android;
using Xamarin.Forms.Platform.Android;

[assembly: ExportRenderer(typeof(Button), typeof(AnyCaseButtonRenderer), new[] { typeof(CustomVisual) })]
namespace VisualExtension.Droid.Renderers
{
    public class AnyCaseButtonRenderer : MaterialButtonRenderer
    {
        public AnyCaseButtonRenderer(Context context) : base(context)
        {
        }

        public AnyCaseButtonRenderer(Context context, BindableObject element) : base(context, element)
        {
        }

        protected override void OnElementChanged(ElementChangedEventArgs<Button> e)
        {
            base.OnElementChanged(e);
            if(Control != null)
            {
                Control.SetAllCaps(false);
            }
        }
    }
}

Step 3.Set Visual="Custom" in your UI code

Open up a .xaml file with a Button in it and set Visual="Custom" on your Button and it will work.

<Button Visual="Custom" Text="Button" />

Better yet, set it on your ContentPage and all your Buttons will be updated.

<ContentPage
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns="http://xamarin.com/schemas/2014/forms"
    x:Class="VisualExtension.Pages.SimplePage"
    Visual="Material">
    ...
</ContentPage>

Or if you want to go all in, set it in your AppShell and every button in your app will use the new renderer. This is the Shell I use in this demo to let me easily switch between Visuals.

<?xml version="1.0" encoding="UTF-8"?>
<Shell
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    Visual="Custom"
    FlyoutBehavior="Disabled"
    x:Class="VisualExtension.AppShell"
    xmlns:pages="clr-namespace:VisualExtension.Pages">
    <ShellItem>
        <pages:FormPage Title="Default" Visual="Default"/>
        <pages:FormPage Title="Material" Visual="Material"/>
        <pages:FormPage Title="Custom Visual" Visual="Custom"/>
    </ShellItem>
</Shell>

Note that Visual="Custom" even though the class is called CustomVisual Xamarin.Forms is smart enough to know you want a Visual so you don't have to type it out in full. Visual="CustomVisual" is also valid but nobody got time for that.

CustomVisual with all the default renderers except for my ButtonRenderer which extends MaterialButtonRenderer

Step 4. Declare the rest of the renderers

The buttons are looking great but when we went moved to CustomVisual we lost out on all the other Material renderers.

In your iOS project create a file called Visual CustomVisualRenderersList.cs. This class isn't actually doing to do anything. It's just going to have bunch of assembly exports. Each of these exports is very similar to the one on the button except instead of using the AnyCaseButtonRenderer, we just use the Material renderer e.g. MaterialEditorRenderer

In between your using statements and you namespace add all these Exports:

using Xamarin.Forms;
using Xamarin.Forms.Material.iOS;
[assembly: ExportRenderer(typeof(Editor), typeof(MaterialEditorRenderer), new[] { typeof(CustomVisual) })]
[assembly: ExportRenderer(typeof(Entry), typeof(MaterialEntryRenderer), new[] { typeof(CustomVisual) })]
[assembly: ExportRenderer(typeof(ActivityIndicator), typeof(MaterialActivityIndicatorRenderer), new[] { typeof(CustomVisual) })]
[assembly: ExportRenderer(typeof(Frame), typeof(MaterialFrameRenderer), new[] {typeof(CustomVisual)})]
[assembly: ExportRenderer(typeof(Slider), typeof(MaterialSliderRenderer), new[] { typeof(CustomVisual) })]
[assembly: ExportRenderer(typeof(ProgressBar), typeof(MaterialProgressBarRenderer), new[] { typeof(CustomVisual) })]
[assembly: ExportRenderer(typeof(Stepper), typeof(MaterialStepperRenderer), new[] { typeof(CustomVisual) })]
[assembly: ExportRenderer(typeof(DatePicker), typeof(MaterialDatePickerRenderer), new[] { typeof(CustomVisual) })]
[assembly: ExportRenderer(typeof(TimePicker), typeof(MaterialTimePickerRenderer), new[] { typeof(CustomVisual) })]

Repeat the above for your Android Project. The only difference will be swapping out the using statement Xamarin.Forms.Material.iOS for Xamarin.Forms.Material.Android.

Done

Done

That's it! Your own Visual, heavily based on Material with the tweaks you want and almost no changes in your existing UI base. I've also updated my MaterialPickerRenderer to keep the title/hint, I won't go through that here but you can check it out on GitHub.

Next Steps

I look forward to a future where Visuals are plentiful, either via Nuget or built into Xamarin.Forms.  I plan to package this up so I can easily add it to any of my apps via Nuget.