Search Unity

Bug UIElements on Android doesn't properly capture pointers

Discussion in 'UI Toolkit' started by perholmes, Sep 3, 2021.

  1. perholmes

    perholmes

    Joined:
    Dec 29, 2017
    Posts:
    296
    Hi,

    UPDATE: Please see further down. The issue is different than initially believed, and is about multiple pointer IDs being received for every tap, an apparent bug in UI Elements on Android.

    ORIGINAL: I'm having some very different behavior of captured pointer events on Android.

    On desktop and in the device simulator, capture is capture, nothing propagates, nothing leaks, the object owns all pointer events. But on Android, even when I capture pointer events in a child object, the parent object keeps receiving events.

    I seems like a bug, because the behavior is markedly different, and breaks many UI assumptions. I'd be able to work around it, but I'd love for the UI Elements team to confirm whether or not this is a bug. And if it's not a bug, then I'd love to hear the reasoning behind the difference in whether or not capturing stops bubble-up propagation.

    Thanks,

    Per
     
    Last edited: Sep 3, 2021
  2. perholmes

    perholmes

    Joined:
    Dec 29, 2017
    Posts:
    296
    I'm attaching a proof.

    I've done the most thorough testing on the PointerMoveEvents of a child item and its parent. Both can capture the pointer. Here is the child item called MenuClickBase. Observe that it captures the mouse on pointer down, and logs mouse move events.

    upload_2021-9-3_20-12-35.png

    And here is the parent item. I have a mechanism where the child releases pointer capture, and the parent captures the mouse events instead.

    upload_2021-9-3_20-14-19.png

    On desktop, when I transfer from child to parent, the logging looks like this, with a clean transfer of capture:

    Code (CSharp):
    1. [20:11:49.334] [INFO] Click base pointer move
    2. [20:11:49.351] [INFO] Click base pointer move
    3. [20:11:49.368] [INFO] Click base pointer move
    4. [20:11:50.252] [INFO] Click base pointer down: 0
    5. [20:11:50.634] [INFO] Click base pointer move
    6. [20:11:50.652] [INFO] Click base pointer move
    7. [20:11:50.668] [INFO] Content pointer move
    8. [20:11:50.685] [INFO] Content pointer move
    9. [20:11:50.701] [INFO] Content pointer move
    10. [20:11:50.718] [INFO] Content pointer move
    11. [20:11:50.734] [INFO] Content pointer move
    But on Android, the capture isn't working. Both on the original click, and the subsequent move, both child and parent receive pointer down and move events, regardless of capture:

    Code (CSharp):
    1. 09-03 20:07:33.517 18143 18168 I Unity   : [20:07:33.517] [INFO] Click base pointer down: 0
    2. 09-03 20:07:33.557 18143 18168 I Unity   : [20:07:33.557] [INFO] Content pointer down: 1
    3. 09-03 20:07:33.581 18143 18168 I Unity   : [20:07:33.581] [INFO] Click base pointer move
    4. 09-03 20:07:33.585 18143 18168 I Unity   : [20:07:33.584] [INFO] Click base pointer move
    5. 09-03 20:07:33.586 18143 18168 I Unity   : [20:07:33.586] [INFO] Content pointer move
    6. 09-03 20:07:33.609 18143 18168 I Unity   : [20:07:33.609] [INFO] Click base pointer move
    7. 09-03 20:07:33.844 18143 18168 I Unity   : [20:07:33.844] [INFO] Click base pointer move
    8. 09-03 20:07:33.847 18143 18168 I Unity   : [20:07:33.847] [INFO] Content pointer move
    9. 09-03 20:07:33.868 18143 18168 I Unity   : [20:07:33.868] [INFO] Click base pointer move
    10. 09-03 20:07:33.868 18143 18168 I Unity   : [20:07:33.868] [INFO] Content pointer move
    11. 09-03 20:07:33.884 18143 18168 I Unity   : [20:07:33.884] [INFO] Click base pointer move
    12. 09-03 20:07:33.885 18143 18168 I Unity   : [20:07:33.885] [INFO] Content pointer move
    13. 09-03 20:07:33.901 18143 18168 I Unity   : [20:07:33.901] [INFO] Click base pointer move
    14. 09-03 20:07:33.901 18143 18168 I Unity   : [20:07:33.901] [INFO] Content pointer move
    15. 09-03 20:07:33.918 18143 18168 I Unity   : [20:07:33.917] [INFO] Click base pointer move
    16. 09-03 20:07:33.918 18143 18168 I Unity   : [20:07:33.918] [INFO] Content pointer move
    17. 09-03 20:07:33.934 18143 18168 I Unity   : [20:07:33.934] [INFO] Click base pointer move
    18. 09-03 20:07:33.934 18143 18168 I Unity   : [20:07:33.934] [INFO] Content pointer move
    19. 09-03 20:07:33.952 18143 18168 I Unity   : [20:07:33.952] [INFO] Click base pointer move
    This basically breaks UI Elements on Android.
     

    Attached Files:

  3. perholmes

    perholmes

    Joined:
    Dec 29, 2017
    Posts:
    296
    Also, I've noticed that I'm getting double touch events on Android. This is a Pixel 2 XL. This could somehow interact poorly with the capture mechanism. My code above filters it out because I only react to the first pointer id. But maybe the capture mechanism gets confused.
     
  4. perholmes

    perholmes

    Joined:
    Dec 29, 2017
    Posts:
    296
    The real issue seems to be that two pointer events are received for every tap. This may be hidden in other code, but since I work with captures, it means that I only capture the first tap, and the second tap propagates to the parent.

    This still seems to be a bug, just a different one. Here I'm logging pointerId and position from the event. Observe that:

    * Two events are received.
    * They have unique pointerIds, so Unity is generating them on purpose.
    * They have the same position.
    * My capture only captures the first one, and the second bubbles to the parent, creating the appearance that capture doesn't work.

    Code (CSharp):
    1.  
    2. 09-03 22:56:51.237 15474 15496 I Unity   : [22:56:51.236] [INFO] Click base pointer down: 0, position (527.94, 239.88, 0.00)
    3. 09-03 22:56:51.274 15474 15496 I Unity   : [22:56:51.274] [INFO] Click base pointer down: 1, position (527.94, 239.88, 0.00)
    4. 09-03 22:56:51.276 15474 15496 I Unity   : [22:56:51.276] [INFO] Content pointer down: 1
    On desktop and in the device simulator, I only receive one pointer for every click. I've verified that this is not somehow an issue with callbacks being instantiated twice. But this is fine in desktop. And multiple callbacks would still only get one pointerId.

    So my best bet is that UIElements sends double pointer events on Android. This will break captures and any pinch-zoom or multi-finger gesture, because it becomes impossible to reason about the pointers.
     
  5. perholmes

    perholmes

    Joined:
    Dec 29, 2017
    Posts:
    296
    Being dead in the water, I've had to roll a temporary fix to de-duplicate pointer events on Android. This hack only works because all my controls are custom for this app. Maybe it'll help someone. But this issue needs urgent attention, because all assumptions about event capture and propagation go out the window.

    The following PointerFix static class registers pointers and detects if they happen in the same screen position. It then lets the caller know whether to process the event or not.

    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections.Generic;
    3. using UnityEngine.UIElements;
    4.  
    5. // This is an EXTREMELY TEMPORARY filter to work around UIElements sending multiple identical
    6. // pointers on Android with different pointer IDs, causing capture and propagation problems. This
    7. // fix is incompatible with hovering, since we only process move events when a pointer is down.
    8.  
    9. public class PointerFix
    10. {
    11.     private static Dictionary<int, Vector3> pointers = new Dictionary<int, Vector3>();
    12.  
    13.     // Call on Down. Returns false if the event should not handled.
    14.     public static bool Down(PointerDownEvent evt)
    15.     {
    16.         if (pointers.ContainsKey(evt.pointerId)) {
    17.             Debug.Log($"Pointer {evt.pointerId} is already registered");
    18.             evt.StopImmediatePropagation();
    19.             return false;
    20.         }
    21.  
    22.         foreach (var pointer in pointers) {
    23.             if (Vector3.Distance(evt.position, pointer.Value) < 1f) {
    24.                 Debug.Log($"Pointer {evt.pointerId} rejected at {evt.position} because it's a duplicate");
    25.                 evt.StopImmediatePropagation();
    26.                 return false;
    27.             }
    28.         }
    29.  
    30.         Debug.Log($"Pointer {evt.pointerId} pressed at {evt.position}");
    31.         pointers[evt.pointerId] = evt.position;
    32.         return true;
    33.     }
    34.  
    35.     // Call on Move. Returns false if the event should not handled.
    36.     public static bool Move(PointerMoveEvent evt)
    37.     {
    38.         var active = pointers.ContainsKey(evt.pointerId);
    39.         if (!active) {
    40.             evt.StopImmediatePropagation();
    41.         }
    42.         return active;
    43.     }
    44.  
    45.     // Call on Up. Returns false if the event should not handled.
    46.     public static bool Up(PointerUpEvent evt)
    47.     {
    48.         var active = pointers.ContainsKey(evt.pointerId);
    49.         Debug.Log($"Pointer {evt.pointerId} released");
    50.         pointers.Remove(evt.pointerId);
    51.         if (!active) {
    52.             evt.StopImmediatePropagation();
    53.         }
    54.         return active;
    55.     }
    56.  
    57.     // For my personal needs, since I do delicate hand-offs of pointer capture between children and
    58.     // parents.
    59.     public static void ForceUp(int pointerId)
    60.     {
    61.         var active = pointers.ContainsKey(pointerId);
    62.         Debug.Log($"Pointer {pointerId} forcibly released");
    63.         pointers.Remove(pointerId);
    64.     }
    65. }
    Every PointerDown, Move and Up event should then be prefixed with a call to this. This is why it only works on custom controls:

    upload_2021-9-5_9-12-37.png

    upload_2021-9-5_9-12-50.png

    upload_2021-9-5_9-13-2.png

    It's ugly, but I get to live another day.
     
  6. perholmes

    perholmes

    Joined:
    Dec 29, 2017
    Posts:
    296
    And to document the problem again, here is the output of PointerFix on desktop:

    Code (CSharp):
    1. Pointer 0 pressed at (754.07, 404.71, 0.00)
    2. Pointer 0 released
    3. Pointer 0 pressed at (755.07, 139.71, 0.00)
    4. Pointer 0 released
    5. Pointer 0 pressed at (769.07, 397.71, 0.00)
    6. Pointer 0 released
    7. Pointer 0 pressed at (766.07, 156.71, 0.00)
    8. Pointer 0 released
    But when running on Android, every pointer is a duplicate, which is detected by PointerFix and discarded:

    Code (CSharp):
    1. 09:16:02 I/Unity    : Pointer 0 pressed at (530.73, 245.96, 0.00)
    2. 09:16:02 I/Unity    : Pointer 1 rejected at (530.73, 245.96, 0.00) because it's a duplicate
    3. 09:16:02 I/Unity    : Pointer 0 released
    4. 09:16:02 I/Unity    : Pointer 1 released
    5. 09:16:04 I/Unity    : Pointer 0 pressed at (551.01, 75.56, 0.00)
    6. 09:16:04 I/Unity    : Pointer 1 rejected at (551.01, 75.56, 0.00) because it's a duplicate
    7. 09:16:04 I/Unity    : Pointer 0 released
    8. 09:16:04 I/Unity    : Pointer 1 released
    9. 09:16:05 I/Unity    : Pointer 0 pressed at (509.68, 236.08, 0.00)
    10. 09:16:05 I/Unity    : Pointer 1 rejected at (509.68, 236.08, 0.00) because it's a duplicate
    11. 09:16:05 I/Unity    : Pointer 0 released
    12. 09:16:05 I/Unity    : Pointer 1 released
    13. 09:16:06 I/Unity    : Pointer 0 pressed at (558.36, 89.26, 0.00)
    14. 09:16:06 I/Unity    : Pointer 1 rejected at (558.36, 89.26, 0.00) because it's a duplicate
    15. 09:16:06 I/Unity    : Pointer 0 released
    16. 09:16:06 I/Unity    : Pointer 1 released
     
  7. AlexandreT-unity

    AlexandreT-unity

    Unity Technologies

    Joined:
    Feb 1, 2018
    Posts:
    377
    Hi @perholmes, could you submit a small repro project through the bug reporter so we can investigate? Go to Help > Report a bug... Thanks!
     
  8. perholmes

    perholmes

    Joined:
    Dec 29, 2017
    Posts:
    296
    I've created a reproduction project where it still happens, and submitted it. I was not given an ID. Here is the text of the report, to help you locate it:

    UIElements sends double pointer events on Android, causing all assumptions about event propagation and pointer capture to break down. When running the reproduction project on desktop, pointer events are received in the expected quantity (log output):

    PointerDownEvent: 0, Position: (300.64, 283.50, 0.00)
    PointerUpEvent: 0, Position: (300.64, 283.50, 0.00)

    However, when running on Android, two pointers are received for every pointer down and up.

    07:34:33 I/Unity : PointerDownEvent: 0, Position: (297.60, 266.10, 0.00)
    07:34:33 I/Unity : PointerDownEvent: 1, Position: (297.60, 266.10, 0.00)
    07:34:33 I/Unity : PointerUpEvent: 0, Position: (297.60, 266.10, 0.00)
    07:34:33 I/Unity : PointerUpEvent: 1, Position: (297.60, 266.10, 0.00)

    This results in both CapturePointer and StopImmediatePropagation stopping working in cases where a child object is tracking the pointer down/up cycle, causing pointers that are believed to be captured to actually go to the parent, breaking UIs completely.
     
  9. perholmes

    perholmes

    Joined:
    Dec 29, 2017
    Posts:
    296
    Case 1364340
     
    AlexandreT-unity likes this.