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
相关产品推荐
相关产品推荐

