Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions src/Controls/src/Core/Platform/Android/PointerGestureHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@
using System;
using Android.Views;
using Microsoft.Maui.Graphics;
using System.Runtime.Versioning;
using AView = Android.Views.View;

namespace Microsoft.Maui.Controls.Platform
{
internal class PointerGestureHandler : Java.Lang.Object, AView.IOnHoverListener
{
// Tracks the last button pressed so we can use it for subsequent Move/Up/Cancel
ButtonsMask? _activeButton;

internal PointerGestureHandler(Func<View> getView, Func<AView> getControl)
{
GetView = getView;
Expand Down Expand Up @@ -50,6 +54,133 @@ public bool OnHover(AView control, MotionEvent e)
return false;
}

// This method is called by InnerGestureListener to handle touch events for pointer gestures
public bool OnTouch(MotionEvent e)
{
var view = GetView();

if (view == null)
return false;

var control = GetControl();
if (control == null)
return false;

var platformPointerArgs = new PlatformPointerEventArgs(control, e);

foreach (var gesture in view.GetCompositeGestureRecognizers())
{
if (gesture is PointerGestureRecognizer pgr)
{
// Determine the button for this action. For Move/Up/Cancel prefer the active button, if any.
ButtonsMask current = GetPressedButton(e);
ButtonsMask effectiveButton = current;

switch (e.Action)
{
case MotionEventActions.Down:
// Primary button goes through Down/Up
_activeButton = current;
effectiveButton = current;
if (!CheckButtonMask(pgr, effectiveButton))
continue;
pgr.SendPointerPressed(view, (relativeTo) => e.CalculatePosition(GetView(), relativeTo), platformPointerArgs, effectiveButton);
break;
case MotionEventActions.Move:
// Keep reporting the button that initiated the press if one is active
effectiveButton = _activeButton ?? current;
if (!CheckButtonMask(pgr, effectiveButton))
continue;
pgr.SendPointerMoved(view, (relativeTo) => e.CalculatePosition(GetView(), relativeTo), platformPointerArgs, effectiveButton);
break;
case MotionEventActions.Up:
// ACTION_UP does not carry ActionButton. Use the active one if available.
effectiveButton = _activeButton ?? current;
if (!CheckButtonMask(pgr, effectiveButton))
continue;
pgr.SendPointerReleased(view, (relativeTo) => e.CalculatePosition(GetView(), relativeTo), platformPointerArgs, effectiveButton);
// Clear active button after release
_activeButton = null;
break;
case MotionEventActions.Cancel:
// Treat cancel similar to release for active button, then exit
effectiveButton = _activeButton ?? current;
if (!CheckButtonMask(pgr, effectiveButton))
continue;
pgr.SendPointerExited(view, (relativeTo) => e.CalculatePosition(GetView(), relativeTo), platformPointerArgs, effectiveButton);
_activeButton = null;
break;
}
}
}

return false;
}

ButtonsMask GetPressedButton(MotionEvent motionEvent)
{
if (motionEvent == null)
return ButtonsMask.Primary;

var action = motionEvent.Action;

// For explicit button change events (mouse/pen), use ActionButton to determine which button changed
if (OperatingSystem.IsAndroidVersionAtLeast(23) &&
(action == MotionEventActions.ButtonPress || action == MotionEventActions.ButtonRelease))
{
#pragma warning disable CA1416 // Validate platform compatibility
var actionButton = motionEvent.ActionButton; // Which button changed for this event
if ((actionButton & MotionEventButtonState.Secondary) == MotionEventButtonState.Secondary)
return ButtonsMask.Secondary;
if ((actionButton & MotionEventButtonState.Primary) == MotionEventButtonState.Primary)
return ButtonsMask.Primary;
#pragma warning restore CA1416 // Validate platform compatibility
}

// Otherwise, infer from current ButtonState (covers Move/Down/Up and API < 23)
var buttonState = motionEvent.ButtonState;

// Check for secondary button (right mouse button)
if ((buttonState & MotionEventButtonState.Secondary) == MotionEventButtonState.Secondary)
{
return ButtonsMask.Secondary;
}

// Check for stylus secondary button on API 23+
if (OperatingSystem.IsAndroidVersionAtLeast(23))
{
#pragma warning disable CA1416 // Validate platform compatibility
if (CheckStylusSecondaryButton(buttonState))
#pragma warning restore CA1416 // Validate platform compatibility
{
return ButtonsMask.Secondary;
}
}

// Default to primary button
return ButtonsMask.Primary;
}

[SupportedOSPlatform("android23.0")]
bool CheckStylusSecondaryButton(MotionEventButtonState buttonState)
{
return (buttonState & MotionEventButtonState.StylusSecondary) == MotionEventButtonState.StylusSecondary;
}

bool CheckButtonMask(PointerGestureRecognizer recognizer, ButtonsMask currentButton)
{
// If no buttons specified (enum backing value is 0), default to Primary only
if ((int)recognizer.Buttons == 0)
return currentButton == ButtonsMask.Primary;

if (currentButton == ButtonsMask.Secondary)
{
return (recognizer.Buttons & ButtonsMask.Secondary) == ButtonsMask.Secondary;
}

return (recognizer.Buttons & ButtonsMask.Primary) == ButtonsMask.Primary;
}

public void SetupHandlerForPointer()
{
var view = GetView();
Expand Down
21 changes: 19 additions & 2 deletions src/Controls/src/Core/Platform/Android/TapAndPanGestureDetector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,19 @@ namespace Microsoft.Maui.Controls.Platform
class TapAndPanGestureDetector : GestureDetector
{
InnerGestureListener? _listener;
PointerGestureHandler? _pointerGestureHandler;

public TapAndPanGestureDetector(Context context, InnerGestureListener listener) : base(context, listener)
{
_listener = listener;
UpdateLongPressSettings();
}

public void SetPointerGestureHandler(PointerGestureHandler pointerGestureHandler)
{
_pointerGestureHandler = pointerGestureHandler;
}

public void UpdateLongPressSettings()
{
if (_listener == null)
Expand All @@ -36,6 +43,12 @@ public override bool OnTouchEvent(MotionEvent ev)
if (base.OnTouchEvent(ev))
return true;

if (_pointerGestureHandler != null && ev?.Action is
MotionEventActions.Up or MotionEventActions.Down or MotionEventActions.Cancel)
{
_pointerGestureHandler.OnTouch(ev);
}

if (_listener != null && ev?.Action == MotionEventActions.Up)
_listener.EndScrolling();

Expand All @@ -48,8 +61,12 @@ protected override void Dispose(bool disposing)

if (disposing)
{
_listener?.Dispose();
_listener = null;
if (_listener != null)
{
_listener.Dispose();
_listener = null;
}
_pointerGestureHandler = null;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ TapAndPanGestureDetector InitializeTapAndPanAndSwipeDetector()
throw new InvalidOperationException("Context cannot be null here");

var context = Control.Context;
var pointerHandler = InitializePointerHandler();
var listener = new InnerGestureListener(
new TapGestureHandler(() => View, () =>
{
Expand All @@ -140,10 +141,12 @@ TapAndPanGestureDetector InitializeTapAndPanAndSwipeDetector()
new PanGestureHandler(() => View),
new SwipeGestureHandler(() => View),
InitializeDragAndDropHandler(),
InitializePointerHandler()
pointerHandler
);

return new TapAndPanGestureDetector(context, listener);
var detector = new TapAndPanGestureDetector(context, listener);
detector.SetPointerGestureHandler(pointerHandler);
return detector;
}

ScaleGestureDetector InitializeScaleDetector()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media.Imaging;
using Microsoft.UI.Input;
using Windows.Storage.Streams;

namespace Microsoft.Maui.Controls.Platform
Expand Down Expand Up @@ -666,26 +667,34 @@ void OnPgrPointerMoved(object sender, PointerRoutedEventArgs e)
=> GetPosition(relativeTo, e), _control is null ? null : new PlatformPointerEventArgs(_control, e)));

void OnPgrPointerPressed(object sender, PointerRoutedEventArgs e)
{
HandlePgrPointerEvent(e, (view, recognizer)
=> recognizer.SendPointerPressed(view, (relativeTo)
=> GetPosition(relativeTo, e), _control is null ? null : new PlatformPointerEventArgs(_control, e)));

if ((_subscriptionFlags & SubscriptionFlags.ContainerManipulationAndPointerEventsSubscribed) != 0)
{
OnPointerPressed(sender, e);
}
}
=> HandlePgrPointerButtonAction(sender, e, true);

void OnPgrPointerReleased(object sender, PointerRoutedEventArgs e)
=> HandlePgrPointerButtonAction(sender, e, false);

void HandlePgrPointerButtonAction(object sender, PointerRoutedEventArgs e, bool isPressed)
{
HandlePgrPointerEvent(e, (view, recognizer)
=> recognizer.SendPointerReleased(view, (relativeTo)
=> GetPosition(relativeTo, e), _control is null ? null : new PlatformPointerEventArgs(_control, e)));
if (Element is View view)
{
var pointerGestures = ElementGestureRecognizers.GetGesturesFor<PointerGestureRecognizer>();
var button = GetPressedButton(sender, e);
foreach (var recognizer in pointerGestures)
{
if (!CheckButtonMask(recognizer, button))
continue;
if (isPressed)
recognizer.SendPointerPressed(view, (relativeTo) => GetPosition(relativeTo, e), _control is null ? null : new PlatformPointerEventArgs(_control, e), button);
else
recognizer.SendPointerReleased(view, (relativeTo) => GetPosition(relativeTo, e), _control is null ? null : new PlatformPointerEventArgs(_control, e), button);
}
}

if ((_subscriptionFlags & SubscriptionFlags.ContainerManipulationAndPointerEventsSubscribed) != 0)
{
OnPointerReleased(sender, e);
if (isPressed)
OnPointerPressed(sender, e);
else
OnPointerReleased(sender, e);
}
}

Expand Down Expand Up @@ -745,6 +754,53 @@ bool IsPointerEventRelevantToCurrentElement(PointerRoutedEventArgs e)
}
}

ButtonsMask GetPressedButton(object? sender, PointerRoutedEventArgs e)
{
// Touch/Pen don't have right button semantics; treat as Primary
if (e.Pointer?.PointerDeviceType != PointerDeviceType.Mouse)
return ButtonsMask.Primary;

var reference = sender as UIElement ?? _container ?? _control ?? _container?.XamlRoot?.Content as UIElement;
if (reference is null)
return ButtonsMask.Primary;

var point = e.GetCurrentPoint(reference);
var props = point?.Properties;
if (props is null)
return ButtonsMask.Primary;

switch (props.PointerUpdateKind)
{
case PointerUpdateKind.RightButtonPressed:
case PointerUpdateKind.RightButtonReleased:
return ButtonsMask.Secondary;
case PointerUpdateKind.LeftButtonPressed:
case PointerUpdateKind.LeftButtonReleased:
return ButtonsMask.Primary;
// Middle/other map to Primary by convention
case PointerUpdateKind.MiddleButtonPressed:
case PointerUpdateKind.MiddleButtonReleased:
case PointerUpdateKind.Other:
default:
break;
}

if (props.IsRightButtonPressed)
return ButtonsMask.Secondary;

return ButtonsMask.Primary;
}

bool CheckButtonMask(PointerGestureRecognizer recognizer, ButtonsMask currentButton)
{
if (currentButton == ButtonsMask.Secondary)
{
return (recognizer.Buttons & ButtonsMask.Secondary) == ButtonsMask.Secondary;
}

return (recognizer.Buttons & ButtonsMask.Primary) == ButtonsMask.Primary;
}

Point? GetPosition(IElement? relativeTo, RoutedEventArgs e)
{
var result = e.GetPositionRelativeToElement(relativeTo);
Expand Down
Loading
Loading