DEV Community

Kamu
Kamu

Posted on

Creating custom ImageSource in Xamarin.Forms, and displaying an SVG image to default Image controll

Since Xamarin.Forms 2.3.5, it comes to be possible to create your own ImageSource.

This article demonstrates how to display an SVG image to default Image control with custom ImageSource.

Repository

https://github.com/muak/SvgImageSource

Nuget

https://www.nuget.org/packages/Xamarin.Forms.Svg/

Procedure of adding custom ImageSource

  1. On the shared project, create a class which derives from ImageSource class. (e.g. SomeImageSource).
  2. On each platform project, create SomeImageSourceHandler class which implements IImageSourcehandler. This needs in order to read an image actually.
  3. Implements LoadImageAsync method in SomeImageSourceHandler, and writes the process of reading an image here.
  4. Registers SomeImageSource and SomeImageSourceHandler types to Xamarin.Forms in order to enable SomeImageSource.

Define custom ImageSource

I explain for the example creating SvgImageSource for displaying an SVG image.

Make StreamImageSouce inherit because the procedure taking a stream of an SVG resource is the same as it.
And define some common BindableProperty's and some static methods for creating ImageSource here.

I copied from Xamarin.Forms source the part of getting assembly. This seems to get the assembly from a calling method.

public class SvgImageSource : StreamImageSource, ISvgImageSource
{
    public static BindableProperty WidthProperty =
        BindableProperty.Create(
            nameof(Width),
            typeof(double),
            typeof(SvgImageSource),
            default(double),
            defaultBindingMode: BindingMode.OneWay
        );

    public double Width {
        get { return (double)GetValue(WidthProperty); }
        set { SetValue(WidthProperty, value); }
    }

    public static BindableProperty HeightProperty =
        BindableProperty.Create(
            nameof(Height),
            typeof(double),
            typeof(SvgImageSource),
            default(double),
            defaultBindingMode: BindingMode.OneWay
        );

    public double Height {
        get { return (double)GetValue(HeightProperty); }
        set { SetValue(HeightProperty, value); }
    }

    // ... Omitted

    static Assembly AssemblyCache;

    public static void RegisterAssembly(Type typeHavingResource = null)
    {
        if (typeHavingResource == null)
        {
            MethodInfo callingAssemblyMethod = typeof(Assembly).GetTypeInfo().GetDeclaredMethod("GetCallingAssembly");
            if (callingAssemblyMethod != null)
            {
                AssemblyCache = (Assembly)callingAssemblyMethod.Invoke(null, new object[0]);
            }
        }
    }

    public static ImageSource FromSvg(string resource, double width, double height, Color color = default(Color))
    {
        if (AssemblyCache == null)
        {
            MethodInfo callingAssemblyMethod = typeof(Assembly).GetTypeInfo().GetDeclaredMethod("GetCallingAssembly");
            if (callingAssemblyMethod != null)
            {
                AssemblyCache = (Assembly)callingAssemblyMethod.Invoke(null, new object[0]);
            }
            else
            {
                return null;
            }
        }
        var source = (SvgImageSource)FromSvg(resource, color);

        source.Width = width;
        source.Height = height;

        return source;
    }

    public static ImageSource FromSvg(string resource, Color color = default(Color))
    {
        if (AssemblyCache == null)
        {
            MethodInfo callingAssemblyMethod = typeof(Assembly).GetTypeInfo().GetDeclaredMethod("GetCallingAssembly");
            if (callingAssemblyMethod != null)
            {
                AssemblyCache = (Assembly)callingAssemblyMethod.Invoke(null, new object[0]);
            }
            else
            {
                return null;
            }
        }

        var realResource = GetRealResource(resource);
        if (realResource == null)
        {
            return null;
        }

        Func<Stream> streamFunc = () => AssemblyCache.GetManifestResourceStream(realResource);

        return new SvgImageSource { Stream = token => Task.Run(streamFunc, token), Color = color };

    }

    static string GetRealResource(string resource)
    {
        return AssemblyCache.GetManifestResourceNames()
                            .FirstOrDefault(x => x.EndsWith(resource, StringComparison.CurrentCultureIgnoreCase));

    }
}

Implementing IImageSourceHandler for iOS

This is almost the same as the source Xamarin.Forms StreamImageSourceHandler.

I changed LoadImageAsync to getting it from SVG stream with Ngraphics.

The LoadImageAsync method is used to load a native image actually, and customized ImageSource can be run by writing own process here.

Although this is iOS example, it can be implemented the same way as this about Android too.
For more information, see Github source.

public class SvgImageSourceHandler : IImageSourceHandler
 {
     internal static float ScreenScale;

     public async Task<UIImage> LoadImageAsync(ImageSource imagesource, CancellationToken cancelationToken = default(CancellationToken), float scale = 1)
     {
         UIImage image = null;
         var svgsource = imagesource as SvgImageSource;
         if (svgsource?.Stream != null)
         {
             using (var streamImage = await ((IStreamImageSource)svgsource).GetStreamAsync(cancelationToken).ConfigureAwait(false))
             {
                 if (streamImage != null)
                     image = GetUIImage(streamImage, svgsource.Width, svgsource.Height, svgsource.Color, scale);
             }
         }

         if (image == null)
         {
             Log.Warning(nameof(SvgImageSourceHandler), "Could not load image: {0}", svgsource);
         }

         return image;
     }

     UIImage GetUIImage(Stream stream, double width, double height, Color color, float scale)
     {
         Graphic g = null;
         using (var sr = new StreamReader(stream))
         {
             g = Graphic.LoadSvg(sr);
         }

         var newSize = SvgUtility.CalcAspect(g.Size, width, height);

         if (width > 0 || height > 0)
         {
             g = SvgUtility.Resize(g, newSize);
         }

         if (scale <= 1)
         {
             scale = ScreenScale;
         }

         var canvas = Platforms.Current.CreateImageCanvas(newSize, scale);

         if (color != Xamarin.Forms.Color.Default)
         {
             var nColor = new NGraphics.Color(color.R, color.G, color.B, color.A);

             foreach (var element in g.Children)
             {
                 SvgUtility.ApplyColor(element, nColor);
                 element.Draw(canvas);
             }
         }
         else
         {
             g.Draw(canvas);
         }

         return canvas.GetImage().GetUIImage();
     }
 }

Registers custom ImageSource and ImageSourceHandler

Finally, register it with Xamarin.Forms that "Do with SvgImageSource with SvgImageSourceHandler".

This example is to register it when initializing this library.

Note that this method isn't shown in intellisense because it is specified EditorBrowsable attribute.

public static class SvgImage
{
    public static void Init()
    {
        Internals.Registrar.Registered.Register(typeof(SvgImageSource), typeof(SvgImageSourceHandler));
    }
}

By registering to Internals.Registrar.Registered, such a control as an Image calls the handler corresponded a type of the ImageSource, and calls LoadImageAsync method and get a native image. Thereby Xamarin.Forms.Image can display an SVG image without changing its code.

The result is that default Image control can be set an SVG image and displayed without changing itself.

For more information about SvgImageSource, see ReadMe.

Conclusion

Disclosing Xamarin.Forms.Internals is very nice. I was impressed with changing the behavior of Image control without extending it like this article.

Likewise, I think, it is nice if my own GestureRecognizer can be created too.

Top comments (0)