在 Xamarin.Forms 中拖动不规则形状
Drag an irregular shape in Xamarin.Forms
我有一个 Xamarin.Forms 应用程序,我需要在其中拖动形状不规则的控件 (TwinTechForms SvgImageView),就像这个:
我希望它只响应黑色区域的触摸,而不响应透明(方格)区域的触摸
我尝试使用 MR.Gestures 包。连接到平移事件可以让我拖动图像,但当我触摸它的透明部分时它也会开始拖动。
我的设置如下所示:
<mr:ContentView x:Name="mrContentView" Panning="PanningEventHandler" Panned="PannedEventHandler" Background="transparent">
<ttf:SvgImageView x:Name="svgView" Background="transparent" SvgPath=.../>
</mr:ContentView>
和代码隐藏
private void PanningEventHandler(object sender, PanningEventParameters arg){
svgView.TranslateX = arg.IsCancelled ? 0: arg.TotalDistance.X;
svgView.TranslateY = arg.IsCancelled ? 0: arg.TotalDistance.Y;
}
private void PannedEventHandler(object sender, PanningEventParameters arg){
if (!arg.IsCancelled){
mrContentView.TranslateX = svgView.TranslateX;
mrContentView.TranslateY = svgView.TranslateY;
}
svgView.TranslateX = 0;
svgView.TranslateY = 0;
}
在这个代码隐藏中,我应该如何检查我是否击中了目标对象上的透明点,当发生这种情况时,我该如何取消手势,以便该视图下的另一个视图可以响应它?在右侧图像中,触摸绿色 O 孔内的红色应该开始拖动红色 O
更新:已解决
接受的答案的建议有效但并不直接。
我不得不分叉并修改两个 NGraphics (github fork) and TwinTechsFormsLib (TTFL, github fork)
在 NGraphics 分支中,我向 SvgReader 添加了一个 XDocument+filter ctor,因此可以将相同的 XDocument 传递到具有不同解析过滤器的不同 SvgImageView 实例,从而有效地将原始 SVG 拆分为多个可以独立移动的 SvgImageView 对象没有太多的内存命中。我必须修复我的 SVG 的一些画笔继承才能按预期显示。
TTFL 分支公开了 XDocument+filter ctor 并将特定于平台的 GetPixelColor 添加到渲染器。
然后在我的 Xamarin.Forms 页面中,我可以将原始 SVG 文件加载到多个 SvgImageView 实例中:
List<SvgImageView> LoadSvgImages(string resourceName, int widthRequest = 500, int heightRequest = 500)
{
var svgImageViews = new List<SvgImageView>();
var assembly = this.GetType().GetTypeInfo().Assembly;
Stream stream = assembly.GetManifestResourceStream(resourceName);
XDocument xdoc = XDocument.Load(stream);
// only groups that don't have other groups
List<XElement> leafGroups = xdoc.Descendants()
.Where(x => x.Name.LocalName == "g" && x.HasElements && !x.Elements().Any(dx => dx.Name.LocalName == "g"))
.ToList();
leafGroups.Insert(0, new XElement("nonGroups")); // this one will
foreach (XElement leafGroup in leafGroups)
{
var svgImage = new SvgImageView
{
HeightRequest = widthRequest,
WidthRequest = heightRequest,
HorizontalOptions = LayoutOptions.Start,
VerticalOptions = LayoutOptions.End,
StyleId = leafGroup.Attribute("id")?.Value, // for debugging
};
// this loads the original SVG as if only there's only one leaf group
// and its parent groups (to preserve transformations, brushes, opacity etc)
svgImage.LoadSvgFromXDocument(xdoc, (xe) =>
{
bool doRender = xe == leafGroup ||
xe.Ancestors().Contains(leafGroup) ||
xe.Descendants().Contains(leafGroup);
return doRender;
});
svgImageViews.Add(svgImage);
}
return svgImageViews;
}
然后我将所有 svgImageView 添加到 MR.Gesture <mr:Grid x:Name="movableHost">
并将 Panning 和 Panned 事件连接到它。
SvgImageView dragSvgView = null;
点originalPosition = Point.Zero;
movableView.Panning += (发件人, pcp) =>
{
// 如果我们没有拖动任何东西——检查之前加载的 SVG 图像
// 如果他们在触摸点有一个不透明的像素
如果(dragSvgView==null){
dragSvgView = svgImages.FirstOrDefault(si => {
var c = si.GetPixelColor(pcp.Touches[0].X - si.TranslationX, pcp.Touches[0].Y - si.TranslationY);
return c.A > 0.0001;
});
if (dragSvgView != null)
{
// save the original position of this item so we can put it back in case dragging was canceled
originalPosition = new Point (dragSvgView.TranslationX, dragSvgView.TranslationY);
}
}
// if we're dragging something - move it along
if (dragSvgView != null)
{
dragSvgView.TranslationX += pcp.DeltaDistance.X;
dragSvgView.TranslationY += pcp.DeltaDistance.Y;
}
}
MR.Gestures 和任何底层平台都不会检查视图中的触摸区域是否透明。监听触摸手势的元素总是矩形的。所以你必须自己做命中测试。
PanningEventParameters
包含一个 Point[] Touches
,其中包含所有触摸手指的坐标。使用这些坐标,您可以检查它们是否与 SVG 中的任何可见区域相匹配。
Hit-testing 从您的样本中提取甜甜圈很容易,但测试一般形状则不然(我认为这就是您想要的)。如果幸运的话,SvgImage 已经支持它了。如果没有,那么您可能会在 SVG Rendering Engine, Point-In-Polygon Algorithm — Determining Whether A Point Is Inside A Complex Polygon or 2D collision detection.
中找到如何完成此操作的原则
不幸的是,元素重叠有点问题。当我最初编写 MR.Gestures 时,我尝试使用 Handled
标志来实现它,但我无法让它在所有平台上工作。因为我认为保持一致比只在一个平台上工作更重要,所以我忽略了所有平台上的 Handled
,而是为所有重叠元素引发事件。 (我应该完全删除标志)
在您的具体情况下,我建议您对多个 SVG 使用这样的结构:
<mr:ContentView x:Name="mrContentView" Panning="PanningEventHandler" Panned="PannedEventHandler" Background="transparent">
<ttf:SvgImageView x:Name="svgView1" Background="transparent" SvgPath=.../>
<ttf:SvgImageView x:Name="svgView2" Background="transparent" SvgPath=.../>
<ttf:SvgImageView x:Name="svgView3" Background="transparent" SvgPath=.../>
</mr:ContentView>
在 PanningEventHandler
中,您可以检查 Touches
是否在任何 SVG 上,如果是,哪个在最上面。
如果您要执行多个 ContentView
,每个都只有一个 SVG,那么将为每个重叠的矩形元素调用 PanningEventHandler
,这不是您想要的。
我有一个 Xamarin.Forms 应用程序,我需要在其中拖动形状不规则的控件 (TwinTechForms SvgImageView),就像这个:
我希望它只响应黑色区域的触摸,而不响应透明(方格)区域的触摸
我尝试使用 MR.Gestures 包。连接到平移事件可以让我拖动图像,但当我触摸它的透明部分时它也会开始拖动。
我的设置如下所示:
<mr:ContentView x:Name="mrContentView" Panning="PanningEventHandler" Panned="PannedEventHandler" Background="transparent">
<ttf:SvgImageView x:Name="svgView" Background="transparent" SvgPath=.../>
</mr:ContentView>
和代码隐藏
private void PanningEventHandler(object sender, PanningEventParameters arg){
svgView.TranslateX = arg.IsCancelled ? 0: arg.TotalDistance.X;
svgView.TranslateY = arg.IsCancelled ? 0: arg.TotalDistance.Y;
}
private void PannedEventHandler(object sender, PanningEventParameters arg){
if (!arg.IsCancelled){
mrContentView.TranslateX = svgView.TranslateX;
mrContentView.TranslateY = svgView.TranslateY;
}
svgView.TranslateX = 0;
svgView.TranslateY = 0;
}
在这个代码隐藏中,我应该如何检查我是否击中了目标对象上的透明点,当发生这种情况时,我该如何取消手势,以便该视图下的另一个视图可以响应它?在右侧图像中,触摸绿色 O 孔内的红色应该开始拖动红色 O
更新:已解决
接受的答案的建议有效但并不直接。
我不得不分叉并修改两个 NGraphics (github fork) and TwinTechsFormsLib (TTFL, github fork)
在 NGraphics 分支中,我向 SvgReader 添加了一个 XDocument+filter ctor,因此可以将相同的 XDocument 传递到具有不同解析过滤器的不同 SvgImageView 实例,从而有效地将原始 SVG 拆分为多个可以独立移动的 SvgImageView 对象没有太多的内存命中。我必须修复我的 SVG 的一些画笔继承才能按预期显示。
TTFL 分支公开了 XDocument+filter ctor 并将特定于平台的 GetPixelColor 添加到渲染器。
然后在我的 Xamarin.Forms 页面中,我可以将原始 SVG 文件加载到多个 SvgImageView 实例中:
List<SvgImageView> LoadSvgImages(string resourceName, int widthRequest = 500, int heightRequest = 500)
{
var svgImageViews = new List<SvgImageView>();
var assembly = this.GetType().GetTypeInfo().Assembly;
Stream stream = assembly.GetManifestResourceStream(resourceName);
XDocument xdoc = XDocument.Load(stream);
// only groups that don't have other groups
List<XElement> leafGroups = xdoc.Descendants()
.Where(x => x.Name.LocalName == "g" && x.HasElements && !x.Elements().Any(dx => dx.Name.LocalName == "g"))
.ToList();
leafGroups.Insert(0, new XElement("nonGroups")); // this one will
foreach (XElement leafGroup in leafGroups)
{
var svgImage = new SvgImageView
{
HeightRequest = widthRequest,
WidthRequest = heightRequest,
HorizontalOptions = LayoutOptions.Start,
VerticalOptions = LayoutOptions.End,
StyleId = leafGroup.Attribute("id")?.Value, // for debugging
};
// this loads the original SVG as if only there's only one leaf group
// and its parent groups (to preserve transformations, brushes, opacity etc)
svgImage.LoadSvgFromXDocument(xdoc, (xe) =>
{
bool doRender = xe == leafGroup ||
xe.Ancestors().Contains(leafGroup) ||
xe.Descendants().Contains(leafGroup);
return doRender;
});
svgImageViews.Add(svgImage);
}
return svgImageViews;
}
然后我将所有 svgImageView 添加到 MR.Gesture <mr:Grid x:Name="movableHost">
并将 Panning 和 Panned 事件连接到它。
SvgImageView dragSvgView = null; 点originalPosition = Point.Zero; movableView.Panning += (发件人, pcp) => { // 如果我们没有拖动任何东西——检查之前加载的 SVG 图像 // 如果他们在触摸点有一个不透明的像素 如果(dragSvgView==null){ dragSvgView = svgImages.FirstOrDefault(si => { var c = si.GetPixelColor(pcp.Touches[0].X - si.TranslationX, pcp.Touches[0].Y - si.TranslationY); return c.A > 0.0001; });
if (dragSvgView != null)
{
// save the original position of this item so we can put it back in case dragging was canceled
originalPosition = new Point (dragSvgView.TranslationX, dragSvgView.TranslationY);
}
}
// if we're dragging something - move it along
if (dragSvgView != null)
{
dragSvgView.TranslationX += pcp.DeltaDistance.X;
dragSvgView.TranslationY += pcp.DeltaDistance.Y;
}
}
MR.Gestures 和任何底层平台都不会检查视图中的触摸区域是否透明。监听触摸手势的元素总是矩形的。所以你必须自己做命中测试。
PanningEventParameters
包含一个 Point[] Touches
,其中包含所有触摸手指的坐标。使用这些坐标,您可以检查它们是否与 SVG 中的任何可见区域相匹配。
Hit-testing 从您的样本中提取甜甜圈很容易,但测试一般形状则不然(我认为这就是您想要的)。如果幸运的话,SvgImage 已经支持它了。如果没有,那么您可能会在 SVG Rendering Engine, Point-In-Polygon Algorithm — Determining Whether A Point Is Inside A Complex Polygon or 2D collision detection.
中找到如何完成此操作的原则不幸的是,元素重叠有点问题。当我最初编写 MR.Gestures 时,我尝试使用 Handled
标志来实现它,但我无法让它在所有平台上工作。因为我认为保持一致比只在一个平台上工作更重要,所以我忽略了所有平台上的 Handled
,而是为所有重叠元素引发事件。 (我应该完全删除标志)
在您的具体情况下,我建议您对多个 SVG 使用这样的结构:
<mr:ContentView x:Name="mrContentView" Panning="PanningEventHandler" Panned="PannedEventHandler" Background="transparent">
<ttf:SvgImageView x:Name="svgView1" Background="transparent" SvgPath=.../>
<ttf:SvgImageView x:Name="svgView2" Background="transparent" SvgPath=.../>
<ttf:SvgImageView x:Name="svgView3" Background="transparent" SvgPath=.../>
</mr:ContentView>
在 PanningEventHandler
中,您可以检查 Touches
是否在任何 SVG 上,如果是,哪个在最上面。
如果您要执行多个 ContentView
,每个都只有一个 SVG,那么将为每个重叠的矩形元素调用 PanningEventHandler
,这不是您想要的。