Skip to content

Commit 60be753

Browse files
[Android] Encode the original pointer count in messages that represent Android touch events (#178015)
Android touch events include updates to multiple pointers, but each pointer data message sent from the embedder to the framework represents a single pointer. So the Android embedder will send multiple messages for each touch event, and the framework's AndroidViewController will reassemble the messages and forward the resulting event to the platform view. The AndroidViewController tracks the number of active pointers in its own local state. If that state is out of sync with the event handled by the Android embedder, then the AndroidViewController may send duplicate events to the platform view. This PR encodes the Android touch event's pointer count in the messages sent to the framework. This allows the AndroidViewController to reliably determine whether it has received all of the pointer messages that originated from an event. Fixes flutter/flutter#176574
1 parent 4a19bff commit 60be753

3 files changed

Lines changed: 84 additions & 8 deletions

File tree

engine/src/flutter/shell/platform/android/io/flutter/embedding/android/AndroidTouchProcessor.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,15 @@ public class AndroidTouchProcessor {
106106
@VisibleForTesting static final int DEFAULT_VERTICAL_SCROLL_FACTOR = 48;
107107
@VisibleForTesting static final int DEFAULT_HORIZONTAL_SCROLL_FACTOR = 48;
108108

109-
// This value must match the value in framework's platform_view.dart.
109+
// These values must match the values in the framework's platform_views.dart.
110110
// This flag indicates whether the original Android pointer events were batched together.
111111
private static final int POINTER_DATA_FLAG_BATCHED = 1;
112+
// This flag indicates that this message is part of a group of messages representing
113+
// a change that affects multiple pointers.
114+
private static final int POINTER_DATA_FLAG_MULTIPLE = 2;
115+
116+
// Bit shift for encoding the pointer count when using POINTER_DATA_FLAG_MULTIPLE
117+
private static final int POINTER_DATA_MULTIPLE_POINTER_COUNT_SHIFT = 8;
112118

113119
// The view ID for the only view in a single-view Flutter app.
114120
private static final int IMPLICIT_VIEW_ID = 0;
@@ -212,7 +218,10 @@ public boolean onTouchEvent(@NonNull MotionEvent event, @NonNull Matrix transfor
212218
// but it's the responsibility of a later part of the system to
213219
// ignore 0-deltas if desired.
214220
for (int p = 0; p < originalPointerCount; p++) {
215-
addPointerForIndex(event, p, pointerChange, 0, transformMatrix, packet);
221+
int pointerData =
222+
POINTER_DATA_FLAG_MULTIPLE
223+
| (originalPointerCount << POINTER_DATA_MULTIPLE_POINTER_COUNT_SHIFT);
224+
addPointerForIndex(event, p, pointerChange, pointerData, transformMatrix, packet);
216225
}
217226
}
218227

packages/flutter/lib/src/services/platform_views.dart

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -619,19 +619,32 @@ class _AndroidMotionEventConverter {
619619
final int pointerIdx = pointers.indexOf(event.pointer);
620620
final int numPointers = pointers.length;
621621

622-
// This value must match the value in engine's FlutterView.java.
622+
// These values must match the values in the engine's AndroidTouchProcessor.java.
623623
// This flag indicates whether the original Android pointer events were batched together.
624624
const int kPointerDataFlagBatched = 1;
625+
// This flag indicates that this event is part of a group of events representing a change
626+
// that affects multiple pointers.
627+
const int kPointerDataFlagMultiple = 2;
628+
629+
// Mask for extracting the flag value from the event's platformData
630+
const int kPointerDataFlagMask = 0xff;
631+
const int kPointerDataMultiplePointerCountShift = 8;
625632

626633
// Android MotionEvent objects can batch information on multiple pointers.
627634
// Flutter breaks these such batched events into multiple PointerEvent objects.
628635
// When there are multiple active pointers we accumulate the information for all pointers
629636
// as we get PointerEvents, and only send it to the embedded Android view when
630637
// we see the last pointer. This way we achieve the same batching as Android.
631-
if (event.platformData == kPointerDataFlagBatched ||
632-
(isSinglePointerAction(event) && pointerIdx < numPointers - 1)) {
638+
final int platformDataFlag = event.platformData & kPointerDataFlagMask;
639+
if (platformDataFlag == kPointerDataFlagBatched) {
633640
return null;
634641
}
642+
if (platformDataFlag == kPointerDataFlagMultiple) {
643+
final int originalPointerCount = event.platformData >> kPointerDataMultiplePointerCountShift;
644+
if (pointerIdx != originalPointerCount - 1) {
645+
return null;
646+
}
647+
}
635648

636649
final int? action = switch (event) {
637650
PointerDownEvent() when numPointers == 1 => AndroidViewController.kActionDown,
@@ -697,9 +710,6 @@ class _AndroidMotionEventConverter {
697710
},
698711
);
699712
}
700-
701-
bool isSinglePointerAction(PointerEvent event) =>
702-
event is! PointerDownEvent && event is! PointerUpEvent;
703713
}
704714

705715
class _CreationParams {

packages/flutter/test/services/platform_views_test.dart

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'package:flutter/gestures.dart';
56
import 'package:flutter/services.dart';
67
import 'package:flutter_test/flutter_test.dart';
78

@@ -439,6 +440,62 @@ void main() {
439440
await viewController.setOffset(const Offset(10, 20));
440441
expect(viewsController.offsets, equals(<int, Offset>{}));
441442
});
443+
444+
testWidgets('motion event converter does not duplicate move events', (
445+
WidgetTester tester,
446+
) async {
447+
final List<MethodCall> log = <MethodCall>[];
448+
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
449+
SystemChannels.platform_views,
450+
(MethodCall methodCall) async {
451+
log.add(methodCall);
452+
return null;
453+
},
454+
);
455+
456+
final AndroidViewController viewController = PlatformViewsService.initSurfaceAndroidView(
457+
id: 7,
458+
viewType: 'web',
459+
layoutDirection: TextDirection.ltr,
460+
);
461+
viewController.pointTransformer = (Offset offset) => offset;
462+
463+
const int pointerCount = 10;
464+
for (int i = 0; i < pointerCount; i++) {
465+
final PointerEvent event = PointerDownEvent(
466+
timeStamp: const Duration(milliseconds: 1),
467+
pointer: i,
468+
);
469+
viewController.dispatchPointerEvent(event);
470+
}
471+
472+
// Pointer event platform data constant from _AndroidMotionEventConverter
473+
const int kPointerDataFlagMultiple = 2;
474+
475+
for (int i = 0; i < pointerCount; i++) {
476+
final PointerEvent event = PointerMoveEvent(
477+
timeStamp: const Duration(milliseconds: 2),
478+
pointer: i,
479+
platformData: kPointerDataFlagMultiple | (pointerCount << 8),
480+
);
481+
viewController.dispatchPointerEvent(event);
482+
}
483+
484+
// Indexes in the list returned by AndroidMotionEvent._asList
485+
const int kAndroidMotionEventListIndexAction = 3;
486+
const int kAndroidMotionEventListIndexPointerCount = 4;
487+
488+
final List<MethodCall> moveCalls = log.where((MethodCall call) {
489+
final List<dynamic> args = call.arguments as List<dynamic>;
490+
return call.method == 'touch' &&
491+
args[kAndroidMotionEventListIndexAction] == AndroidViewController.kActionMove;
492+
}).toList();
493+
494+
// The _AndroidMotionEventConverter should yield one touch event containing all of the pointers.
495+
expect(moveCalls.length, equals(1));
496+
final List<dynamic> moveArgs = moveCalls.single.arguments as List<dynamic>;
497+
expect(moveArgs[kAndroidMotionEventListIndexPointerCount], equals(pointerCount));
498+
});
442499
});
443500

444501
group('iOS', () {

0 commit comments

Comments
 (0)