TintImage – Xamarin Forms

A Xamarin Forms Image is View that holds an Image. Images are a crucial part of application navigation, usability, and branding. They can be shared across platforms with Xamarin.Forms, can be loaded specifically for each platform, or they can be downloaded for display. However, at present, it is not possible to change the foreground color of an image in Xamarin Forms. TintImage solves this problem to customize the foreground color or the tint color of an image in Xamarin Forms.

In short, a TintImage is an extended Image. It allows you to customize the foreground of an image with different colors. It is possible to change color even in the runtime.

TintImage for Xamarin.Forms

In this post, you will find answers for the below questions with step by step instructions.

  • How to create a TintImage in Xamarin.Forms?
  • How to use them in your Xamarin.Forms application pages?

Setting up a Xamarin Forms Project

Check my post “Get started with Xamarin in just 10 minutes” to set up a brand new Xamarin Forms project.

Let’s create TintImage

Create a new class called TintImage.cs extending from View. Create a new BindableProperty called TintColor in it.

public class TintImage : Image
{
    public static readonly BindableProperty TintColorProperty = BindableProperty.Create(nameof(TintColor), typeof(Color), typeof(TintImage), Color.Transparent);

    public Color TintColor
    {
        get { return (Color)GetValue(TintColorProperty); }
        set { SetValue(TintColorProperty, value); }
    }
}

Custom Renderer – Registration

To get the above created TintColor property working fine, you need to write custom renderers for the TintImage on all supported platforms.

Most importantly, add an ExportRenderer attribute to the custom renderer class to specify that it will be used to render the Xamarin.Forms control. This attribute is used to register the custom renderer with Xamarin.Forms. Add the below line of code above the namespace in all the custom renderer files.

[assembly: ExportRenderer(typeof(TintImage), typeof(TintImageRenderer))]

TintImageRenderer – Android

Right-click the Android project and create a new class called TintImageRenderer.cs. Override the OnElementChanged and OnElementPropertyChanged methods to update the native control’s TintColor whenever the TintImage.TintColorProperty get changed.

public class TintImageRenderer : ImageRenderer
{
    public TintImageRenderer(Context context) : base(context)
    {

    }

    protected override void OnElementChanged(ElementChangedEventArgs<Image> e)
    {
        base.OnElementChanged(e);
        if (e.NewElement != null && this.Control != null)
            this.UpdateTintColor();
    }

    protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        base.OnElementPropertyChanged(sender, e);
        if (e.PropertyName == "TintColor" || e.PropertyName == "Source" || e.PropertyName == "IsLoading")
            this.UpdateTintColor();
    }

    private void UpdateTintColor()
    {
        if (this.Control == null || this.Element == null)
            return;

        var tintImage = this.Element as TintImage;
        if (tintImage == null)
            return;

        if (tintImage.TintColor == Xamarin.Forms.Color.Transparent)
        {
            //Cancelling the applied tint.
            if (Control.ColorFilter != null)
                Control.ClearColorFilter();
        }
        else
        {
            //Applying tint color
            var colorFilter = new PorterDuffColorFilter(tintImage.TintColor.ToAndroid(), PorterDuff.Mode.SrcIn);
            Control.SetColorFilter(tintImage.TintColor.ToAndroid(), PorterDuff.Mode.SrcIn);
        }
    }
}

TintImageRenderer – iOS

Right-click the iOS project and create a new class called TintImageRenderer.cs. Override the OnElementChanged and OnElementPropertyChanged methods to update the native control’s TintColor whenever the TintImage.TintColorProperty get changed.

public class TintImageRenderer : ImageRenderer
{
    protected override void OnElementChanged(ElementChangedEventArgs<Image> e)
    {
        base.OnElementChanged(e);
        if (e.NewElement != null && this.Control != null)
            this.UpdateTintColor();
    }

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

        if (e.PropertyName == "TintColor" || e.PropertyName == "Source" || e.PropertyName == "IsLoading")
            this.UpdateTintColor();
    }

    private void UpdateTintColor()
    {
        if (Control?.Image == null || Element == null)
            return;

        var tintImage = Element as TintImage;
        if (tintImage == null)
            return;

        if (tintImage.TintColor == Color.Transparent)
        {
            //Cancelling the applied tint.
            Control.Image = Control.Image.ImageWithRenderingMode(UIImageRenderingMode.Automatic);
            Control.TintColor = null;
        }
        else
        {
            //Applying tint color
            Control.Image = Control.Image.ImageWithRenderingMode(UIImageRenderingMode.AlwaysTemplate);
            Control.TintColor = tintImage.TintColor.ToUIColor();
        }
    }
}

TintImageRenderer – UWP

Unlike, Android and iOS, UWP does not have direct support for customizing the tint color of the native image. Hence after a lot of research, I came across this post that gave an idea to achieve a tint effect in UWP using Compositor. I further explored more on the Composition Brushes to achieve the desired result in UWP.

Firstly, right-click the UWP project and choose the Manage Nuget Packages option. Install the CompositionProToolkit package from the NuGet.org.

Secondly, right-click the UWP project and create a new class called TintImageRenderer.cs. Override the OnElementChanged and OnElementPropertyChanged methods to update the native control’s TintColor whenever the TintImage.TintColorProperty get changed.

The idea is we have overlapped a visual surface exactly like the image and updated its color using the CompositionBrush. Override the ArrangeOverride method to refresh the native visual surface and the composition brush value.

public class TintImageRenderer : ImageRenderer
{
    private Compositor tintImageCompositor; 
    private CompositionEffectBrush tintCompositionEffectBrush;
    private CompositionSurfaceBrush tintCompositionSurfaceBrush;
    private ICompositionGenerator compositionGenerator;
    private IImageSurface imageSurface;
    private SpriteVisual spriteVisual;
    private TintImage tintImage;

    protected override void OnElementChanged(ElementChangedEventArgs<Image> e)
    {
        base.OnElementChanged(e);
        if (e.NewElement != null)
        {
            this.tintImage = e.NewElement as TintImage;
            this.GetCompositorAndCreateCompositionGenerator();
        }
    }

    protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        base.OnElementPropertyChanged(sender, e);
        if (this.tintImage == null)
            return;

        try
        {
            if (e.PropertyName == TintImage.TintColorProperty.PropertyName)
            {
                if (this.tintImage.TintColor == Color.Transparent)
                {
                    //Cancelling the applied tint.
                    this.tintCompositionEffectBrush = null;
                    this.UpdateSpriteVisualBrushAndElementChildVisual(this.tintCompositionSurfaceBrush);
                }
                else
                {
                    this.ConfigureFilePathAndCreateSpriteVisualAndTintCompositeEffectBrush();
                    //Applying tint color
                    UpdateTintColor(this.tintImage.TintColor.ToWindowsColor());
                    return;
                }
            }

            if (e.PropertyName == Image.SourceProperty.PropertyName)
                this.ConfigureFilePathAndCreateSpriteVisualAndTintCompositeEffectBrush();
        }
        catch (Exception ex)
        {

        }
    }

    protected override Native.Size ArrangeOverride(Native.Size finalSize)
    {
        var arrangeSize = base.ArrangeOverride(finalSize);
        if(arrangeSize.Height != 0 && arrangeSize.Width != 0)
        {
            this.ConfigureFilePathAndCreateSpriteVisualAndTintCompositeEffectBrush();
            if (this.spriteVisual != null && this.imageSurface != null)
            {
                //To refresh the native tint effect the SpriteVisual and ImageSurface are resized here
                this.spriteVisual.Size = new Vector2((float)Element.Width, (float)Element.Height);
                this.imageSurface.Resize(new Native.Size(Element.Width, Element.Height));
            }
        }
        return arrangeSize;
    }

    private void GetCompositorAndCreateCompositionGenerator()
    {
        if (this.tintImageCompositor != null && this.compositionGenerator != null)
            return;

        this.tintImageCompositor = ElementCompositionPreview.GetElementVisual(Control).Compositor;
        this.compositionGenerator = this.tintImageCompositor.CreateCompositionGenerator();
    }

    private void UpdateTintColor(Windows.UI.Color color)
    {
        if (this.tintCompositionEffectBrush == null)
            return;

        //Updating the tint color of the native view.
        this.tintCompositionEffectBrush.Properties.InsertColor("colorSource.Color", color);
    }

    private async void ConfigureFilePathAndCreateSpriteVisualAndTintCompositeEffectBrush()
    {
        //Skip when tint composition effect brush is already created when applying a tint color.
        if (this.spriteVisual != null && this.tintCompositionEffectBrush != null)
            return;

        var source = Element.Source;
        if (source == null)
            return;

        var fileSource = source as FileImageSource;
        if (fileSource == null)
            return;

        var filePath = Path.GetDirectoryName(fileSource.File);
        await CreateSpriteVisualAndTintCompositeEffectBrushAsync(new Uri($"ms-appx:///{filePath}"));
    }

    private void UpdateSpriteVisualBrushAndElementChildVisual(CompositionBrush compositionBrush)
    {
        this.spriteVisual.Brush = compositionBrush;
        ElementCompositionPreview.SetElementChildVisual(Control, this.spriteVisual);
    }

    private async Task CreateSpriteVisualAndTintCompositeEffectBrushAsync(Uri uri)
    {
        if (Control == null || Element == null || Element.Width < 0 || Element.Height < 0)
            return;

        if (spriteVisual == null)
        {
            this.spriteVisual = this.tintImageCompositor.CreateSpriteVisual();
            this.spriteVisual.Size = new Vector2((float)Element.Width, (float)Element.Height);
        }

        if (this.imageSurface == null)
            this.imageSurface = await this.compositionGenerator.CreateImageSurfaceAsync(uri, new Native.Size(Element.Width, Element.Height), ImageSurfaceOptions.DefaultOptimized);

        if (this.tintCompositionSurfaceBrush == null)
            this.tintCompositionSurfaceBrush = this.tintImageCompositor.CreateSurfaceBrush(this.imageSurface.Surface);

        if (this.tintImage.TintColor == Color.Transparent)
        {
            //Cancelling/Skipping the native tint effect.
            this.tintCompositionEffectBrush = null;
            this.UpdateSpriteVisualBrushAndElementChildVisual(this.tintCompositionSurfaceBrush);
            return;
        }
            
        #region Applying tint color

        //Creating Composite effect based on tint color to reflect on the native view. 
        IGraphicsEffect graphicsEffect = new CompositeEffect
        {
            Mode = CanvasComposite.DestinationIn,
            Sources =
                {
                    new ColorSourceEffect
                    {
                        Name = "colorSource",
                        Color = this.tintImage.TintColor.ToWindowsColor()
                    },
                    new CompositionEffectSourceParameter("mask")
                }
        };

        //Creating effect factory based on the created composite effect to apply tint effect.
        CompositionEffectFactory effectFactory = this.tintImageCompositor.CreateEffectFactory(graphicsEffect,
            new[] { "colorSource.Color" });
        this.tintCompositionEffectBrush = effectFactory.CreateBrush();
        this.tintCompositionEffectBrush.SetSourceParameter("mask", tintCompositionSurfaceBrush);

        this.UpdateTintColor(this.tintImage.TintColor.ToWindowsColor());
        this.UpdateSpriteVisualBrushAndElementChildVisual(this.tintCompositionEffectBrush);

        #endregion
    }
}

Working with Embedded Images

In UWP, the TintImage will work only when loading the images locally. In order to get this working when using embedded images, then you may have to copy the images in your .NET standard project to the UWP project as well and change the logic in the ConfigureFilePathAndCreateSpriteVisualAndTintCompositeEffectBrush method as below.

Certainly, you need to find the name of the image resource corresponding to the current image file rendered. There are two ways to do this. Either to introduce a property in the TintImage view and pass the image name to the UWP renderer. Otherwise, use reflection to get the image file name.

public class TintImage : Image
{
    ...

    public static readonly BindableProperty HintProperty = BindableProperty.Create(nameof(Hint), typeof(string), typeof(TintImage), string.Empty);

    public string Hint
    {
        get { return (string)GetValue(HintProperty); }
        set { SetValue(HintProperty, value); }
    }
}
private async void ConfigureFilePathAndCreateSpriteVisualAndTintCompositeEffectBrush()
{
    //Skip when tint composition effect brush is already created when applying a tint color.
    if (this.spriteVisual != null && this.tintCompositionEffectBrush != null)
        return;

    var source = Element.Source;
    if (source == null)
        return;

    var fileSource = source as FileImageSource;
    string filePath = string.Empty;
    if (fileSource == null)
    {
        filePath = tintImage?.Hint; //finding image name by passing from control.
        //filePath = this.GetImageName(); //finding image name using reflection
    }
    else
        filePath = Path.GetDirectoryName(fileSource.File);
    await CreateSpriteVisualAndTintCompositeEffectBrushAsync(new Uri($"ms-appx:///{filePath}"));
}

private string GetImageName()
{
    try
    {
        var streamSource = this.Element.Source as StreamImageSource;
        if (streamSource != null)
        {
            var streamTarget = streamSource.Stream?.Target;
            var streamValue = streamTarget?.GetType().GetField("stream")?.GetValue(streamTarget);
            var targetValue = streamValue?.GetType().GetProperty("Target")?.GetValue(streamValue);
            var imageName = targetValue?.GetType().GetField("resource")?.GetValue(targetValue);
            var resultArray = imageName?.ToString().Split(".");
            if (resultArray.Length >= 2)
                return resultArray[resultArray.Length - 2] + "." + resultArray[resultArray.Length - 1];
        }
    }
    catch { }
    return null;
}

Congratulations, you have successfully created the TintImage and got that working fine for Android, iOS, and UWP platforms.

Let’s create a sample and check the working of the TintImage.

Firstly, open the MainPage.xaml and write the below code. Ensure you have placed the HomeIcon.png image parallel to the project file in Android, iOS, and UWP projects.

<?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:d="http://xamarin.com/schemas/2014/forms/design"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:control="clr-namespace:TintImageDemo.Control"
             xmlns:viewModel="clr-namespace:TintImageDemo.ViewModel"
             mc:Ignorable="d"
             x:Class="TintImageDemo.MainPage">

    <ContentPage.BindingContext>
        <viewModel:TintImageViewModel />
    </ContentPage.BindingContext>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="100" />
            <RowDefinition Height="100" />
        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <control:TintImage x:Name="homeIcon" Grid.ColumnSpan="3" Aspect="AspectFit" Source="HomeIcon.png" TintColor="{Binding TintColor}" Hint="{Binding ImageName}" VerticalOptions="Center" HorizontalOptions="CenterAndExpand" HeightRequest="{Binding TintHeight}" WidthRequest="{Binding TintWidth}" />

        <Button Grid.Row="1" Grid.Column="0" Text="Default" Command="{Binding ApplyDefaultCommand}" HorizontalOptions="Center" VerticalOptions="Center"/>
        <Button Grid.Row="1" Grid.Column="1" Text="Black" Command="{Binding ApplyBlackCommand}" HorizontalOptions="Center" VerticalOptions="Center"/>
        <Button Grid.Row="1" Grid.Column="2" Text="Blue" Command="{Binding ApplyBlueCommand}" HorizontalOptions="Center" VerticalOptions="Center"/>
        <Button Grid.Row="2" Grid.ColumnSpan="3" Text="Change Size" Command="{Binding ChangeSizeCommand}" HorizontalOptions="Center" VerticalOptions="Center"/>
    </Grid>
</ContentPage>

Right-click the .NET standard project and add a new class called TintImageViewModel.cs and write the below code.

public class TintImageViewModel : INotifyPropertyChanged
{
    private Color _tintColor;
    private double _tintHeight;
    private double _tintWidth;

    public TintImageViewModel()
    {
        this.TintColor = this.GetDefaultTintColor();
        this.TintImageSource = ImageSource.FromResource("TintImageDemo.Image.HomeIcon.png", this.GetType().Assembly);
        this.ImageName = "HomeIcon.png";
        this.ApplyDefaultCommand = new Command(ExecuteDefaultCommand);
        this.ApplyBlackCommand = new Command(ExecuteBlackCommand);
        this.ApplyBlueCommand = new Command(ExecuteBlueCommand);
        this.ChangeSizeCommand = new Command(UpdateTintImageSize);
        this.TintHeight = 200;
        this.TintWidth = 200;
    }

    public Color TintColor
    {
        get { return this._tintColor; }
        set { this._tintColor = value; OnPropertyChanged(); }
    }

    public double TintHeight
    {
        get { return this._tintHeight; }
        set { this._tintHeight = value; OnPropertyChanged(); }
    }

    public double TintWidth
    {
        get { return this._tintWidth; }
        set { this._tintWidth = value; OnPropertyChanged(); }
    }

    public ImageSource TintImageSource { get; set; }
    public string ImageName { get; set; }
    public ICommand ApplyDefaultCommand { get; set; }
    public ICommand ApplyBlackCommand { get; set; }
    public ICommand ApplyBlueCommand { get; set; }
    public ICommand ChangeSizeCommand { get; set; }

    private Color GetDefaultTintColor()
    {
        return (Color)Control.TintImage.TintColorProperty.DefaultValue;
    }

    private void ExecuteDefaultCommand(object obj)
    {
        this.TintColor = this.GetDefaultTintColor();
    }

    private void ExecuteBlackCommand(object obj)
    {
        this.TintColor = Color.Black;
    }

    private void ExecuteBlueCommand(object obj)
    {
        this.TintColor = Color.Blue;
    }

    private void UpdateTintImageSize(object obj)
    {
        if (TintHeight == 200)
        {
            TintHeight = 300;
            TintWidth = 300;
        }
        else
        {
            TintHeight = 200;
            TintWidth = 200;
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    private void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        if (this.PropertyChanged != null)
            this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

Click the “Run” button to try it out. Below is a sample output of the demo across Android, iOS, and UWP.

TintImage for Xamarin.Forms

Source code

The complete code for the sample demonstrated in this article can be found in the GitHub repository here.

I hope now you have understood how to create a TintImage and how to use it in a Xamarin.Forms application.

Thanks for reading. Please share your comments and feedback. Happy Coding…!