You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SkiaSharp缩放变换后如何从触摸位置获取画布精确坐标?

SkiaSharp Xamarin 触摸坐标与绘制坐标不匹配问题解决

问题描述

我正在开发一个基于SkiaSharp的Xamarin项目,作为SkiaSharp新手,已经花了好几天排查,还是没解决这个问题:对画布做了缩放和变换操作后,触摸SKCanvasView时,触摸事件返回的坐标和画布上矩形的实际绘制坐标对不上,我需要获取绘制矩形的精确坐标。

核心代码片段:

public static void DrawLayout(SKImageInfo info, SKCanvas canvas, SKSvg svg, SetupViewModel vm)
{
    var layout = vm.SelectedReticleLayout;
    float yRatio;
    float xRatio;
    float widgetHeight = 75;
    float widgetWidth = 170;
    float availableWidth = 720;
    float availableHeight = 1280;

    var currentZoomScale = getScale();
    canvas.Translate(info.Width / 2f, info.Height / 2f);
    SKRect bounds = svg.ViewBox;

    xRatio = (info.Width / bounds.Width) + ((info.Width / bounds.Width) * currentZoomScale);
    yRatio = (info.Height / bounds.Height) + ((info.Height / bounds.Height) * currentZoomScale);
    float ratio = Math.Min(xRatio, yRatio);

    canvas.Scale(ratio);
    canvas.Translate(-bounds.MidX, -bounds.MidY);
    canvas.DrawPicture(svg.Picture, new SKPaint { Color = SKColors.White, Style = SKPaintStyle.Fill });

    // 设置大红色矩形的坐标与尺寸
    float imageCenter = canvas.LocalClipBounds.Width / 2;
    layout.RedBorderXOffSet = imageCenter - (imageCenter / 2.0f) + canvas.LocalClipBounds.Left;
    float redBorderYOffSet = (float)(svg.Picture.CullRect.Top + Math.Ceiling(.0654450261780105f * svg.Picture.CullRect.Bottom));
    layout.RedBorderYOffSet = (float)(canvas.LocalClipBounds.Top + Math.Ceiling(.0654450261780105f * canvas.LocalClipBounds.Bottom));
    layout.RedBorderWidth = canvas.LocalClipBounds.Width / 2.0f;
    layout.RedBorderWidthXOffSet = layout.RedBorderWidth + layout.RedBorderXOffSet;
    layout.RedBorderHeight = (float)(canvas.LocalClipBounds.Bottom - Math.Ceiling(.0654450261780105f * canvas.LocalClipBounds.Bottom * 2)) - canvas.LocalClipBounds.Top;
    layout.RedBorderHeightYOffSet = layout.RedBorderYOffSet + layout.RedBorderHeight;

    // 绘制大红色矩形
    canvas.DrawRect(layout.RedBorderXOffSet, layout.RedBorderYOffSet, layout.RedBorderWidth, layout.RedBorderHeight, RedBorderPaint);

    // 清空已追踪的小部件,每次绘制时更新
    layout.TrackedWidgets.Clear();

    var widget = new widget
    {
        X = layout.RedBorderXOffSet + 5,
        Y = layout.RedBorderYOffSet + layout.TrackedReticleWidgets[0].Height + 15,
        Height = layout.RedBorderHeight * (widgetHeight / availableHeight),
        Width = layout.RedBorderWidth * (widgetWidth / availableWidth) // 修正原拼写错误RedBorderWdith
    };
    layout.TrackedWidgets.Add(widget);

    // 定义小部件的绘制样式
    public static SKPaint SelectedWidgetColor => new SKPaint { Color = SKColors.LightPink, Style = SKPaintStyle.StrokeAndFill, StrokeWidth = 3 };
    public static SKPaint EmptyWidgetBorder => new SKPaint { Color = SKColors.DarkGray, Style = SKPaintStyle.Stroke, StrokeWidth = 3 };
    public static SKPaint EmptyWidgetText => new SKPaint { Color = SKColors.Black, TextSize = 10, FakeBoldText = false, Style = SKPaintStyle.Stroke, Typeface = SKTypeface.FromFamilyName("Arial") };
    public static SKPaint DefinedWidgetText => new SKPaint { Color = SKColors.DarkRed, FakeBoldText = false, Style = SKPaintStyle.Stroke };

    // 绘制小部件矩形
    var widgetRectangle = SKRect.Create(widget.X, widget.Y, widget.Width, widget.Height);
    canvas.DrawRect(widgetRectangle, widget.IsSelected ? SelectedWidgetColor : EmptyWidgetBorder);

    // 绘制小部件文本
    string text = EnumUtility.GetDescription(widget.WidgetDataType);
    float textWidth = EmptyWidgetText.MeasureText(text);
    EmptyWidgetText.TextSize = widget.Width * GetUnscaledWidgetWith(widget) * EmptyWidgetText.TextSize / textWidth;

    SKRect textBounds = new SKRect();
    EmptyWidgetText.MeasureText(text, ref textBounds);

    float xText = widgetRectangle.MidX - textBounds.MidX;
    float yText = widgetRectangle.MidY - textBounds.MidY;

    canvas.DrawText(text, xText, yText, EmptyWidgetText);
}

解决方案

1. 逆矩阵转换触摸坐标

触摸事件返回的是屏幕坐标系坐标,而绘制时用的是经过Translate、Scale变换后的画布局部坐标系。要匹配两者,需要用画布变换矩阵的逆矩阵将触摸点转换到画布局部坐标系:

private void OnCanvasViewTouch(object sender, SKTouchEventArgs e)
{
    if (e.ActionType == SKTouchAction.Pressed)
    {
        SKCanvasView canvasView = (SKCanvasView)sender;
        // 获取画布的总变换矩阵
        SKMatrix canvasMatrix = canvasView.Canvas.TotalMatrix;
        SKMatrix inverseMatrix;
        
        // 计算逆矩阵(缩放/平移矩阵均可逆)
        if (SKMatrix.TryInvert(canvasMatrix, out inverseMatrix))
        {
            // 将屏幕触摸点转换为画布局部坐标
            SKPoint transformedPoint = SKPoint.Transform(e.Location, inverseMatrix);
            // 用转换后的坐标判断是否命中矩形
            CheckTouchHit(transformedPoint, vm.SelectedReticleLayout);
        }
    }
    e.Handled = true;
}

// 示例:检查触摸点是否命中小部件
private void CheckTouchHit(SKPoint point, ReticleLayout layout)
{
    foreach (var widget in layout.TrackedWidgets)
    {
        SKRect widgetRect = SKRect.Create(widget.X, widget.Y, widget.Width, widget.Height);
        if (widgetRect.Contains(point))
        {
            // 命中该小部件,执行逻辑
            widget.IsSelected = !widget.IsSelected;
            ((SKCanvasView)FindByName("YourCanvasView")).InvalidateSurface();
            break;
        }
    }
}

2. 修正坐标计算逻辑

避免依赖canvas.LocalClipBounds——变换后的裁剪边界会跟随画布缩放平移,直接基于SVG的原始ViewBox计算坐标更可靠:

// 替换原红色矩形计算逻辑
SKRect bounds = svg.ViewBox;
float svgWidth = bounds.Width;
float svgHeight = bounds.Height;

// 红色矩形宽度为SVG宽度的一半,水平居中
layout.RedBorderWidth = svgWidth / 2f;
layout.RedBorderXOffSet = (svgWidth - layout.RedBorderWidth) / 2f;

// 红色矩形上下留空比例0.0654,计算Y坐标和高度
float topBottomMargin = (float)Math.Ceiling(0.0654450261780105f * svgHeight);
layout.RedBorderHeight = svgHeight - topBottomMargin * 2;
layout.RedBorderYOffSet = topBottomMargin;

// 小部件坐标基于红色矩形的画布局部坐标计算
var widget = new widget
{
    X = layout.RedBorderXOffSet + 5,
    Y = layout.RedBorderYOffSet + layout.TrackedReticleWidgets[0].Height + 15,
    Height = layout.RedBorderHeight * (widgetHeight / availableHeight),
    Width = layout.RedBorderWidth * (widgetWidth / availableWidth)
};

3. 简化缩放比例计算

原比例计算可以简化,避免重复运算:

float baseXRatio = info.Width / bounds.Width;
float baseYRatio = info.Height / bounds.Height;
// 缩放比例 = 基础适配比例 * (1 + 缩放系数)
xRatio = baseXRatio * (1 + currentZoomScale);
yRatio = baseYRatio * (1 + currentZoomScale);
float ratio = Math.Min(xRatio, yRatio);

4. 修正代码拼写错误

原代码中layout.RedBorderWdith是拼写错误,应改为layout.RedBorderWidth,否则会导致小部件宽度计算错误。

内容的提问来源于stack exchange,提问作者SRI

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 09:10:32