I have a Xamarin.Forms app where I need to drag irregularly shaped controls (TwinTechForms SvgImageView) around, like this one:
I want it to only respond to touches on the black area and not on transparent (checkered) areas
I tried using MR.Gestures package. Hooking up to the Panning event lets me drag the image but it also starts dragging when I touch the transparent parts of it.
My setup looks like this:
<mr:ContentView x:Name="mrContentView" Panning="PanningEventHandler" Panned="PannedEventHandler" Background="transparent">
<ttf:SvgImageView x:Name="svgView" Background="transparent" SvgPath=.../>
</mr:ContentView>
and code-behind
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;
}
In this code-behind how should I check if I'm hitting a transparent point on the target object and when that happens how do I cancel the gesture so that another view under this one may respond to it? In the right side image touching the red inside the green O's hole should start dragging the red O
Update: SOLVED
The accepted answer's suggestion worked but was not straightforward.
I had to fork and modify both NGraphics (github fork) and TwinTechsFormsLib (TTFL, github fork)
In the NGraphics fork I added an XDocument+filter ctor to SvgReader so the same XDocument can be passed into different SvgImageView instances with a different parse filter, effectively splitting up the original SVG into multiple SvgImageView objects that can be moved independently without too much of a memory hit. I had to fix some brush inheritance for my SVGs to show as expected.
The TTFL fork exposes the XDocument+filter ctor and adds platform-specific GetPixelColor to the renderers.
Then in my Xamarin.Forms page I can load the original SVG file into multiple SvgImageView instances:
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;
}
Then I add all of the svgImageViews to a MR.Gesture <mr:Grid x:Name="movableHost">
and hook up Panning and Panned events to it.
SvgImageView dragSvgView = null; Point originalPosition = Point.Zero; movableView.Panning += (sender, pcp) => { // if we're not dragging anything - check the previously loaded SVG images // if they have a non-transparent pixel at the touch point if (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;
}
}