Flutter Engine
The Flutter Engine
AndroidTouchProcessor.java
Go to the documentation of this file.
1package io.flutter.embedding.android;
2
3import static io.flutter.Build.API_LEVELS;
4
5import android.annotation.TargetApi;
6import android.content.Context;
7import android.graphics.Matrix;
8import android.os.Build;
9import android.util.TypedValue;
10import android.view.InputDevice;
11import android.view.MotionEvent;
12import android.view.ViewConfiguration;
13import androidx.annotation.IntDef;
14import androidx.annotation.NonNull;
15import androidx.annotation.VisibleForTesting;
16import io.flutter.embedding.engine.renderer.FlutterRenderer;
17import java.nio.ByteBuffer;
18import java.nio.ByteOrder;
19import java.util.HashMap;
20import java.util.Map;
21
22/** Sends touch information from Android to Flutter in a format that Flutter understands. */
24 private static final String TAG = "AndroidTouchProcessor";
25 // Must match the PointerChange enum in pointer.dart.
26 @IntDef({
36 PointerChange.PAN_ZOOM_END
37 })
38 public @interface PointerChange {
39 int CANCEL = 0;
40 int ADD = 1;
41 int REMOVE = 2;
42 int HOVER = 3;
43 int DOWN = 4;
44 int MOVE = 5;
45 int UP = 6;
46 int PAN_ZOOM_START = 7;
47 int PAN_ZOOM_UPDATE = 8;
48 int PAN_ZOOM_END = 9;
49 }
50
51 // Must match the PointerDeviceKind enum in pointer.dart.
52 @IntDef({
58 PointerDeviceKind.UNKNOWN
59 })
60 public @interface PointerDeviceKind {
61 int TOUCH = 0;
62 int MOUSE = 1;
63 int STYLUS = 2;
64 int INVERTED_STYLUS = 3;
65 int TRACKPAD = 4;
66 int UNKNOWN = 5;
67 }
68
69 // Must match the PointerSignalKind enum in pointer.dart.
70 @IntDef({
75 PointerSignalKind.UNKNOWN
76 })
77 public @interface PointerSignalKind {
78 int NONE = 0;
79 int SCROLL = 1;
80 int SCROLL_INERTIA_CANCEL = 2;
81 int SCALE = 3;
82 int UNKNOWN = 4;
83 }
84
85 // This value must match kPointerDataFieldCount in pointer_data.cc. (The
86 // pointer_data.cc also lists other locations that must be kept consistent.)
87 private static final int POINTER_DATA_FIELD_COUNT = 36;
88 @VisibleForTesting static final int BYTES_PER_FIELD = 8;
89
90 // Default if context is null, chosen to ensure reasonable speed scrolling.
91 @VisibleForTesting static final int DEFAULT_VERTICAL_SCROLL_FACTOR = 48;
92 @VisibleForTesting static final int DEFAULT_HORIZONTAL_SCROLL_FACTOR = 48;
93
94 // This value must match the value in framework's platform_view.dart.
95 // This flag indicates whether the original Android pointer events were batched together.
96 private static final int POINTER_DATA_FLAG_BATCHED = 1;
97
98 // The view ID for the only view in a single-view Flutter app.
99 private static final int IMPLICIT_VIEW_ID = 0;
100
101 @NonNull private final FlutterRenderer renderer;
102 @NonNull private final MotionEventTracker motionEventTracker;
103
104 private static final Matrix IDENTITY_TRANSFORM = new Matrix();
105
106 private final boolean trackMotionEvents;
107
108 private final Map<Integer, float[]> ongoingPans = new HashMap<>();
109
110 // Only used on api 25 and below to avoid requerying display metrics.
111 private int cachedVerticalScrollFactor;
112
113 /**
114 * Constructs an {@code AndroidTouchProcessor} that will send touch event data to the Flutter
115 * execution context represented by the given {@link FlutterRenderer}.
116 *
117 * @param renderer The object that manages textures for rendering.
118 * @param trackMotionEvents This is used to query motion events when platform views are rendered.
119 */
120 // TODO(mattcarroll): consider moving packet behavior to a FlutterInteractionSurface instead of
121 // FlutterRenderer
122 public AndroidTouchProcessor(@NonNull FlutterRenderer renderer, boolean trackMotionEvents) {
123 this.renderer = renderer;
124 this.motionEventTracker = MotionEventTracker.getInstance();
125 this.trackMotionEvents = trackMotionEvents;
126 }
127
128 public boolean onTouchEvent(@NonNull MotionEvent event) {
129 return onTouchEvent(event, IDENTITY_TRANSFORM);
130 }
131
132 /**
133 * Sends the given {@link MotionEvent} data to Flutter in a format that Flutter understands.
134 *
135 * @param event The motion event from the view.
136 * @param transformMatrix Applies to the view that originated the event. It's used to transform
137 * the gesture pointers into screen coordinates.
138 * @return True if the event was handled.
139 */
140 public boolean onTouchEvent(@NonNull MotionEvent event, @NonNull Matrix transformMatrix) {
141 int pointerCount = event.getPointerCount();
142
143 // The following packing code must match the struct in pointer_data.h.
144
145 // Prepare a data packet of the appropriate size and order.
146 ByteBuffer packet =
147 ByteBuffer.allocateDirect(pointerCount * POINTER_DATA_FIELD_COUNT * BYTES_PER_FIELD);
148 packet.order(ByteOrder.LITTLE_ENDIAN);
149
150 int maskedAction = event.getActionMasked();
151 int pointerChange = getPointerChangeForAction(event.getActionMasked());
152 boolean updateForSinglePointer =
153 maskedAction == MotionEvent.ACTION_DOWN || maskedAction == MotionEvent.ACTION_POINTER_DOWN;
154 boolean updateForMultiplePointers =
155 !updateForSinglePointer
156 && (maskedAction == MotionEvent.ACTION_UP
157 || maskedAction == MotionEvent.ACTION_POINTER_UP);
158 if (updateForSinglePointer) {
159 // ACTION_DOWN and ACTION_POINTER_DOWN always apply to a single pointer only.
160 addPointerForIndex(event, event.getActionIndex(), pointerChange, 0, transformMatrix, packet);
161 } else if (updateForMultiplePointers) {
162 // ACTION_UP and ACTION_POINTER_UP may contain position updates for other pointers.
163 // We are converting these updates to move events here in order to preserve this data.
164 // We also mark these events with a flag in order to help the framework reassemble
165 // the original Android event later, should it need to forward it to a PlatformView.
166 for (int p = 0; p < pointerCount; p++) {
167 if (p != event.getActionIndex() && event.getToolType(p) == MotionEvent.TOOL_TYPE_FINGER) {
168 addPointerForIndex(
169 event, p, PointerChange.MOVE, POINTER_DATA_FLAG_BATCHED, transformMatrix, packet);
170 }
171 }
172 // It's important that we're sending the UP event last. This allows PlatformView
173 // to correctly batch everything back into the original Android event if needed.
174 addPointerForIndex(event, event.getActionIndex(), pointerChange, 0, transformMatrix, packet);
175 } else {
176 // ACTION_MOVE may not actually mean all pointers have moved
177 // but it's the responsibility of a later part of the system to
178 // ignore 0-deltas if desired.
179 for (int p = 0; p < pointerCount; p++) {
180 addPointerForIndex(event, p, pointerChange, 0, transformMatrix, packet);
181 }
182 }
183
184 // Verify that the packet is the expected size.
185 if (packet.position() % (POINTER_DATA_FIELD_COUNT * BYTES_PER_FIELD) != 0) {
186 throw new AssertionError("Packet position is not on field boundary");
187 }
188
189 // Send the packet to Flutter.
190 renderer.dispatchPointerDataPacket(packet, packet.position());
191
192 return true;
193 }
194
195 /**
196 * Sends the given generic {@link MotionEvent} data to Flutter in a format that Flutter
197 * understands.
198 *
199 * <p>Generic motion events include joystick movement, mouse hover, track pad touches, scroll
200 * wheel movements, etc.
201 *
202 * @param event The generic motion event being processed.
203 * @param context For use by ViewConfiguration.get(context) to scale input.
204 * @return True if the event was handled.
205 */
206 public boolean onGenericMotionEvent(@NonNull MotionEvent event, @NonNull Context context) {
207 // Method isFromSource is only available in API 18+ (Jelly Bean MR2)
208 // Mouse hover support is not implemented for API < 18.
209 boolean isPointerEvent = event.isFromSource(InputDevice.SOURCE_CLASS_POINTER);
210 boolean isMovementEvent =
211 (event.getActionMasked() == MotionEvent.ACTION_HOVER_MOVE
212 || event.getActionMasked() == MotionEvent.ACTION_SCROLL);
213 if (isPointerEvent && isMovementEvent) {
214 // Continue.
215 } else {
216 return false;
217 }
218
219 int pointerChange = getPointerChangeForAction(event.getActionMasked());
220 ByteBuffer packet =
221 ByteBuffer.allocateDirect(
222 event.getPointerCount() * POINTER_DATA_FIELD_COUNT * BYTES_PER_FIELD);
223 packet.order(ByteOrder.LITTLE_ENDIAN);
224
225 // ACTION_HOVER_MOVE always applies to a single pointer only.
226 addPointerForIndex(
227 event, event.getActionIndex(), pointerChange, 0, IDENTITY_TRANSFORM, packet, context);
228 if (packet.position() % (POINTER_DATA_FIELD_COUNT * BYTES_PER_FIELD) != 0) {
229 throw new AssertionError("Packet position is not on field boundary.");
230 }
231 renderer.dispatchPointerDataPacket(packet, packet.position());
232 return true;
233 }
234
235 /// Calls addPointerForIndex with null for context.
236 ///
237 /// Without context the scroll wheel will not mimick android's scroll speed.
238 private void addPointerForIndex(
239 MotionEvent event,
240 int pointerIndex,
241 int pointerChange,
242 int pointerData,
243 Matrix transformMatrix,
244 ByteBuffer packet) {
245 addPointerForIndex(
246 event, pointerIndex, pointerChange, pointerData, transformMatrix, packet, null);
247 }
248
249 // TODO: consider creating a PointerPacket class instead of using a procedure that
250 // mutates inputs. https://github.com/flutter/flutter/issues/132853
251 private void addPointerForIndex(
252 MotionEvent event,
253 int pointerIndex,
254 int pointerChange,
255 int pointerData,
256 Matrix transformMatrix,
257 ByteBuffer packet,
258 Context context) {
259 if (pointerChange == -1) {
260 return;
261 }
262 // TODO(dkwingsmt): Use the correct source view ID once Android supports
263 // multiple views.
264 // https://github.com/flutter/flutter/issues/134405
265 final int viewId = IMPLICIT_VIEW_ID;
266 final int pointerId = event.getPointerId(pointerIndex);
267
268 int pointerKind = getPointerDeviceTypeForToolType(event.getToolType(pointerIndex));
269 // We use this in lieu of using event.getRawX and event.getRawY as we wish to support
270 // earlier versions than API level 29.
271 float viewToScreenCoords[] = {event.getX(pointerIndex), event.getY(pointerIndex)};
272 transformMatrix.mapPoints(viewToScreenCoords);
273 long buttons;
274 if (pointerKind == PointerDeviceKind.MOUSE) {
275 buttons = event.getButtonState() & 0x1F;
276 if (buttons == 0
277 && event.getSource() == InputDevice.SOURCE_MOUSE
278 && pointerChange == PointerChange.DOWN) {
279 // Some implementations translate trackpad scrolling into a mouse down-move-up event
280 // sequence with buttons: 0, such as ARC on a Chromebook. See #11420, a legacy
281 // implementation that uses the same condition but converts differently.
282 ongoingPans.put(pointerId, viewToScreenCoords);
283 }
284 } else if (pointerKind == PointerDeviceKind.STYLUS) {
285 // Returns converted android button state into flutter framework normalized state
286 // and updates ongoingPans for chromebook trackpad scrolling.
287 // See
288 // https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/gestures/events.dart
289 // for target button constants.
290 buttons = (event.getButtonState() >> 4) & 0xF;
291 } else {
292 buttons = 0;
293 }
294
295 int panZoomType = -1;
296 boolean isTrackpadPan = ongoingPans.containsKey(pointerId);
297 if (isTrackpadPan) {
298 panZoomType = getPointerChangeForPanZoom(pointerChange);
299 if (panZoomType == -1) {
300 return;
301 }
302 }
303
304 long motionEventId = 0;
305 if (trackMotionEvents) {
306 MotionEventTracker.MotionEventId trackedEvent = motionEventTracker.track(event);
307 motionEventId = trackedEvent.getId();
308 }
309
310 int signalKind =
311 event.getActionMasked() == MotionEvent.ACTION_SCROLL
312 ? PointerSignalKind.SCROLL
313 : PointerSignalKind.NONE;
314
315 long timeStamp = event.getEventTime() * 1000; // Convert from milliseconds to microseconds.
316
317 packet.putLong(motionEventId); // motionEventId
318 packet.putLong(timeStamp); // time_stamp
319 if (isTrackpadPan) {
320 packet.putLong(panZoomType); // change
321 packet.putLong(PointerDeviceKind.TRACKPAD); // kind
322 } else {
323 packet.putLong(pointerChange); // change
324 packet.putLong(pointerKind); // kind
325 }
326 packet.putLong(signalKind); // signal_kind
327 packet.putLong(pointerId); // device
328 packet.putLong(0); // pointer_identifier, will be generated in pointer_data_packet_converter.cc.
329
330 if (isTrackpadPan) {
331 float[] panStart = ongoingPans.get(pointerId);
332 packet.putDouble(panStart[0]); // physical_x
333 packet.putDouble(panStart[1]); // physical_y
334 } else {
335 packet.putDouble(viewToScreenCoords[0]); // physical_x
336 packet.putDouble(viewToScreenCoords[1]); // physical_y
337 }
338
339 packet.putDouble(
340 0.0); // physical_delta_x, will be generated in pointer_data_packet_converter.cc.
341 packet.putDouble(
342 0.0); // physical_delta_y, will be generated in pointer_data_packet_converter.cc.
343
344 packet.putLong(buttons); // buttons
345
346 packet.putLong(0); // obscured
347
348 packet.putLong(0); // synthesized
349
350 packet.putDouble(event.getPressure(pointerIndex)); // pressure
351 double pressureMin = 0.0;
352 double pressureMax = 1.0;
353 if (event.getDevice() != null) {
354 InputDevice.MotionRange pressureRange =
355 event.getDevice().getMotionRange(MotionEvent.AXIS_PRESSURE);
356 if (pressureRange != null) {
357 pressureMin = pressureRange.getMin();
358 pressureMax = pressureRange.getMax();
359 }
360 }
361 packet.putDouble(pressureMin); // pressure_min
362 packet.putDouble(pressureMax); // pressure_max
363
364 if (pointerKind == PointerDeviceKind.STYLUS) {
365 packet.putDouble(event.getAxisValue(MotionEvent.AXIS_DISTANCE, pointerIndex)); // distance
366 packet.putDouble(0.0); // distance_max
367 } else {
368 packet.putDouble(0.0); // distance
369 packet.putDouble(0.0); // distance_max
370 }
371
372 packet.putDouble(event.getSize(pointerIndex)); // size
373
374 packet.putDouble(event.getToolMajor(pointerIndex)); // radius_major
375 packet.putDouble(event.getToolMinor(pointerIndex)); // radius_minor
376
377 packet.putDouble(0.0); // radius_min
378 packet.putDouble(0.0); // radius_max
379
380 packet.putDouble(event.getAxisValue(MotionEvent.AXIS_ORIENTATION, pointerIndex)); // orientation
381
382 if (pointerKind == PointerDeviceKind.STYLUS) {
383 packet.putDouble(event.getAxisValue(MotionEvent.AXIS_TILT, pointerIndex)); // tilt
384 } else {
385 packet.putDouble(0.0); // tilt
386 }
387
388 packet.putLong(pointerData); // platformData
389
390 // See android scrollview for inspiration.
391 // https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/widget/ScrollView.java?q=function:onGenericMotionEvent%20filepath:widget%2FScrollView.java&ss=android%2Fplatform%2Fsuperproject%2Fmain
392 if (signalKind == PointerSignalKind.SCROLL) {
393 double horizontalScaleFactor = DEFAULT_HORIZONTAL_SCROLL_FACTOR;
394 double verticalScaleFactor = DEFAULT_VERTICAL_SCROLL_FACTOR;
395 if (context != null) {
396 horizontalScaleFactor = getHorizontalScrollFactor(context);
397 verticalScaleFactor = getVerticalScrollFactor(context);
398 }
399 // We flip the sign of the scroll value below because it aligns the pixel value with the
400 // scroll direction in native android.
401 final double horizontalScrollPixels =
402 horizontalScaleFactor * -event.getAxisValue(MotionEvent.AXIS_HSCROLL, pointerIndex);
403 final double verticalScrollPixels =
404 verticalScaleFactor * -event.getAxisValue(MotionEvent.AXIS_VSCROLL, pointerIndex);
405 packet.putDouble(horizontalScrollPixels); // scroll_delta_x
406 packet.putDouble(verticalScrollPixels); // scroll_delta_y
407 } else {
408 packet.putDouble(0.0); // scroll_delta_x
409 packet.putDouble(0.0); // scroll_delta_y
410 }
411
412 if (isTrackpadPan) {
413 float[] panStart = ongoingPans.get(pointerId);
414 packet.putDouble(viewToScreenCoords[0] - panStart[0]);
415 packet.putDouble(viewToScreenCoords[1] - panStart[1]);
416 } else {
417 packet.putDouble(0.0); // pan_x
418 packet.putDouble(0.0); // pan_y
419 }
420 packet.putDouble(0.0); // pan_delta_x
421 packet.putDouble(0.0); // pan_delta_y
422 packet.putDouble(1.0); // scale
423 packet.putDouble(0.0); // rotation
424 packet.putLong(viewId); // view_id
425
426 if (isTrackpadPan && (panZoomType == PointerChange.PAN_ZOOM_END)) {
427 ongoingPans.remove(pointerId);
428 }
429 }
430
431 private float getHorizontalScrollFactor(@NonNull Context context) {
432 if (Build.VERSION.SDK_INT >= API_LEVELS.API_26) {
433 return ViewConfiguration.get(context).getScaledHorizontalScrollFactor();
434 } else {
435 // Vertical scroll factor is not a typo. This is what View.java does in android.
436 return getVerticalScrollFactorPre26(context);
437 }
438 }
439
440 private float getVerticalScrollFactor(@NonNull Context context) {
441 if (Build.VERSION.SDK_INT >= API_LEVELS.API_26) {
442 return getVerticalScrollFactorAbove26(context);
443 } else {
444 return getVerticalScrollFactorPre26(context);
445 }
446 }
447
448 @TargetApi(API_LEVELS.API_26)
449 private float getVerticalScrollFactorAbove26(@NonNull Context context) {
450 return ViewConfiguration.get(context).getScaledVerticalScrollFactor();
451 }
452
453 // See
454 // https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/view/View.java?q=function:getVerticalScrollFactor%20filepath:android%2Fview%2FView.java&ss=android%2Fplatform%2Fsuperproject%2Fmain
455 private int getVerticalScrollFactorPre26(@NonNull Context context) {
456 if (cachedVerticalScrollFactor == 0) {
457 TypedValue outValue = new TypedValue();
458 if (!context
459 .getTheme()
460 .resolveAttribute(android.R.attr.listPreferredItemHeight, outValue, true)) {
462 }
463 cachedVerticalScrollFactor =
464 (int) outValue.getDimension(context.getResources().getDisplayMetrics());
465 }
466 return cachedVerticalScrollFactor;
467 }
468
469 @PointerChange
470 private int getPointerChangeForAction(int maskedAction) {
471 // Primary pointer:
472 if (maskedAction == MotionEvent.ACTION_DOWN) {
473 return PointerChange.DOWN;
474 }
475 if (maskedAction == MotionEvent.ACTION_UP) {
476 return PointerChange.UP;
477 }
478 // Secondary pointer:
479 if (maskedAction == MotionEvent.ACTION_POINTER_DOWN) {
480 return PointerChange.DOWN;
481 }
482 if (maskedAction == MotionEvent.ACTION_POINTER_UP) {
483 return PointerChange.UP;
484 }
485 // All pointers:
486 if (maskedAction == MotionEvent.ACTION_MOVE) {
487 return PointerChange.MOVE;
488 }
489 if (maskedAction == MotionEvent.ACTION_HOVER_MOVE) {
490 return PointerChange.HOVER;
491 }
492 if (maskedAction == MotionEvent.ACTION_CANCEL) {
493 return PointerChange.CANCEL;
494 }
495 if (maskedAction == MotionEvent.ACTION_SCROLL) {
496 return PointerChange.HOVER;
497 }
498 return -1;
499 }
500
501 @PointerChange
502 private int getPointerChangeForPanZoom(int pointerChange) {
503 if (pointerChange == PointerChange.DOWN) {
504 return PointerChange.PAN_ZOOM_START;
505 } else if (pointerChange == PointerChange.MOVE) {
506 return PointerChange.PAN_ZOOM_UPDATE;
507 } else if (pointerChange == PointerChange.UP || pointerChange == PointerChange.CANCEL) {
508 return PointerChange.PAN_ZOOM_END;
509 }
510 return -1;
511 }
512
513 @PointerDeviceKind
514 private int getPointerDeviceTypeForToolType(int toolType) {
515 switch (toolType) {
516 case MotionEvent.TOOL_TYPE_FINGER:
517 return PointerDeviceKind.TOUCH;
518 case MotionEvent.TOOL_TYPE_STYLUS:
519 return PointerDeviceKind.STYLUS;
520 case MotionEvent.TOOL_TYPE_MOUSE:
521 return PointerDeviceKind.MOUSE;
522 case MotionEvent.TOOL_TYPE_ERASER:
523 return PointerDeviceKind.INVERTED_STYLUS;
524 default:
525 // MotionEvent.TOOL_TYPE_UNKNOWN will reach here.
526 return PointerDeviceKind.UNKNOWN;
527 }
528 }
529}
@ SCALE
Definition: SkPictureFlat.h:65
static const int MOVE
boolean onTouchEvent(@NonNull MotionEvent event, @NonNull Matrix transformMatrix)
boolean onGenericMotionEvent(@NonNull MotionEvent event, @NonNull Context context)
AndroidTouchProcessor(@NonNull FlutterRenderer renderer, boolean trackMotionEvents)
FlKeyEvent * event
def Build(configs, env, options)
Definition: build.py:232
SK_API sk_sp< PrecompileColorFilter > Matrix()