Skip to content

Commit

Permalink
fix(pointers)!: [iOS][Android] Enter will now be raised using managed…
Browse files Browse the repository at this point in the history
… bubbling only

BREAKING CHANGE: On iOS and Android, PointerEntered are now raised on the whole tree **before** the PointerPressed instead of being raise per layer.
This follow the behavior of UWP and the same as Skia and WASM.
The PointerReleased + PointerExited are still raised per layer.
  • Loading branch information
dr1rrb committed Feb 2, 2022
1 parent ef2c2ef commit ad80757
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 24 deletions.
11 changes: 11 additions & 0 deletions src/Uno.UI/UI/Xaml/Input/PointerRoutedEventArgs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,17 @@ public IList<PointerPoint> GetIntermediatePoints(UIElement relativeTo)

public Pointer Pointer { get; }

/// <summary>
/// Reset the internal state in order to re-use that event args to raise another event
/// </summary>
internal PointerRoutedEventArgs Reset()
{
CanBubbleNatively = PlatformSupportsNativeBubbling;
Handled = false;

return this;
}

/// <inheritdoc />
public override string ToString()
=> $"PointerRoutedEventArgs({Pointer}@{GetCurrentPoint(null).Position})";
Expand Down
20 changes: 16 additions & 4 deletions src/Uno.UI/UI/Xaml/UIElement.Pointers.Android.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,14 +124,26 @@ private bool OnNativeMotionEvent(MotionEvent nativeEvent, PointerRoutedEventArgs

case MotionEventActions.Down when args.Pointer.PointerDeviceType == PointerDeviceType.Touch:
case MotionEventActions.PointerDown when args.Pointer.PointerDeviceType == PointerDeviceType.Touch:
return OnNativePointerEnter(args) | OnNativePointerDown(args);
// We don't have any enter / exit on Android for touches, so we explicitly generate one on down / up.
// That event args is requested to bubble in managed code only (args.CanBubbleNatively = false),
// so we follow the same sequence as UWP (the whole tree gets entered before the pressed),
// and we make sure that the event will bubble through the whole tree, no matter if the Pressed event is handle or not.
// Note: Parents will also try to raise the "Enter" but they will be silent since the pointer is already considered as pressed.
args.CanBubbleNatively = false;
OnNativePointerEnter(args);
return OnNativePointerDown(args.Reset());
case PointerRoutedEventArgs.StylusWithBarrelDown:
case MotionEventActions.Down:
case MotionEventActions.PointerDown:
return OnNativePointerDown(args);
case MotionEventActions.Up when args.Pointer.PointerDeviceType == PointerDeviceType.Touch:
case MotionEventActions.PointerUp when args.Pointer.PointerDeviceType == PointerDeviceType.Touch:
return OnNativePointerUp(args) | OnNativePointerExited(args);
var handled = OnNativePointerUp(args);
// Like for the Down, we manually generate an Exited, but we DON'T REQUEST IT to bubble in managed as it would mean
// that parent elements would get the Exit **before** the Release!
// Instead we raise it per layer, which means that we won't follow the UWP behavior where the whole tree gets the Released and then the Exited.
OnNativePointerExited(args.Reset());
return handled;
case PointerRoutedEventArgs.StylusWithBarrelUp:
case MotionEventActions.Up:
case MotionEventActions.PointerUp:
Expand All @@ -141,9 +153,9 @@ private bool OnNativeMotionEvent(MotionEvent nativeEvent, PointerRoutedEventArgs
// So on each POINTER_MOVE we make sure to update the pressed state if it does not match.
// Note: We can also have HOVER_MOVE with barrel button pressed, so we make sure to "PointerDown" only for Mouse.
case MotionEventActions.HoverMove when args.Pointer.PointerDeviceType == PointerDeviceType.Mouse && args.HasPressedButton && !IsPressed(args.Pointer):
return OnNativePointerDown(args) | OnNativePointerMoveWithOverCheck(args, isInView);
return OnNativePointerDown(args) | OnNativePointerMoveWithOverCheck(args.Reset(), isInView);
case MotionEventActions.HoverMove when !args.HasPressedButton && IsPressed(args.Pointer):
return OnNativePointerUp(args) | OnNativePointerMoveWithOverCheck(args, isInView);
return OnNativePointerUp(args) | OnNativePointerMoveWithOverCheck(args.Reset(), isInView);

case PointerRoutedEventArgs.StylusWithBarrelMove:
case MotionEventActions.Move:
Expand Down
51 changes: 31 additions & 20 deletions src/Uno.UI/UI/Xaml/UIElement.Pointers.iOS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,23 +88,6 @@ public override void TouchesBegan(NSSet touches, UIEvent evt)
return; // Will also prevent subsequents events
}

/* Note: Here we have a mismatching behavior with UWP, if the events bubble natively we're going to get
(with Ctrl_02 is a child of Ctrl_01):
Ctrl_02: Entered
Pressed
Ctrl_01: Entered
Pressed
While on UWP we will get:
Ctrl_02: Entered
Ctrl_01: Entered
Ctrl_02: Pressed
Ctrl_01: Pressed
However, to fix this is would mean that we handle all events in managed code, but this would
break lots of control (ScrollViewer) and ability to easily integrate an external component.
*/

try
{
if (ManipulationMode == ManipulationModes.None)
Expand All @@ -127,8 +110,15 @@ public override void TouchesBegan(NSSet touches, UIEvent evt)
continue;
}

isHandledOrBubblingInManaged |= OnNativePointerEnter(args);
isHandledOrBubblingInManaged |= OnNativePointerDown(args);
// We don't have any enter on iOS for touches, so we explicitly generate one on down.
// That event args is requested to bubble in managed code only (args.CanBubbleNatively = false),
// so we follow the same sequence as UWP (the whole tree gets entered before the pressed),
// and we make sure that the event will bubble through the whole tree, no matter if the Pressed event is handle or not.
// Note: Parents will also try to raise the "Enter" but they will be silent since the pointer is already considered as pressed.
args.CanBubbleNatively = false;
OnNativePointerEnter(args);

isHandledOrBubblingInManaged |= OnNativePointerDown(args.Reset());

if (isHandledOrBubblingInManaged)
{
Expand Down Expand Up @@ -190,6 +180,23 @@ public override void TouchesMoved(NSSet touches, UIEvent evt)

public override void TouchesEnded(NSSet touches, UIEvent evt)
{
/* Note: Here we have a mismatching behavior with UWP, if the events bubble natively we're going to get
(with Ctrl_02 is a child of Ctrl_01):
Ctrl_02: Released
Exited
Ctrl_01: Released
Exited
While on UWP we will get:
Ctrl_02: Released
Ctrl_01: Released
Ctrl_02: Exited
Ctrl_01: Exited
However, to fix this is would mean that we handle all events in managed code, but this would
break lots of control (ScrollViewer) and ability to easily integrate an external component.
*/

try
{
var isHandledOrBubblingInManaged = default(bool);
Expand All @@ -215,7 +222,11 @@ public override void TouchesEnded(NSSet touches, UIEvent evt)
}

isHandledOrBubblingInManaged |= OnNativePointerUp(args);
isHandledOrBubblingInManaged |= OnNativePointerExited(args);

// Like for the Down, we manually generate an Exited, but we DON'T REQUEST IT to bubble in managed as it would mean
// that parent elements would get the Exit **before** the Release!
// Instead we raise it per layer, which means that we won't follow the UWP behavior where the whole tree gets the Released and then the Exited.
OnNativePointerExited(args.Reset());

pt.Release(this);
}
Expand Down

0 comments on commit ad80757

Please sign in to comment.