Некоторые элементы управления в приложении могут реагировать на жесты - например кнопки, CollectionView, выезжающее сбоку меню у FlyoutPage. Ещё есть распознаватели жестов (GestureRecognizers), которые можно добавить элементам управления, не умеющим реагировать на касания. Можно использовать эффекты (Effects) - Invoking Events from Effects.
Но иногда надо узнать о касании в любом месте экрана и координаты этого касания. Например, для рисования пальцем или для написания пользовательских элементов управления. На андроид и iOS есть способ это сделать при помощи DependencyService. Это странно, но часть такого полезного и можно сказать базового функционала надо писать самому или покупать (https://www.mrgestures.com/).
Если хотите покороче, то ссылка на код вверху статьи. А далее будет более длинный вариант с объяснениями.
На мульти-тач экранах одновременных касаний может быть несколько. В данном примере берётся только первое из них. Как обрабатывать несколько одновременно написано здесь: Multi-Touch Finger Tracking.
Итак, нам понадобятся:
В кроссплатформенном проекте:
интерфейс для DependencyService - IGlobalTouch.cs
TouchEventArgs.cs для передачи данных из андроид/iOS
В андроид-проекте:
GlobalTouch.cs
дополнить MainActivity.cs
В iOS-проекте:
GlobalTouch.cs
TouchCoordinatesRecognizer.cs
правки в AppDelegate.cs
Поэкспериментировав, добавил кроме самого определения координат нажатия еще несколько полезных для этой задачи методов:
получение координат того view на который нажали (GetPosition);
получение размера SafeArea для iOS (GetSafeAreaTop / GetSafeAreaBottom);
получение размера панели навигации (GetNavBarHeight).
Они все пригодятся для расчётов при нажатии.
Кроссплатформенный проект
Здесь нужен интерфейс IGlobalTouch для DependencyService и класс-наследник EventArgs с дополнительным свойством для передачи координат.
public interface IGlobalTouch { void Subscribe(EventHandler handler); void Unsubscribe(EventHandler handler); Point GetPosition(VisualElement element); double GetSafeAreaTop(); double GetSafeAreaBottom(); int GetNavBarHeight(); }
public class TouchEventArgs<T> : EventArgs { public T EventData { get; private set; } public TouchEventArgs(T EventData) { this.EventData = EventData; } }
Android
MainActivity.cs:
В классе MainActivity два изменения: EventHandler для события изменения координат и переопределить метод DispatchTouchEvent. Координаты здесь сразу делятся на DeviceDisplay.MainDisplayInfo.Density чтобы в кроссплатформенный проект приходили одинаковые с андроида и iOS.
public EventHandler globalTouchHandler;
public override bool DispatchTouchEvent(MotionEvent e) { var d = DeviceDisplay.MainDisplayInfo.Density; globalTouchHandler?.Invoke(null, new TouchEventArgs<Point>(new Point(e.GetX() / d, e.GetY() / d))); return base.DispatchTouchEvent(e); }
Также о нажатии на View в андроиде можно узнать, если создать свой View и переопределить метод OnTouchEvent или если подписаться на событие Touch этого View.
GlobalTouch.cs:
В реализации IGlobalTouch даём подписаться/отписаться на нажатие и вспомогательные методы. SafeArea это про айфон, поэтому здесь нули.
public class GlobalTouch : IGlobalTouch { public void Subscribe(EventHandler handler) => (Platform.CurrentActivity as MainActivity).globalTouchHandler += handler; public void Unsubscribe(EventHandler handler) => (Platform.CurrentActivity as MainActivity).globalTouchHandler -= handler; public Point GetPosition(VisualElement element) { var d = DeviceDisplay.MainDisplayInfo.Density; var view = Xamarin.Forms.Platform.Android.Platform.GetRenderer(element).View; return new Point(view.GetX() / d, view.GetY() / d); } public double GetSafeAreaBottom() => 0; public double GetSafeAreaTop() => 0; public int GetNavBarHeight() { var d = DeviceDisplay.MainDisplayInfo.Density; int statusBarHeight = -1; int resourceId = Platform.CurrentActivity.Resources.GetIdentifier("status_bar_height", "dimen", "android"); if (resourceId > 0) { statusBarHeight = Platform.CurrentActivity.Resources.GetDimensionPixelSize(resourceId); } return (int)(statusBarHeight / d); } }
iOS
На iOS можно узнать о нажатии на UIView если создать свой UIView и переопределить в нём методы TouchesBegan, TouchesMoved, TouchesEnded и TouchesCancelled. А также эти методы есть в UIGestureRecognizer, чем мы и воспользуемся, создав свой и добавив его к UIApplication.KeyWindow
TouchCoordinatesRecognizer.cs
public class TouchCoordinatesRecognizer : UIGestureRecognizer { public override void TouchesBegan(NSSet touches, UIEvent evt) { base.TouchesBegan(touches, evt); SendCoords(touches); } public override void TouchesMoved(NSSet touches, UIEvent evt) { base.TouchesMoved(touches, evt); SendCoords(touches); } public override void TouchesEnded(NSSet touches, UIEvent evt) { base.TouchesEnded(touches, evt); SendCoords(touches); } public override void TouchesCancelled(NSSet touches, UIEvent evt) { base.TouchesCancelled(touches, evt); SendCoords(touches); } void SendCoords(NSSet touches) { foreach (UITouch touch in touches.Cast<UITouch>()) { var window = UIApplication.SharedApplication.KeyWindow; var vc = window.RootViewController; AppDelegate.Current.globalTouchHandler?.Invoke(null, new TouchEventArgs<Point>( new Point( touch.LocationInView(vc.View).X, touch.LocationInView(vc.View).Y))); return; } } }
AppDelegate.cs
Затем в классе AppDelegate в методе FinishedLaunching добавить его к "главному" представлению UIApplication.KeyWindow:
public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate, IUIGestureRecognizerDelegate { public static AppDelegate Current { get; private set; } public EventHandler globalTouchHandler; public override bool FinishedLaunching(UIApplication app, NSDictionary options) { Current = this; global::Xamarin.Forms.Forms.Init(); LoadApplication(new App()); var res = base.FinishedLaunching(app, options); app.KeyWindow.AddGestureRecognizer(new TouchCoordinatesRecognizer()); return res; } }
Использовать можно так:
service = DependencyService.Get<IGlobalTouch>(); DependencyService.Get<IGlobalTouch>().Subscribe((sender, e) => { var touchPoint = (e as TouchEventArgs<Point>).EventData; var touchX = touchPoint.X; var touchY = touchPoint.Y; var viewPosition = service.GetPosition(this); var navBarHeight = service.GetNavBarHeight(); var viewX = viewPosition.X; var viewY = viewPosition.Y + navBarHeight; if (touchX >= viewX && touchX <= viewX + Width && touchY >= viewY && touchY <= viewY + Height) { item.TranslationX = touchPoint.X - screenWidth / 2; } });
В сочетании с TouchEffect из Xamarin.CommunityToolkit уже можно реализовать более сложное поведение чем доступно по умолчанию. В демке на гитхабе, например, заготовка для своего CarouselView.
Немного громоздко, да, согласен. Но столкнулся в очередной раз с тем что шаг влево - шаг вправо и стандартные контролы не позволяют сделать элементарных вещей - определить, что пользователь закончил взаимодействие или задать направление вращения CollectionView итп. Также сейчас актуален вопрос перехода на .net maui и обнаруживается, что многие удобные элементы управления были взяты из когда-то популярных nuget-ов и авторами больше не поддерживаются. Поэтому приходится некоторые заменять.
