From 8e64d69e8316d09fe7e7c9fc88f2255b7e5acbbd Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 8 Aug 2022 01:58:45 -0400 Subject: [PATCH 1/4] Fix control catalog crash --- samples/ControlCatalog/Pages/PointersPage.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/ControlCatalog/Pages/PointersPage.xaml.cs b/samples/ControlCatalog/Pages/PointersPage.xaml.cs index 6fc468e37f..e82fcb6226 100644 --- a/samples/ControlCatalog/Pages/PointersPage.xaml.cs +++ b/samples/ControlCatalog/Pages/PointersPage.xaml.cs @@ -59,7 +59,7 @@ Position: ??? ???"; e.Pointer.Capture(null); e.Handled = true; } - else + else if (e.Pointer.Captured is not null) { throw new InvalidOperationException("How?"); } From 37dab7a565b047ff330fb2534bc531d30b43e3ce Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 8 Aug 2022 02:18:44 -0400 Subject: [PATCH 2/4] Minor scroll gesture recognizer fixes --- .../Input/GestureRecognizers/ScrollGestureRecognizer.cs | 3 +-- src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs | 4 ++++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs index 889b7e3b82..0fb991eaac 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs @@ -66,8 +66,7 @@ namespace Avalonia.Input.GestureRecognizers public void PointerPressed(PointerPressedEventArgs e) { - if (e.Pointer.IsPrimary && - (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen)) + if (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen) { EndGesture(); _tracking = e.Pointer; diff --git a/src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs index 0e4e0ed3e2..b8f6f99ae8 100644 --- a/src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs +++ b/src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs @@ -137,9 +137,13 @@ namespace Avalonia.Input.Raw /// public Point Position { get; set; } + /// public float Twist { get; set; } + /// public float Pressure { get; set; } + /// public float XTilt { get; set; } + /// public float YTilt { get; set; } From 8716a5111acfd67d07585053d4564870d314f1b0 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 8 Aug 2022 03:00:51 -0400 Subject: [PATCH 3/4] Revamp android input handling to support mouse, pen and historical points --- .../Platform/SkiaPlatform/TopLevelImpl.cs | 17 +- .../Helpers/AndroidMotionEventsHelper.cs | 251 ++++++++++++++++++ .../Helpers/AndroidTouchEventsHelper.cs | 85 ------ 3 files changed, 264 insertions(+), 89 deletions(-) create mode 100644 src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidMotionEventsHelper.cs delete mode 100644 src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index dc74214170..17a0d6b63a 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; + using Android.Content; using Android.Graphics; using Android.Views; @@ -30,7 +31,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform private readonly IFramebufferPlatformSurface _framebuffer; private readonly AndroidKeyboardEventsHelper _keyboardHelper; - private readonly AndroidTouchEventsHelper _touchHelper; + private readonly AndroidMotionEventsHelper _pointerHelper; private readonly ITextInputMethodImpl _textInputMethod; private ViewImpl _view; @@ -39,8 +40,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform _view = new ViewImpl(avaloniaView.Context, this, placeOnTop); _textInputMethod = new AndroidInputMethod(_view); _keyboardHelper = new AndroidKeyboardEventsHelper(this); - _touchHelper = new AndroidTouchEventsHelper(this, () => InputRoot, - GetAvaloniaPointFromEvent); + _pointerHelper = new AndroidMotionEventsHelper(this); _gl = GlPlatformSurface.TryCreate(this); _framebuffer = new FramebufferManager(this); @@ -160,10 +160,19 @@ namespace Avalonia.Android.Platform.SkiaPlatform _tl.Draw(); } + protected override bool DispatchGenericPointerEvent(MotionEvent e) + { + bool callBase; + bool? result = _tl._pointerHelper.DispatchMotionEvent(e, out callBase); + bool baseResult = callBase ? base.DispatchGenericPointerEvent(e) : false; + + return result != null ? result.Value : baseResult; + } + public override bool DispatchTouchEvent(MotionEvent e) { bool callBase; - bool? result = _tl._touchHelper.DispatchTouchEvent(e, out callBase); + bool? result = _tl._pointerHelper.DispatchMotionEvent(e, out callBase); bool baseResult = callBase ? base.DispatchTouchEvent(e) : false; return result != null ? result.Value : baseResult; diff --git a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidMotionEventsHelper.cs b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidMotionEventsHelper.cs new file mode 100644 index 0000000000..ce385ebe34 --- /dev/null +++ b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidMotionEventsHelper.cs @@ -0,0 +1,251 @@ +using System; +using System.Collections.Generic; + +using Android.Views; + +using Avalonia.Android.Platform.SkiaPlatform; +using Avalonia.Collections.Pooled; +using Avalonia.Input; +using Avalonia.Input.Raw; + +#nullable enable + +namespace Avalonia.Android.Platform.Specific.Helpers +{ + internal class AndroidMotionEventsHelper : IDisposable + { + private static readonly PooledList s_intermediatePointsPooledList = new(ClearMode.Never); + private static readonly float s_radiansToDegree = (float)(180f * Math.PI); + private readonly TouchDevice _touchDevice; + private readonly MouseDevice _mouseDevice; + private readonly PenDevice _penDevice; + private readonly TopLevelImpl _view; + private bool _disposed; + + public AndroidMotionEventsHelper(TopLevelImpl view) + { + _touchDevice = new TouchDevice(); + _penDevice = new PenDevice(); + _mouseDevice = new MouseDevice(); + _view = view; + } + + public bool? DispatchMotionEvent(MotionEvent e, out bool callBase) + { + callBase = true; + if (_disposed) + { + return null; + } + + var eventTime = (ulong)DateTime.Now.Millisecond; + var inputRoot = _view.InputRoot; + var actionMasked = e.ActionMasked; + var modifiers = GetModifiers(e.MetaState, e.ButtonState); + + if (actionMasked == MotionEventActions.Move) + { + for (int index = 0; index < e.PointerCount; index++) + { + var toolType = e.GetToolType(index); + var device = GetDevice(toolType); + var eventType = toolType == MotionEventToolType.Finger ? RawPointerEventType.TouchUpdate : RawPointerEventType.Move; + var point = CreatePoint(e, index); + modifiers |= GetToolModifiers(toolType); + + // ButtonState reports only mouse buttons, but not touch or stylus pointer. + if (toolType != MotionEventToolType.Mouse) + { + modifiers |= RawInputModifiers.LeftMouseButton; + } + + var args = new RawTouchEventArgs(device, eventTime, inputRoot, eventType, point, modifiers, e.GetPointerId(index)) + { + IntermediatePoints = new Lazy?>(() => + { + var site = e.HistorySize; + s_intermediatePointsPooledList.Clear(); + s_intermediatePointsPooledList.Capacity = site; + + for (int pos = 0; pos < site; pos++) + { + s_intermediatePointsPooledList.Add(CreateHistoricalPoint(e, index, pos)); + } + + return s_intermediatePointsPooledList; + }) + }; + _view.Input(args); + } + } + else + { + var index = e.ActionIndex; + var toolType = e.GetToolType(index); + var device = GetDevice(toolType); + modifiers |= GetToolModifiers(toolType); + var point = CreatePoint(e, index); + + if (actionMasked == MotionEventActions.Scroll && toolType == MotionEventToolType.Mouse) + { + var delta = new Vector(e.GetAxisValue(Axis.Hscroll), e.GetAxisValue(Axis.Vscroll)); + var args = new RawMouseWheelEventArgs(device, eventTime, inputRoot, point.Position, delta, RawInputModifiers.None); + _view.Input(args); + } + else + { + var eventType = GetActionType(e, actionMasked, toolType); + if (eventType >= 0) + { + var args = new RawTouchEventArgs(device, eventTime, inputRoot, eventType, point, modifiers, e.GetPointerId(index)); + _view.Input(args); + } + } + } + + return true; + } + + private static RawInputModifiers GetModifiers(MetaKeyStates metaState, MotionEventButtonState buttonState) + { + var modifiers = RawInputModifiers.None; + if (metaState.HasAnyFlag(MetaKeyStates.ShiftOn)) + { + modifiers |= RawInputModifiers.Shift; + } + if (metaState.HasAnyFlag(MetaKeyStates.CtrlOn)) + { + modifiers |= RawInputModifiers.Control; + } + if (metaState.HasAnyFlag(MetaKeyStates.AltOn)) + { + modifiers |= RawInputModifiers.Alt; + } + if (metaState.HasAnyFlag(MetaKeyStates.MetaOn)) + { + modifiers |= RawInputModifiers.Meta; + } + if (buttonState.HasAnyFlag(MotionEventButtonState.Primary)) + { + modifiers |= RawInputModifiers.LeftMouseButton; + } + if (buttonState.HasAnyFlag(MotionEventButtonState.Secondary)) + { + modifiers |= RawInputModifiers.RightMouseButton; + } + if (buttonState.HasAnyFlag(MotionEventButtonState.Tertiary)) + { + modifiers |= RawInputModifiers.MiddleMouseButton; + } + if (buttonState.HasAnyFlag(MotionEventButtonState.Back)) + { + modifiers |= RawInputModifiers.XButton1MouseButton; + } + if (buttonState.HasAnyFlag(MotionEventButtonState.Forward)) + { + modifiers |= RawInputModifiers.XButton2MouseButton; + } + if (buttonState.HasAnyFlag(MotionEventButtonState.StylusPrimary)) + { + modifiers |= RawInputModifiers.PenBarrelButton; + } + return modifiers; + } + +#pragma warning disable CA1416 // Validate platform compatibility + private static RawPointerEventType GetActionType(MotionEvent e, MotionEventActions actionMasked, MotionEventToolType toolType) + { + var isTouch = toolType == MotionEventToolType.Finger; + var isMouse = toolType == MotionEventToolType.Mouse; + switch (actionMasked) + { + // DOWN + case MotionEventActions.Down when !isMouse: + case MotionEventActions.PointerDown when !isMouse: + return isTouch ? RawPointerEventType.TouchBegin : RawPointerEventType.LeftButtonDown; + case MotionEventActions.ButtonPress: + return e.ActionButton switch + { + MotionEventButtonState.Back => RawPointerEventType.XButton1Down, + MotionEventButtonState.Forward => RawPointerEventType.XButton2Down, + MotionEventButtonState.Primary => RawPointerEventType.LeftButtonDown, + MotionEventButtonState.Secondary => RawPointerEventType.RightButtonDown, + MotionEventButtonState.StylusPrimary => RawPointerEventType.LeftButtonDown, + MotionEventButtonState.StylusSecondary => RawPointerEventType.RightButtonDown, + MotionEventButtonState.Tertiary => RawPointerEventType.MiddleButtonDown, + _ => RawPointerEventType.LeftButtonDown + }; + // UP + case MotionEventActions.Up when !isMouse: + case MotionEventActions.PointerUp when !isMouse: + return isTouch ? RawPointerEventType.TouchEnd : RawPointerEventType.LeftButtonUp; + case MotionEventActions.ButtonRelease: + return e.ActionButton switch + { + MotionEventButtonState.Back => RawPointerEventType.XButton1Up, + MotionEventButtonState.Forward => RawPointerEventType.XButton2Up, + MotionEventButtonState.Primary => RawPointerEventType.LeftButtonUp, + MotionEventButtonState.Secondary => RawPointerEventType.RightButtonUp, + MotionEventButtonState.StylusPrimary => RawPointerEventType.LeftButtonUp, + MotionEventButtonState.StylusSecondary => RawPointerEventType.RightButtonUp, + MotionEventButtonState.Tertiary => RawPointerEventType.MiddleButtonUp, + _ => RawPointerEventType.LeftButtonUp + }; + // MOVE + case MotionEventActions.Outside: + case MotionEventActions.HoverMove: + case MotionEventActions.Move: + return isTouch ? RawPointerEventType.TouchUpdate : RawPointerEventType.Move; + // CANCEL + case MotionEventActions.Cancel: + return isTouch ? RawPointerEventType.TouchCancel : RawPointerEventType.LeaveWindow; + default: + return (RawPointerEventType)(-1); + } + } +#pragma warning restore CA1416 // Validate platform compatibility + + private IPointerDevice GetDevice(MotionEventToolType type) + { + return type switch + { + MotionEventToolType.Mouse => _mouseDevice, + MotionEventToolType.Stylus => _penDevice, + MotionEventToolType.Eraser => _penDevice, + MotionEventToolType.Finger => _touchDevice, + _ => _touchDevice + }; + } + + private RawPointerPoint CreatePoint(MotionEvent e, int index) + { + return new RawPointerPoint + { + Position = new Point(e.GetX(index), e.GetY(index)) / _view.RenderScaling, + Pressure = Math.Min(e.GetPressure(index), 1), // android pressure can depend on the device, can be mixed up with "GetSize", may be larger than 1.0f on some devices + Twist = e.GetOrientation(index) * s_radiansToDegree + }; + } + + private RawPointerPoint CreateHistoricalPoint(MotionEvent e, int index, int pos) + { + return new RawPointerPoint + { + Position = new Point(e.GetHistoricalX(index, pos), e.GetHistoricalY(index, pos)) / _view.RenderScaling, + Pressure = Math.Min(e.GetHistoricalPressure(index, pos), 1), + Twist = e.GetHistoricalOrientation(index, pos) * s_radiansToDegree + }; + } + + private static RawInputModifiers GetToolModifiers(MotionEventToolType toolType) + { + // Android "Eraser" indicates Inverted pen OR actual Eraser. So we have to go both here. + return toolType == MotionEventToolType.Eraser ? RawInputModifiers.PenInverted | RawInputModifiers.PenEraser : RawInputModifiers.None; + } + + public void Dispose() + { + _disposed = true; + } + } +} diff --git a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs deleted file mode 100644 index 6142598514..0000000000 --- a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using Android.Views; -using Avalonia.Input; -using Avalonia.Input.Raw; -using Avalonia.Platform; - -namespace Avalonia.Android.Platform.Specific.Helpers -{ - public class AndroidTouchEventsHelper : IDisposable where TView : ITopLevelImpl, IAndroidView - { - private TView _view; - public bool HandleEvents { get; set; } - - public AndroidTouchEventsHelper(TView view, Func getInputRoot, Func getPointfunc) - { - this._view = view; - HandleEvents = true; - _getPointFunc = getPointfunc; - _getInputRoot = getInputRoot; - } - - private TouchDevice _touchDevice = new TouchDevice(); - private Func _getPointFunc; - private Func _getInputRoot; - - public bool? DispatchTouchEvent(MotionEvent e, out bool callBase) - { - if (!HandleEvents) - { - callBase = true; - return null; - } - - var eventTime = DateTime.Now; - - //Basic touch support - var pointerEventType = e.Action switch - { - MotionEventActions.Down => RawPointerEventType.TouchBegin, - MotionEventActions.Up => RawPointerEventType.TouchEnd, - MotionEventActions.Cancel => RawPointerEventType.TouchCancel, - _ => RawPointerEventType.TouchUpdate - }; - - if (e.Action.HasFlag(MotionEventActions.PointerDown)) - { - pointerEventType = RawPointerEventType.TouchBegin; - } - - if (e.Action.HasFlag(MotionEventActions.PointerUp)) - { - pointerEventType = RawPointerEventType.TouchEnd; - } - - for (int i = 0; i < e.PointerCount; i++) - { - //if point is in view otherwise it's possible avalonia not to find the proper window to dispatch the event - var point = _getPointFunc(e, i); - - double x = _view.View.GetX(); - double y = _view.View.GetY(); - double r = x + _view.View.Width; - double b = y + _view.View.Height; - - if (x <= point.X && r >= point.X && y <= point.Y && b >= point.Y) - { - var inputRoot = _getInputRoot(); - - var mouseEvent = new RawTouchEventArgs(_touchDevice, (uint)eventTime.Ticks, inputRoot, - i == e.ActionIndex ? pointerEventType : RawPointerEventType.TouchUpdate, point, RawInputModifiers.None, e.GetPointerId(i)); - _view.Input(mouseEvent); - } - } - - callBase = true; - //if return false events for move and up are not received!!! - return e.Action != MotionEventActions.Up; - } - - public void Dispose() - { - HandleEvents = false; - } - } -} From 26e401a36cf3c14897c182a849b3cc79854041ea Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 9 Aug 2022 02:41:50 -0400 Subject: [PATCH 4/4] Revert ScrollGestureRecognizer changes for now --- .../Input/GestureRecognizers/ScrollGestureRecognizer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs index 0fb991eaac..889b7e3b82 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs @@ -66,7 +66,8 @@ namespace Avalonia.Input.GestureRecognizers public void PointerPressed(PointerPressedEventArgs e) { - if (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen) + if (e.Pointer.IsPrimary && + (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen)) { EndGesture(); _tracking = e.Pointer;