Home > Mobile >  Example of how to create an image with clickable regions
Example of how to create an image with clickable regions

Time:01-10

This is not a question, but an article on how to do this.
All the details and focus will be in the answer to this question.

What is it about:
I was looking for a way to achieve is this

enter image description here

This is what I call a image with clickable regions, it seemed so simple but it took me some doing

What I found about this topic:
I found many questions about this on stack and other sites, but nothing about how it is actually done. Only bits and pieces scattered around.
I noticed SkiaSharp being mentioned a few times, so I searched on how to do this using that library, but again no actuall answer.
So I looked at their documentation and examples, what I needed was in there but it's hard to find if you don't know what methods and terminology to search for.
Also I had never worked with vector images before this, that was completely new for me, so I did not knew that was what I had to search on.

What I have came up with
So I took some time to figure it out, had to learn about vector images, about the skiasharp package, and lots of other stuff.

I managed to get it working, and I decided to write down here all the steps that are needed, so other people don't have to do that same search. It is all in my answer on this question.

The example in my answer is done with xamarin forms using c# and the SkiaSharp package

You can see a step by step approach in detail on how to do this, hopefully other people that are searching for this can use this as a starting point.

CodePudding user response:

I had to do a lot of searching, researching and experimenting on how to do this.
Maybe there are better ways of doing this, I don't know, but this method sure works and I want to share my findings here so other people can benefit from my struggles.

What I want to achieve is this

enter image description here

Looks not so bad hey, let's see how this is done

So, what do you need for this ?

You have to get the SkiaSharp package from nuget, this can be easily installed using the nuget manager, search for SkiaSharp.Views.Forms

Next you need an image that you can use as your base image, in this example the image of the car you can see in the gif above.

The XAML file

The xaml file is actually very simple, in the example above I need a label on top, a Skia Canvas in the middle, and 2 buttons with a label between them at the bottom

<StackLayout VerticalOptions="Start" HorizontalOptions="FillAndExpand" Orientation="Horizontal" Margin="1, 1">
    <Label Text="SELECT DAMAGE REGION" 
           VerticalOptions="StartAndExpand" HorizontalOptions="FillAndExpand" >
    </Label>
</StackLayout>

<skia:SKCanvasView 
    x:Name="sKCanvasViewCar"
    HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" 
    EnableTouchEvents="True" >
</skia:SKCanvasView>

<StackLayout HorizontalOptions="FillAndExpand" Orientation="Horizontal" VerticalOptions="End" Padding="5, 5">
    <Button x:Name="ButtonSelectFromCarBack" 
            WidthRequest="150" 
            HorizontalOptions="Start" 
            Text="Back" />
    <Label x:Name="labelRegionCar" 
           VerticalOptions="Center" 
           HorizontalOptions="CenterAndExpand" HorizontalTextAlignment="Center" />
    <Button x:Name="ButtonSelectFromCarSelect" IsEnabled="false" 
            WidthRequest="150" 
            HorizontalOptions="End" 
            Text="Next" />
</StackLayout>

To avoid the error The type 'skia:SKCanvasView' was not found have this in the definition of the form xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"

The code behind

In the code behind we need to do 3 things

  1. draw the base image on the screen
  2. determine that someone clicked on the image, and where on the image he clicked
  3. draw the clicked region with a different color (red in this example) over the base image

1. draw the base image on the screen

My base image is a file called draw_regions_car.png that I have in my project as embedded resource
To draw it on the Skia Canvas I need put it on a SKBitmap that I can use later in the event PaintSurface to draw it on the screen.

In my example this looks like this

namespace yourProject.Pages
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class PageDamageSelectFromCarImage : ContentPage
    {
        SKBitmap _bitmap;
        SKMatrix _matrix = SKMatrix.CreateIdentity();

        bool _firstTime = true;
        string _region = "";


        public PageDamageSelectFromCarImage()
        {
            InitializeComponent();

            sKCanvasViewCar.PaintSurface  = OnCanvasViewPaintSurface;
            sKCanvasViewCar.Touch  = SKCanvasView_Touch;
            ButtonSelectFromCarBack.Clicked  = ButtonSelectFromCarBack_Clicked;
            ButtonSelectFromCarSelect.Clicked  = ButtonSelectFromCarSelect_Clicked;

            // put the car_region image from the resources into a skia bitmap, so we can draw it later in the Surface event
            string resourceID = "yourProject.Resources.draw_regions_car.png";
            var assembly = Assembly.GetExecutingAssembly();
            using (Stream stream = assembly.GetManifestResourceStream(resourceID))
            {
                _bitmap = SKBitmap.Decode(stream);
            }
        }
}

and this is how the surface event looks like for now

private void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    SKImageInfo info = args.Info;
    SKSurface surface = args.Surface;
    SKCanvas canvas = surface.Canvas;

    if (_firstTime)
    {
        // scale the matrix so that we can transform our path variables, so they will have the same size as the stretched image in _bitmap
        _matrix.ScaleX = info.Width / (float)_bitmap.Width;
        _matrix.ScaleY = info.Height / (float)_bitmap.Height;
        _firstTime = false;
    }

    canvas.Clear();

    // draw the entire bitmap first, so we can draw regions (called paths) over it
    canvas.DrawBitmap(_bitmap, info.Rect);

    using (SKPaint paint = new SKPaint())
    {
        // here we will draw the selected regions over the base image
    }
}

If you would run the app now, it should display the base image on the screen.
From this we can get to the next steps


2. determine that someone clicked on the image, and where on the image he clicked

To do this, we need to use the Touch event of the SKCanvasView (see the ctor in this article)
In our example we called it SKCanvasView_Touch
There I determine what kind of touch has happened, and if it was a click then I call a private method called OnPressed

private void SKCanvasView_Touch(object sender, SKTouchEventArgs e)
{
    switch (e.ActionType)
    {
        case SKTouchAction.Pressed:
            OnPressed(sender, e.Location);
            break;
    }
}

So in the OnPressed method we can handle all the clicks that are done on the image.
For example, let's see how I can know if the user clicked on the front_door_left.
To do that, I need a path of this door.
What is this path??
Well, it is a vector drawing. A vector drawing (find in .svg files) is a series of commands that when executed results in the image.
for our front-door-left these commands could look like this

"M77.5 289.7c.3 3.2 1.9 37.3 3.6 75.8 1.7 38.5 3.2 70.1 3.4 70.3.1.1 5.6.7 12.1 1.1 6.6.5 28 2.1 47.7 3.7 19.6 1.5 36 2.5 36.3 2.2.3-.3-.6-22.6-2-49.4-1.5-26.9-2.6-49.7-2.6-50.7 0-1.4-.9-1.7-4.8-1.7-7.1 0-8.2-1.6-8.2-12.3v-8.5l-3.7-1.3c-5.3-1.6-6.3-2.7-6.3-6.3 0-6 .9-6.3 10.7-2.8 4.9 1.8 9.2 3.2 9.6 3.2.9 0 .9-17.2-.1-18.5-.4-.6-3.6-3.2-7.1-5.8l-6.5-4.7H77l.5 5.7zm90.8 124.2c.4 6.3.6 12 .3 12.7-.3.8-1.9 1.4-3.6 1.4h-3v-26.1l2.8.3 2.7.3.8 11.4z"

What they mean is not important for this example, what we need to do is create a variable of type SKPath that holds this value, and with that variable we can instruct Skia to do its magic for us.

SKPath _path_10;

_path_10 = SKPath.ParseSvgPathData("M77.5 289.7c.3 3.2 1.9 37.3 3.6 75.8 1.7 38.5 3.2 70.1 3.4 70.3.1.1 5.6.7 12.1 1.1 6.6.5 28 2.1 47.7 3.7 19.6 1.5 36 2.5 36.3 2.2.3-.3-.6-22.6-2-49.4-1.5-26.9-2.6-49.7-2.6-50.7 0-1.4-.9-1.7-4.8-1.7-7.1 0-8.2-1.6-8.2-12.3v-8.5l-3.7-1.3c-5.3-1.6-6.3-2.7-6.3-6.3 0-6 .9-6.3 10.7-2.8 4.9 1.8 9.2 3.2 9.6 3.2.9 0 .9-17.2-.1-18.5-.4-.6-3.6-3.2-7.1-5.8l-6.5-4.7H77l.5 5.7zm90.8 124.2c.4 6.3.6 12 .3 12.7-.3.8-1.9 1.4-3.6 1.4h-3v-26.1l2.8.3 2.7.3.8 11.4z");

I will explain later how you can extract this Path for every region from your base image, for now let's focus on how to use this path.

In the OnPressed method I can use this path to find out if the user clicked on this door or not, and if he did, then I will put code '10' in the private variabel _region
After that, I call InvalidateSurface to fire the Surface OnCanvasViewPaintSurface event again, where we do all our drawing

// I define this (and all other paths) on top of the class, so they are in the global scope for this class  
SKPath _path_10;


private async void OnPressed(object sender, SKPoint point)
{
    SKPoint location = point;

    _region = "";

    if (_path_10.Contains(location.X, location.Y))
    {
        _region = "10";
    }

    sKCanvasViewCar.InvalidateSurface();
}

3. draw the clicked region with a different color (red in this example) over the base image

Let's look at the event OnCanvasViewPaintSurface again with the extra code

private void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    SKImageInfo info = args.Info;
    SKSurface surface = args.Surface;
    SKCanvas canvas = surface.Canvas;

    if (_firstTime)
    {
        // scale the matrix so that we can transform our path variables, so they will have the same size as the stretched image in _bitmap
        _matrix.ScaleX = info.Width / (float)_bitmap.Width;
        _matrix.ScaleY = info.Height / (float)_bitmap.Height;
        _firstTime = false;
    }

    canvas.Clear();

    // draw the entire bitmap first, so we can draw regions (called paths) over it
    canvas.DrawBitmap(_bitmap, info.Rect);

    using (SKPaint paint = new SKPaint())
    {
        _path_10 = SKPath.ParseSvgPathData("M77.5 289.7c.3 3.2 1.9 37.3 3.6 75.8 1.7 38.5 3.2 70.1 3.4 70.3.1.1 5.6.7 12.1 1.1 6.6.5 28 2.1 47.7 3.7 19.6 1.5 36 2.5 36.3 2.2.3-.3-.6-22.6-2-49.4-1.5-26.9-2.6-49.7-2.6-50.7 0-1.4-.9-1.7-4.8-1.7-7.1 0-8.2-1.6-8.2-12.3v-8.5l-3.7-1.3c-5.3-1.6-6.3-2.7-6.3-6.3 0-6 .9-6.3 10.7-2.8 4.9 1.8 9.2 3.2 9.6 3.2.9 0 .9-17.2-.1-18.5-.4-.6-3.6-3.2-7.1-5.8l-6.5-4.7H77l.5 5.7zm90.8 124.2c.4 6.3.6 12 .3 12.7-.3.8-1.9 1.4-3.6 1.4h-3v-26.1l2.8.3 2.7.3.8 11.4z");
        _path_10.Transform(_matrix);
        if (_region == "10")
        {
            DrawRegion(canvas, paint, _path_10);
        }
    }
}

private void DrawRegion(SKCanvas canvas, SKPaint paint, SKPath path)
{
        paint.Style = SKPaintStyle.StrokeAndFill;
        paint.Color = (Xamarin.Forms.Color.Red).ToSKColor();
        paint.StrokeWidth = 1;
        canvas.DrawPath(path, paint);
}
    

This you need to repeat for every region in the base image you want to be clickable

private async void OnPressed(object sender, SKPoint point)
{
    SKPoint location = point;

    _region = "";

    if (_path_10.Contains(location.X, location.Y))
    {
        _region = "10";
    }
    else if (_path_11.Contains(location.X, location.Y))
    {
        _region = "11";
    }
    // and so on...

    sKCanvasViewCar.InvalidateSurface();
}


private void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    ...

    using (SKPaint paint = new SKPaint())
    {
        _path_10 = SKPath.ParseSvgPathData("M77.5 289.7c.3 3.2 1.9 37.3 3.6 75.8 1.7 38.5 3.2 70.1 3.4 70.3.1.1 5.6.7 12.1 1.1 6.6.5 28 2.1 47.7 3.7 19.6 1.5 36 2.5 36.3 2.2.3-.3-.6-22.6-2-49.4-1.5-26.9-2.6-49.7-2.6-50.7 0-1.4-.9-1.7-4.8-1.7-7.1 0-8.2-1.6-8.2-12.3v-8.5l-3.7-1.3c-5.3-1.6-6.3-2.7-6.3-6.3 0-6 .9-6.3 10.7-2.8 4.9 1.8 9.2 3.2 9.6 3.2.9 0 .9-17.2-.1-18.5-.4-.6-3.6-3.2-7.1-5.8l-6.5-4.7H77l.5 5.7zm90.8 124.2c.4 6.3.6 12 .3 12.7-.3.8-1.9 1.4-3.6 1.4h-3v-26.1l2.8.3 2.7.3.8 11.4z");
        _path_10.Transform(_matrix);
        if (_region == "10")
        {
            DrawRegion(canvas, paint, _path_10);
        }

        _path_11 = SKPath.ParseSvgPathData("M84.5 444.7c.3 3.2.9 19.7 1.5 36.8.6 17 1.3 34.1 1.6 38l.6 7 6.4 4c15.1 9.4 24.6 22.7 29.3 41.1l1.1 4.1 25.5.7 25.5.8 6-3.7 6-3.6-.6-7.2c-.3-4-1.8-31.6-3.3-61.5-1.5-29.8-3-54.4-3.2-54.6-.3-.4-91.7-7.6-95.6-7.6-.9 0-1.1 1.6-.8 5.7zm87.2 87.5c1.5 1.9 2.3 23.4 1 24.7-.6.6-2.1 1.1-3.3 1.1-2.2 0-2.3-.3-2.6-12.8-.2-7-.2-13 0-13.5.5-1.2 3.9-.8 4.9.5z");
        _path_11.Transform(_matrix);
        if (_region == "11")
        {
            DrawRegion(canvas, paint, _path_11);
        }

        // and so on...
    }

How to extract a path from my base image ?

My base image draw_regions_car.png is a simple png image, which is not a vector image and thus has no paths.
So here is how I extract the path for the front-door-left from this image.
First I open it in an application that is able to do complex selection, I use the free program paint.net for this.

In there I select Tools/Magic Wand and drop it on the door, so it becomes selected, then I click on ctrl-I to revert the selection and then click on delete.

enter image description here

Now save this file, I saved it as 10.png
Next step is to convert this to svg and for that I use the website https://svgco.de/

enter image description here

Now you have a .svg file you can open with any text editor, the content looks like this

<svg xmlns="http://www.w3.org/2000/svg" version="1.0" viewBox="0 0 688 833">
<path fill="#007CFF" d="M77.5 289.7c.3 3.2 1.9 37.3 3.6 75.8 1.7 38.5 3.2 70.1 3.4 70.3.1.1 5.6.7 12.1 1.1 6.6.5 28 2.1 47.7 3.7 19.6 1.5 36 2.5 36.3 2.2.3-.3-.6-22.6-2-49.4-1.5-26.9-2.6-49.7-2.6-50.7 0-1.4-.9-1.7-4.8-1.7-7.1 0-8.2-1.6-8.2-12.3v-8.5l-3.7-1.3c-5.3-1.6-6.3-2.7-6.3-6.3 0-6 .9-6.3 10.7-2.8 4.9 1.8 9.2 3.2 9.6 3.2.9 0 .9-17.2-.1-18.5-.4-.6-3.6-3.2-7.1-5.8l-6.5-4.7H77l.5 5.7zm90.8 124.2c.4 6.3.6 12 .3 12.7-.3.8-1.9 1.4-3.6 1.4h-3v-26.1l2.8.3 2.7.3.8 11.4z"/>
<path fill="#0000CB" d="M77.2 291.5c0 1.6.2 2.2.5 1.2.2-.9.2-2.3 0-3-.3-.6-.5.1-.5 1.8zm97 20.5c0 1.4.2 1.9.5 1.2.2-.6.2-1.8 0-2.5-.3-.6-.5-.1-.5 1.3zm-96 1.5c0 1.6.2 2.2.5 1.2.2-.9.2-2.3 0-3-.3-.6-.5.1-.5 1.8zm1 22c0 1.6.2 2.2.5 1.2.2-.9.2-2.3 0-3-.3-.6-.5.1-.5 1.8zm97 15.5c0 1.4.2 1.9.5 1.2.2-.6.2-1.8 0-2.5-.3-.6-.5-.1-.5 1.3zm-96 6.5c0 1.6.2 2.2.5 1.2.2-.9.2-2.3 0-3-.3-.6-.5.1-.5 1.8zm97 12.5c0 1.4.2 1.9.5 1.2.2-.6.2-1.8 0-2.5-.3-.6-.5-.1-.5 1.3zm-96 9.5c0 1.6.2 2.2.5 1.2.2-.9.2-2.3 0-3-.3-.6-.5.1-.5 1.8zm97 10.5c0 1.4.2 1.9.5 1.2.2-.6.2-1.8 0-2.5-.3-.6-.5-.1-.5 1.3zm-96 11.5c0 1.6.2 2.2.5 1.2.2-.9.2-2.3 0-3-.3-.6-.5.1-.5 1.8zm97 7.5c0 1.4.2 1.9.5 1.2.2-.6.2-1.8 0-2.5-.3-.6-.5-.1-.5 1.3zm-10.9 12.5c0 2.2.2 3 .4 1.7.2-1.2.2-3 0-4-.3-.9-.5.1-.4 2.3zm-85.1 2c0 1.6.2 2.2.5 1.2.2-.9.2-2.3 0-3-.3-.6-.5.1-.5 1.8zm79 1.5c0 1.4.2 1.9.5 1.2.2-.6.2-1.8 0-2.5-.3-.6-.5-.1-.5 1.3zm18 3.5c0 1.6.2 2.2.5 1.2.2-.9.2-2.3 0-3-.3-.6-.5.1-.5 1.8zm-92.4 8.2c.7.3 1.6.2 1.9-.1.4-.3-.2-.6-1.3-.5-1.1 0-1.4.3-.6.6zm26 2c.7.3 1.6.2 1.9-.1.4-.3-.2-.6-1.3-.5-1.1 0-1.4.3-.6.6zm40 3c.7.3 1.6.2 1.9-.1.4-.3-.2-.6-1.3-.5-1.1 0-1.4.3-.6.6zm13 1c.7.3 1.6.2 1.9-.1.4-.3-.2-.6-1.3-.5-1.1 0-1.4.3-.6.6z"/>
</svg>

And there you will find the path that you need in your code

Conclusion

Maybe there are better ways to do this, but at least this methods works.
It is not even so complicated once you realize how this works, it just takes some time to set it all up.
For all you people desperate looking on the web on how to do this, I hope you can all use this as a starting point.

  • Related