Некоторые элементы управления в приложении могут реагировать на жесты - например кнопки, 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-ов и авторами больше не поддерживаются. Поэтому приходится некоторые заменять.