Friday, 6 November 2020

Display SVGs as TabbedPage tab icons in Xamarin.Forms

A common question that Xamarin.Forms users ask is “how the hell do I get my TabbedPage to display SVG-based tab icons?” A quick look at the TabbedPage doc reveals a couple of hints:

  • iOS – The TabbedRenderer has an overridable GetIcon method that can be used to load tab icons from a specified source. This override makes it possible to use SVG images as icons on a TabbedPage.
  • Android – The TabbedPageRenderer for Android AppCompat has an overridable SetTabIconImageSource method that can be used to load tab icons from a custom Drawable. This override makes it possible to use SVG images as icons on a TabbedPage.

After a few hours investigation I discovered that while the doc is accurate, the advice for Android is no longer the best approach. Therefore, the purpose of this blog post is to outline the simplest techniques for displaying SVGs as TabbedPage icons on iOS and Android, with Xamarin.Forms.

The sample this code comes from can be found on GitHub.

Implementation

iOS

Xcode 12 introduced SVG image support for iOS (specifically iOS 13 or greater). However, the Xamarin.Forms TabbedPage doesn’t currently support consuming them. Therefore, to display an SVG as a TabbedPage icon it’s necessary to use a third-party library. Enter FFImageLoading. For information about using the SVG support in FFImageLoading, including how to configure your iOS project to use this library, see SVG Support.

The process for displaying SVGs as TabbedPage icons on iOS is as follows:

  1. Add the Xamarin.FFImageLoading.Svg.Forms NuGet package to your iOS project.
  2. Call CachedImageRenderer.Init() in your AppDelegate class.
  3. Add your SVGs to the Resources folder of your iOS project, and set the build action for each SVG to BundleResource.
  4. Write a custom renderer for the TabbedPage, which overrides the GetIcon method to load an SVG file.

The following code shows a custom renderer for TabbedPage, which overrides the GetIcon method:

[assembly: ExportRenderer(typeof(MyTabbedPage), typeof(MyTabbedPageRenderer))]
namespace TabbedPageSVGIcons.iOS
{
    public class MyTabbedPageRenderer : TabbedRenderer
    {
        protected override async Task<Tuple<UIImage, UIImage>> GetIcon(Page page)
        {
            UIImage imageIcon;

            if (page.IconImageSource is FileImageSource fileImage && fileImage.File.Contains(".svg"))
            {
                // Load SVG from file
                UIImage uiImage = await ImageService.Instance.LoadFile(fileImage.File)
                    .WithCustomDataResolver(new SvgDataResolver(15, 15, true))
                    .AsUIImageAsync();
                imageIcon = uiImage.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal);
            }
            else
            {
                // Load Xamarin.Forms supported image
                IImageSourceHandler sourceHandler = null;
                if (page.IconImageSource is UriImageSource)
                    sourceHandler = new ImageLoaderSourceHandler();
                else if (page.IconImageSource is FileImageSource)
                    sourceHandler = new FileImageSourceHandler();
                else if (page.IconImageSource is StreamImageSource)
                    sourceHandler = new StreamImagesourceHandler();
                else if (page.IconImageSource is FontImageSource)
                    sourceHandler = new FontImageSourceHandler();

                UIImage uiImage = await sourceHandler.LoadImageAsync(page.IconImageSource);
                imageIcon = uiImage.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal);
            }

            return new Tuple<UIImage, UIImage>(imageIcon, null);
        }
    }
}

In this example, if the image filename contains .svg, the SVG is loaded by the FFImageLoading library, and returned as a UIImage for display. If the image isn’t an SVG, it’s loaded by Xamarin.Forms with the assistance of some in-built image handlers.

Note that FFImageLoading is actually rasterising the SVG to a UIImage, which raises the question why bother when you could just supply your own raster images? Well, exactly. The only benefit to the approach presented here is that SvgDataResolver type enables you to specify the size of the rasterised image. This enables you to develop logic that returns a specific sized image (based on the device the code is running on) from a single image file.

Android

It used to be necessary to write a custom renderer to display SVGs as TabbedPage icons on Android. However, this is no longer the case.

Android Studio includes a tool called Vector Asset Studio that enables you to import SVG files into your Android project as vector drawable resources. These vector resources can then automatically be displayed as TabbedPage icons, without any additional work. For more information about Vector Asset Studio, see Add multi-density vector graphics.

The process for displaying SVGs as TabbedPage icons on Android is as follows:

  1. Convert your SVGs to vector drawable resource XML files.
  2. Add your vector drawable resource XML files to the Resources/drawable folder of your Android project, and set the build action for each XML file to AndroidResource.

That’s it! There’s literally nothing else to consuming SVGs as TabbedPage icons on Android, via Xamarin.Forms. If you don’t want to install Android Studio to handle the conversion from SVG to vector files, you can use an online tool such as Android SVG to VectorDrawable. While this tool is deprecated, it still works (and there are other online tools if this one becomes unavailable).

Create a TabbedPage

The icon displayed for each tab on a TabbedPage is specified by the IconImageSource property of the page type that represents the tab in the TabbedPage.The following code example shows a TabbedPage containing three ContentPage objects:

<local:MyTabbedPage xmlns="http://xamarin.com/schemas/2014/forms"
                    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                    xmlns:local="clr-namespace:TabbedPageSVGIcons"
                    xmlns:android="clr-namespace:Xamarin.Forms.PlatformConfiguration.AndroidSpecific;assembly=Xamarin.Forms.Core"
                    x:Class="TabbedPageSVGIcons.MainPage"
                    android:TabbedPage.ToolbarPlacement="Bottom">
    <ContentPage Title="Tab 1"
                 IconImageSource="{OnPlatform iOS=apple.svg, Android=ic_apple}">
        <StackLayout Margin="20">
            <Label Text="Tab 1"
                   HorizontalOptions="Center"
                   VerticalOptions="CenterAndExpand" />
        </StackLayout>
    </ContentPage>
    <ContentPage Title="Tab 2"
                 IconImageSource="{OnPlatform iOS=android.svg, Android=ic_android}">
        <StackLayout Margin="20">
            <Label Text="Tab 2"
                   HorizontalOptions="Center"
                   VerticalOptions="CenterAndExpand" />
        </StackLayout>
    </ContentPage>
    <ContentPage Title="Tab 3"
                 IconImageSource="{OnPlatform iOS=windows.svg, Android=ic_windows}">
        <StackLayout Margin="20">
            <Label Text="Tab 3"
                   HorizontalOptions="Center"
                   VerticalOptions="CenterAndExpand" />
        </StackLayout>
    </ContentPage>
</local:MyTabbedPage>

In this example, each ContentPage object specifies an IconImageSource as a vector file (SVG for iOS, vector drawable resource on Android). The OnPlatform markup extension is used to specify the different resource filenames on iOS and Android.

On iOS, the custom renderer ensures that each SVG is drawn on the TabbedPage tabs:

On Android, the Android runtime ensures that each vector drawable resource is drawn on the TabbedPage tabs by Xamarin.Forms:

It’s also worth pointing out that the vector drawable resources are drawn regardless of whether the TabbedPage toolbar is displayed at the top or bottom of the screen:

Wrapping up

Displaying SVGs as TabbedPage icons on iOS and Android, using Xamarin.Forms, requires two different techniques:

  • On iOS, you write a custom renderer for the TabbedPage, and override the GetIcon method to convert an SVG file to a UIImage.
  • On Android, you convert your SVGs to vector drawable resources, and consume them from Xamarin.Forms as you would any other image resource.

Hopefully this has cleared up how to do this. I’ll get the TabbedPage doc updated to reflect this.

The sample this code comes from can be found on GitHub.

No comments:

Post a comment