Track motion events for reuse post gesture disambiguation (#19484)
This change makes it so that we track all the motion events encountered by `FlutterView` and all of its subviews in the `MotionEventTracker` class, indexed by a unique `MotionEventId`. This identifier is then passed to the Flutter framework as seen in https://github.com/flutter/flutter/pull/60930. Once the gestures take part in gesture disambiguation and are sent back to the engine, we look-up the original motion event using the `MotionEventId` and dispatch it to the platform.
Bug: https://github.com/flutter/flutter/issues/58837
diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter
index 206221a..057c045 100755
--- a/ci/licenses_golden/licenses_flutter
+++ b/ci/licenses_golden/licenses_flutter
@@ -697,6 +697,7 @@
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterSurfaceView.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterTextureView.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterView.java
+FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/MotionEventTracker.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/RenderMode.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/SplashScreen.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/SplashScreenProvider.java
diff --git a/lib/ui/hooks.dart b/lib/ui/hooks.dart
index 645e54e..ff2dcaa 100644
--- a/lib/ui/hooks.dart
+++ b/lib/ui/hooks.dart
@@ -287,9 +287,9 @@
// If this value changes, update the encoding code in the following files:
//
// * pointer_data.cc
-// * pointers.dart
+// * pointer.dart
// * AndroidTouchProcessor.java
-const int _kPointerDataFieldCount = 28;
+const int _kPointerDataFieldCount = 29;
PointerDataPacket _unpackPointerDataPacket(ByteData packet) {
const int kStride = Int64List.bytesPerElement;
@@ -300,6 +300,7 @@
for (int i = 0; i < length; ++i) {
int offset = i * _kPointerDataFieldCount;
data.add(PointerData(
+ embedderId: packet.getInt64(kStride * offset++, _kFakeHostEndian),
timeStamp: Duration(microseconds: packet.getInt64(kStride * offset++, _kFakeHostEndian)),
change: PointerChange.values[packet.getInt64(kStride * offset++, _kFakeHostEndian)],
kind: PointerDeviceKind.values[packet.getInt64(kStride * offset++, _kFakeHostEndian)],
diff --git a/lib/ui/pointer.dart b/lib/ui/pointer.dart
index bf9ba64..3c12cb9 100644
--- a/lib/ui/pointer.dart
+++ b/lib/ui/pointer.dart
@@ -72,6 +72,7 @@
class PointerData {
/// Creates an object that represents the state of a pointer.
const PointerData({
+ this.embedderId = 0,
this.timeStamp = Duration.zero,
this.change = PointerChange.cancel,
this.kind = PointerDeviceKind.touch,
@@ -102,6 +103,13 @@
this.scrollDeltaY = 0.0,
});
+ /// Unique identifier that ties the [PointerEvent] to embedder event created it.
+ ///
+ /// No two pointer events can have the same [embedderId]. This is different from
+ /// [pointerIdentifier] - used for hit-testing, whereas [embedderId] is used to
+ /// identify the platform event.
+ final int embedderId;
+
/// Time of event dispatch, relative to an arbitrary timeline.
final Duration timeStamp;
@@ -263,6 +271,7 @@
/// Returns a complete textual description of the information in this object.
String toStringFull() {
return '$runtimeType('
+ 'embedderId: $embedderId, '
'timeStamp: $timeStamp, '
'change: $change, '
'kind: $kind, '
diff --git a/lib/ui/window/pointer_data.h b/lib/ui/window/pointer_data.h
index 96a9e7d..124f35e 100644
--- a/lib/ui/window/pointer_data.h
+++ b/lib/ui/window/pointer_data.h
@@ -10,7 +10,7 @@
namespace flutter {
// If this value changes, update the pointer data unpacking code in hooks.dart.
-static constexpr int kPointerDataFieldCount = 28;
+static constexpr int kPointerDataFieldCount = 29;
static constexpr int kBytesPerField = sizeof(int64_t);
// Must match the button constants in events.dart.
enum PointerButtonMouse : int64_t {
@@ -58,6 +58,7 @@
kScroll,
};
+ int64_t embedder_id;
int64_t time_stamp;
Change change;
DeviceKind kind;
diff --git a/lib/web_ui/lib/src/ui/pointer.dart b/lib/web_ui/lib/src/ui/pointer.dart
index d541aba..698badc 100644
--- a/lib/web_ui/lib/src/ui/pointer.dart
+++ b/lib/web_ui/lib/src/ui/pointer.dart
@@ -71,6 +71,7 @@
class PointerData {
/// Creates an object that represents the state of a pointer.
const PointerData({
+ this.embedderId = 0,
this.timeStamp = Duration.zero,
this.change = PointerChange.cancel,
this.kind = PointerDeviceKind.touch,
@@ -101,6 +102,13 @@
this.scrollDeltaY = 0.0,
});
+ /// Unique identifier that ties the [PointerEvent] to embedder event created it.
+ ///
+ /// No two pointer events can have the same [embedderId]. This is different from
+ /// [pointerIdentifier] - used for hit-testing, whereas [embedderId] is used to
+ /// identify the platform event.
+ final int embedderId;
+
/// Time of event dispatch, relative to an arbitrary timeline.
final Duration timeStamp;
@@ -257,46 +265,47 @@
final double scrollDeltaY;
@override
- String toString() => '$runtimeType(x: $physicalX, y: $physicalY)';
+ String toString() => 'PointerData(x: $physicalX, y: $physicalY)';
/// Returns a complete textual description of the information in this object.
String toStringFull() {
return '$runtimeType('
- 'timeStamp: $timeStamp, '
- 'change: $change, '
- 'kind: $kind, '
- 'signalKind: $signalKind, '
- 'device: $device, '
- 'pointerIdentifier: $pointerIdentifier, '
- 'physicalX: $physicalX, '
- 'physicalY: $physicalY, '
- 'physicalDeltaX: $physicalDeltaX, '
- 'physicalDeltaY: $physicalDeltaY, '
- 'buttons: $buttons, '
- 'synthesized: $synthesized, '
- 'pressure: $pressure, '
- 'pressureMin: $pressureMin, '
- 'pressureMax: $pressureMax, '
- 'distance: $distance, '
- 'distanceMax: $distanceMax, '
- 'size: $size, '
- 'radiusMajor: $radiusMajor, '
- 'radiusMinor: $radiusMinor, '
- 'radiusMin: $radiusMin, '
- 'radiusMax: $radiusMax, '
- 'orientation: $orientation, '
- 'tilt: $tilt, '
- 'platformData: $platformData, '
- 'scrollDeltaX: $scrollDeltaX, '
- 'scrollDeltaY: $scrollDeltaY'
- ')';
+ 'embedderId: $embedderId, '
+ 'timeStamp: $timeStamp, '
+ 'change: $change, '
+ 'kind: $kind, '
+ 'signalKind: $signalKind, '
+ 'device: $device, '
+ 'pointerIdentifier: $pointerIdentifier, '
+ 'physicalX: $physicalX, '
+ 'physicalY: $physicalY, '
+ 'physicalDeltaX: $physicalDeltaX, '
+ 'physicalDeltaY: $physicalDeltaY, '
+ 'buttons: $buttons, '
+ 'synthesized: $synthesized, '
+ 'pressure: $pressure, '
+ 'pressureMin: $pressureMin, '
+ 'pressureMax: $pressureMax, '
+ 'distance: $distance, '
+ 'distanceMax: $distanceMax, '
+ 'size: $size, '
+ 'radiusMajor: $radiusMajor, '
+ 'radiusMinor: $radiusMinor, '
+ 'radiusMin: $radiusMin, '
+ 'radiusMax: $radiusMax, '
+ 'orientation: $orientation, '
+ 'tilt: $tilt, '
+ 'platformData: $platformData, '
+ 'scrollDeltaX: $scrollDeltaX, '
+ 'scrollDeltaY: $scrollDeltaY'
+ ')';
}
}
/// A sequence of reports about the state of pointers.
class PointerDataPacket {
/// Creates a packet of pointer data reports.
- const PointerDataPacket({this.data = const <PointerData>[]}) : assert(data != null); // ignore: unnecessary_null_comparison
+ const PointerDataPacket({ this.data = const <PointerData>[] }) : assert(data != null); // ignore: unnecessary_null_comparison
/// Data about the individual pointers in this packet.
///
diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn
index 70e77a9..da29ead 100644
--- a/shell/platform/android/BUILD.gn
+++ b/shell/platform/android/BUILD.gn
@@ -140,6 +140,7 @@
"io/flutter/embedding/android/FlutterSurfaceView.java",
"io/flutter/embedding/android/FlutterTextureView.java",
"io/flutter/embedding/android/FlutterView.java",
+ "io/flutter/embedding/android/MotionEventTracker.java",
"io/flutter/embedding/android/RenderMode.java",
"io/flutter/embedding/android/SplashScreen.java",
"io/flutter/embedding/android/SplashScreenProvider.java",
diff --git a/shell/platform/android/io/flutter/embedding/android/AndroidTouchProcessor.java b/shell/platform/android/io/flutter/embedding/android/AndroidTouchProcessor.java
index 027b13c..a772d14 100644
--- a/shell/platform/android/io/flutter/embedding/android/AndroidTouchProcessor.java
+++ b/shell/platform/android/io/flutter/embedding/android/AndroidTouchProcessor.java
@@ -57,7 +57,7 @@
}
// Must match the unpacking code in hooks.dart.
- private static final int POINTER_DATA_FIELD_COUNT = 28;
+ private static final int POINTER_DATA_FIELD_COUNT = 29;
private static final int BYTES_PER_FIELD = 8;
// This value must match the value in framework's platform_view.dart.
@@ -65,6 +65,7 @@
private static final int POINTER_DATA_FLAG_BATCHED = 1;
@NonNull private final FlutterRenderer renderer;
+ @NonNull private final MotionEventTracker motionEventTracker;
private static final int _POINTER_BUTTON_PRIMARY = 1;
@@ -76,6 +77,7 @@
// FlutterRenderer
public AndroidTouchProcessor(@NonNull FlutterRenderer renderer) {
this.renderer = renderer;
+ this.motionEventTracker = MotionEventTracker.getInstance();
}
/** Sends the given {@link MotionEvent} data to Flutter in a format that Flutter understands. */
@@ -174,6 +176,8 @@
return;
}
+ MotionEventTracker.MotionEventId motionEventId = motionEventTracker.track(event);
+
int pointerKind = getPointerDeviceTypeForToolType(event.getToolType(pointerIndex));
int signalKind =
@@ -183,6 +187,7 @@
long timeStamp = event.getEventTime() * 1000; // Convert from milliseconds to microseconds.
+ packet.putLong(motionEventId.getId());
packet.putLong(timeStamp); // time_stamp
packet.putLong(pointerChange); // change
packet.putLong(pointerKind); // kind
diff --git a/shell/platform/android/io/flutter/embedding/android/MotionEventTracker.java b/shell/platform/android/io/flutter/embedding/android/MotionEventTracker.java
new file mode 100644
index 0000000..27d3dcf
--- /dev/null
+++ b/shell/platform/android/io/flutter/embedding/android/MotionEventTracker.java
@@ -0,0 +1,79 @@
+package io.flutter.embedding.android;
+
+import android.util.LongSparseArray;
+import android.view.MotionEvent;
+import androidx.annotation.Nullable;
+import java.util.PriorityQueue;
+import java.util.concurrent.atomic.AtomicLong;
+
+/** Tracks the motion events received by the FlutterView. */
+public final class MotionEventTracker {
+
+ /** Represents a unique identifier corresponding to a motion event. */
+ public static class MotionEventId {
+ private static final AtomicLong ID_COUNTER = new AtomicLong(0);
+ private final long id;
+
+ private MotionEventId(long id) {
+ this.id = id;
+ }
+
+ public static MotionEventId from(long id) {
+ return new MotionEventId(id);
+ }
+
+ public static MotionEventId createUnique() {
+ return MotionEventId.from(ID_COUNTER.incrementAndGet());
+ }
+
+ public long getId() {
+ return id;
+ }
+ }
+
+ private final LongSparseArray<MotionEvent> eventById;
+ private final PriorityQueue<Long> unusedEvents;
+ private static MotionEventTracker INSTANCE;
+
+ public static MotionEventTracker getInstance() {
+ if (INSTANCE == null) {
+ INSTANCE = new MotionEventTracker();
+ }
+ return INSTANCE;
+ }
+
+ private MotionEventTracker() {
+ eventById = new LongSparseArray<>();
+ unusedEvents = new PriorityQueue<>();
+ }
+
+ /** Tracks the event and returns a unique MotionEventId identifying the event. */
+ public MotionEventId track(MotionEvent event) {
+ MotionEventId eventId = MotionEventId.createUnique();
+ eventById.put(eventId.id, event);
+ unusedEvents.add(eventId.id);
+ return eventId;
+ }
+
+ /**
+ * Returns the MotionEvent corresponding to the eventId while discarding all the motion events
+ * that occured prior to the event represented by the eventId. Returns null if this event was
+ * popped or discarded.
+ */
+ @Nullable
+ public MotionEvent pop(MotionEventId eventId) {
+ // remove all the older events.
+ while (!unusedEvents.isEmpty() && unusedEvents.peek() < eventId.id) {
+ eventById.remove(unusedEvents.poll());
+ }
+
+ // remove the current event from the heap if it exists.
+ if (!unusedEvents.isEmpty() && unusedEvents.peek() == eventId.id) {
+ unusedEvents.poll();
+ }
+
+ MotionEvent event = eventById.get(eventId.id);
+ eventById.remove(eventId.id);
+ return event;
+ }
+}
diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java
index f5b5e51..3f238c9 100644
--- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java
+++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java
@@ -166,7 +166,8 @@
(int) args.get(11),
(int) args.get(12),
(int) args.get(13),
- (int) args.get(14));
+ (int) args.get(14),
+ ((Number) args.get(15)).longValue());
try {
handler.onTouch(touch);
@@ -380,6 +381,8 @@
public final int source;
/** TODO(mattcarroll): javadoc */
public final int flags;
+ /** TODO(iskakaushik): javadoc */
+ public final long motionEventId;
PlatformViewTouch(
int viewId,
@@ -396,7 +399,8 @@
int deviceId,
int edgeFlags,
int source,
- int flags) {
+ int flags,
+ long motionEventId) {
this.viewId = viewId;
this.downTime = downTime;
this.eventTime = eventTime;
@@ -412,6 +416,7 @@
this.edgeFlags = edgeFlags;
this.source = source;
this.flags = flags;
+ this.motionEventId = motionEventId;
}
}
}
diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java
index 61ceab0..3929698 100644
--- a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java
+++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java
@@ -21,6 +21,7 @@
import androidx.annotation.VisibleForTesting;
import io.flutter.embedding.android.FlutterImageView;
import io.flutter.embedding.android.FlutterView;
+import io.flutter.embedding.android.MotionEventTracker;
import io.flutter.embedding.engine.FlutterOverlaySurface;
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.embedding.engine.mutatorsstack.*;
@@ -93,6 +94,9 @@
// Platform view IDs that were displayed since the start of the current frame.
private HashSet<Integer> currentFrameUsedPlatformViewIds;
+ // Used to acquire the original motion events using the motionEventIds.
+ private final MotionEventTracker motionEventTracker;
+
private final PlatformViewsChannel.PlatformViewsHandler channelHandler =
new PlatformViewsChannel.PlatformViewsHandler() {
@@ -301,8 +305,16 @@
}
};
- private static MotionEvent toMotionEvent(
- float density, PlatformViewsChannel.PlatformViewTouch touch) {
+ private MotionEvent toMotionEvent(float density, PlatformViewsChannel.PlatformViewTouch touch) {
+ MotionEventTracker.MotionEventId motionEventId =
+ MotionEventTracker.MotionEventId.from(touch.motionEventId);
+ MotionEvent trackedEvent = motionEventTracker.pop(motionEventId);
+ if (trackedEvent != null) {
+ return trackedEvent;
+ }
+
+ // TODO (kaushikiska) : warn that we are potentially using an untracked
+ // event in the platform views.
PointerProperties[] pointerProperties =
parsePointerPropertiesList(touch.rawPointerPropertiesList)
.toArray(new PointerProperties[touch.pointerCount]);
@@ -339,6 +351,8 @@
platformViewRequests = new SparseArray<>();
platformViews = new SparseArray<>();
mutatorViews = new SparseArray<>();
+
+ motionEventTracker = MotionEventTracker.getInstance();
}
/**
diff --git a/shell/platform/embedder/embedder.cc b/shell/platform/embedder/embedder.cc
index 6e2f5bc..c9b3589 100644
--- a/shell/platform/embedder/embedder.cc
+++ b/shell/platform/embedder/embedder.cc
@@ -1225,6 +1225,8 @@
for (size_t i = 0; i < events_count; ++i) {
flutter::PointerData pointer_data;
pointer_data.Clear();
+ // this is currely in use only on android embedding.
+ pointer_data.embedder_id = 0;
pointer_data.time_stamp = SAFE_ACCESS(current, timestamp, 0);
pointer_data.change = ToPointerDataChange(
SAFE_ACCESS(current, phase, FlutterPointerPhase::kCancel));