xamarin 如何创建带有可单击区域的图像的示例

gg0vcinb  于 2023-01-06  发布在  其他
关注(0)|答案(1)|浏览(204)

这不是一个问题,更多的是一篇关于如何做到这一点的文章。
我一直在寻找一种方法来实现这个

这就是我所说的带有可点击区域的图像,它看起来很简单,但我花了一些时间
我在Stack和其他网站上发现了很多问题,但没有任何关于它实际上是如何做的。只有零星的碎片散落在周围。
所以我花了一些时间才弄明白,不得不学习矢量图像,关于skiasharp包,还有很多其他的东西。
使用c#SkiaSharp软件包通过xamarin forms完成此操作
所有的细节是如何工作的是在我的答案,起初我写它作为一篇文章,但后来我被要求格式化它作为一个问题与答案,所以我这样做了。
在答案中,你可以看到一个循序渐进的方法,详细说明如何做到这一点,希望其他正在搜索这一点的人可以以此为起点。

fd3cxomn

fd3cxomn1#

我不得不做了大量的搜索,研究和实验如何做到这一点。
也许有更好的方法来做到这一点,我不知道,但这种方法肯定有效,我想在这里分享我的发现,让其他人可以受益于我的斗争。
我想要达到的是

看起来还不错嘿,让我们看看这是怎么做的

那么,你需要什么呢?

你必须从nuget获得skiaSharp包,这可以很容易地使用nuget管理器安装,搜索SkiaSharp.Views.Forms
接下来,您需要一个可以用作基础图像的图像,在本例中,您可以在上面的gif中看到汽车的图像。

XAML文件

xaml文件实际上非常简单,在上面的例子中,我需要一个标签在顶部,一个Skia画布在中间,两个按钮之间的标签在底部

<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>

为了避免错误The type 'skia:SKCanvasView' was not found,请在xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"的定义中包含此内容

背后的代码

在后面的代码中,我们需要做3件事
1.在屏幕上绘制基本图像
1.确定某人点击了图像,以及他点击了图像的什么地方
1.用不同的颜色(本例中为红色)在基本图像上绘制单击区域

    • 1.在屏幕上绘制基本图像**

我的基础映像是一个名为draw_regions_car.png的文件,我在项目中将其命名为embedded resource
要在Skia Canvas上绘制它,我需要将它放在SKBitmap上,以便稍后在PaintSurface事件中使用它在屏幕上绘制它。
在我的示例中,它看起来如下所示

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);
            }
        }
}

这是地表事件目前的样子

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
    }
}

如果您现在运行应用程序,它应该会在屏幕上显示基础图像。
从这里我们可以进入下一步

    • 2.确定有人点击了图像,以及他点击了图像的什么地方**

为此,我们需要使用SKCanvasView的Touch事件(参见本文中的ctor)
在我们的示例中,我们将其命名为SKCanvasView_Touch
在这里,我确定发生了什么类型的触摸,如果是click,则调用一个名为OnPressed的私有方法

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

因此,在OnPressed方法中,我们可以处理在图像上执行的所有clicks
例如,让我们看看如何知道用户是否单击了front_door_left。
为此,我需要此门的path
这是什么?
嗯,这是一个矢量绘图。矢量绘图(在. svg文件中查找)是一系列命令,当执行时会产生图像。
对于我们的左前门,这些命令可能如下所示

"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"

它们的含义并不重要,我们需要做的是创建一个类型为SKPath的变量来保存这个值,使用这个变量我们可以指示Skia为我们发挥它的魔力。

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");

我将在后面解释如何从基础图像中为每个区域提取此路径,现在让我们集中讨论如何使用此路径。
OnPressed方法中,我可以使用这个路径来确定用户是否单击了这个门,如果单击了,我将把代码'10'放在私有变量_region
之后,我调用InvalidateSurface以再次触发Surface OnCanvasViewPaintSurface事件,在该事件中我们进行所有的绘制

// 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.在基本图像上用不同的颜色(本例中为红色)绘制单击区域**

让我们使用额外的代码再次查看事件OnCanvasViewPaintSurface

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);
}

您需要对基础图像中的每个区域重复此操作,以使其成为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...
    }

如何从基础映像中提取路径?

我的基本图像draw_regions_car.png是一个简单的png图像,它不是矢量图像,因此没有路径。
这就是我如何从这张图片中提取出左前门的路径。
First I open it in an application that is able to do complex selection, I use the free program paint.net for this.
在那里我选择Tools/Magic Wand并将其放在门上,这样它就变成了选中状态,然后我单击ctrl-I以恢复选择,然后单击删除。

现在保存此文件,我将其保存为10.png
下一步是将其转换为svg,为此我使用网站https://svgco.de/

现在您有了一个可以用任何文本编辑器打开的. svg文件,其内容如下所示

<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>

在那里,您将找到代码中所需的路径

结论

也许有更好的方法来做到这一点,但至少这种方法是有效的。
它甚至不是那么复杂,一旦你意识到这是如何工作的,它只是需要一些时间来设置这一切。
对于你们这些绝望地在网上寻找如何做到这一点的人来说,我希望你们都能把这作为一个起点。

相关问题