Reputation: 1737
I want to get the coordinates (rectangle bounds: x, y, width and height) of the selected item in the listview relative to the screen (assume the listview fills the whole screen), so that I can create an object at that location and animate it to display some details of the selected item in my Xamarin.Forms app.
listview in xaml:
<ListView ItemTapped="ItemTapped"
AbsoluteLayout.LayoutFlags="All"
AbsoluteLayout.LayoutBounds="0.5, 0.5, 1.0, 1.0">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell Height="50">
<AbsoluteLayout>
<Label Text="{Binding Info}"
AbsoluteLayout.LayoutFlags="All"
AbsoluteLayout.LayoutBounds="0.1, 0.5, 0.7, 0.5"/>
</AbsoluteLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
c# code for ItemTapped event:
void ItemTapped(object sender, EventArgs args)
{
var listView = (ListView)sender; // the listview
var selectedItem = args.Item; // the selected item
// need to get selected item coordinates for the animation
var selectedItemBounds = ...
...
}
Eventually I want to create somehting like this in Xamarin.Forms with a listview (the number of objects in the listview varies):
Upvotes: 11
Views: 6108
Reputation: 169
I have created an ILocationFetcher
implementation of the GetCoordinates
method.
In my case, I needed to get the bounds of the element relative to the parent element (the child element is inside the parent element). If no parent element is specified, the method returns the bounds of the element relative to the window. Using the inPixels
parameter, you can request the dimensions in pixels (may be required if you are drawing something using SkiaSharp).
Dependency interface:
public interface ILocationFetcher
{
RectangleF GetCoordinates(Xamarin.Forms.VisualElement element, Xamarin.Forms.VisualElement parentElement, bool inPixels = false);
}
Android Implementation:
[assembly:Dependency(typeof(LocationFetcher))]
namespace App.Droid
{
public class LocationFetcher : ILocationFetcher
{
public RectangleF GetCoordinates(Xamarin.Forms.VisualElement element, Xamarin.Forms.VisualElement parentElement, bool inPixels = false)
{
IVisualElementRenderer renderer = Platform.GetRenderer(element);
Android.Views.View nativeView = renderer.View;
float density = inPixels ? 1 : nativeView.Context.Resources.DisplayMetrics.Density;
ViewGroup parentNativeView = null;
if (parentElement != null)
{
parentNativeView = Platform.GetRenderer(parentElement).View as ViewGroup;
}
float x;
float y;
float width;
float height;
if (parentNativeView == null)
{
var windowBounds = new Android.Graphics.Rect();
nativeView.GetWindowVisibleDisplayFrame(windowBounds);
int statusBarHeight = windowBounds.Top;
var elementLocation = new int[2];
nativeView.GetLocationInWindow(elementLocation);
x = elementLocation[0] / density;
y = (elementLocation[1] - statusBarHeight) / density;
width = nativeView.Width / density;
height = nativeView.Height / density;
}
else
{
var elementBounds = new Android.Graphics.Rect();
nativeView.GetDrawingRect(elementBounds);
parentNativeView.OffsetDescendantRectToMyCoords(nativeView, elementBounds);
x = elementBounds.Left / density;
y = elementBounds.Top / density;
width = elementBounds.Width() / density;
height = elementBounds.Height() / density;
}
return new RectangleF(x, y, width, height);
}
}
}
iOS Implementation:
[assembly: Dependency(typeof(LocationFetcher))]
namespace App.iOS
{
public class LocationFetcher : ILocationFetcher
{
public RectangleF GetCoordinates(VisualElement element, VisualElement parentElement, bool inPixels = false)
{
IVisualElementRenderer renderer = Platform.GetRenderer(element);
UIView elementNativeView = renderer.NativeView;
nfloat scale = inPixels ? UIScreen.MainScreen.NativeScale : 1;
UIView parentNativeView = null;
if (parentElement != null)
parentNativeView = Platform.GetRenderer(parentElement).NativeView;
CGRect rect = elementNativeView.ConvertRectToView(elementNativeView.Frame, parentNativeView);
float x = (float)Math.Round(rect.X * scale);
float y = (float)Math.Round(rect.Y * scale);
float width = (float)Math.Round(rect.Width * scale);
float height = (float)Math.Round(rect.Height * scale);
return new RectangleF(x, y, width, height);
}
}
}
Upvotes: 1
Reputation: 1882
I've created a dependency you can use to get the absolute position of a VisualElement in iOS and Android. I use it for a similar purpose. We use it to determine the position of a popup to show when tapping in the listview. Works perfectly:
Dependency:
public interface ILocationFetcher
{
System.Drawing.PointF GetCoordinates(global::Xamarin.Forms.VisualElement view);
}
iOS Implementation:
class LocationFetcher : ILocationFetcher
{
public System.Drawing.PointF GetCoordinates(global::Xamarin.Forms.VisualElement element)
{
var renderer = Platform.GetRenderer(element);
var nativeView = renderer.NativeView;
var rect = nativeView.Superview.ConvertPointToView(nativeView.Frame.Location, null);
return new System.Drawing.PointF((int)Math.Round(rect.X), (int)Math.Round(rect.Y));
}
}
Android Implementation:
class LocationFetcher : ILocationFetcher
{
public System.Drawing.PointF GetCoordinates(global::Xamarin.Forms.VisualElement element)
{
var renderer = Platform.GetRenderer(element);
var nativeView = renderer.View;
var location = new int[2];
var density = nativeView.Context.Resources.DisplayMetrics.Density;
nativeView.GetLocationOnScreen(location);
return new System.Drawing.PointF(location[0] / density, location[1] / density);
}
}
Thanks to @Emil we also have an UWP implementation:
public System.Drawing.PointF GetCoordinates(global::Xamarin.Forms.VisualElement element)
{
var renderer = Xamarin.Forms.Platform.UWP.Platform.GetRenderer(element);
var nativeView = renderer.GetNativeElement();
var element_Visual_Relative = nativeView.TransformToVisual(Window.Current.Content);
Point point = element_Visual_Relative.TransformPoint(new Point(0, 0));
return new System.Drawing.PointF((int)Math.Round(point.X), (int)Math.Round(point.Y));
}
usage example:
var locationFetcher = DependencyService.Get<ILocationFetcher>();
var location = locationFetcher.GetCoordinates(myVisualElement);
Be sure to properly register the dependency correctly (see https://learn.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/dependency-service/ ) in the android and ios using the dependency attribute. Otherwise the DependencyService.Get will return null.
Upvotes: 15
Reputation: 6921
PaulVrugt's answer perfectly works for IOS and Android. Just to extend his implementation for anyone requires also UWP.
public System.Drawing.PointF GetCoordinates(global::Xamarin.Forms.VisualElement element)
{
var renderer = Xamarin.Forms.Platform.UWP.Platform.GetRenderer(element);
var nativeView = renderer.GetNativeElement();
var element_Visual_Relative = nativeView.TransformToVisual(Window.Current.Content);
Point point = element_Visual_Relative.TransformPoint(new Point(0, 0));
return new System.Drawing.PointF((int)Math.Round(point.X), (int)Math.Round(point.Y));
}
Upvotes: 2
Reputation: 3143
A quick idea:
Have a view helper service which runs as a singleton:
interface IViewHelper
{
Rect GetScreenCoordinates(View view);
}
Have a tap gesture added on the AbsoluteLayout you have in the ListView item template, and in the tap event handler call:
IViewHelper viewHelper = CrossViewHelper.Instance;
Rect rcItem = viewHelper.GetScreenCoordinates((View)sender);
The native implementation of the service GetScreenCoordinates
does two things:
// Get the native view from the Xamarin Forms view
var nativeView = Platform.GetRenderer(view);
// Call native functions to get screen coordinates
Android: nativeView.GetLocationOnScreen(coords)
iOS: Use nativeView.ConvertPoint(coords, null)
See https://forums.xamarin.com/discussion/86941/get-x-y-co-ordinates-in-tap-gesture-inside-listview
Also see: https://michaelridland.com/xamarin/creating-native-view-xamarin-forms-viewpage/
I don't have a complete code, but I hope this can help you.
I think in the end, after you implement animating and showing the view, you can refactor the implementation into a nice Behavior you can attach to any view, it won't matter if it's inside list-view or not, it will just work.
Upvotes: 1