Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 28b8bd5

Browse files
authored
i82973 scroll mouse wheel support (#44724)
Fixes flutter/flutter#82973 Mouse scroll wheel support for android. I chose to not cache the vertical and horizontal scale factors that come from view configuration. The primary reason is that in the current code path context is only used when the user scrolls which was the unimplemented feature. This smaller blast radius I decided was worth the additional calls. A secondary reason is that when the scale factors are changed is not a well documented path nor is there a lifecycle callback to listen to. Scroll factor is cached on api 25 and below because that more closely mirrors the behavior I see in pre 25 versions of android scroll view. Note flutter takes longer to "see" a mouse that then scrolls than android Fixes #flutter/flutter/82973 Todo list prior to merge ## Links * Some pre api 26 scroll factor code - 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 - https://cs.android.com/android/_/android/platform/frameworks/base/+/main:core/java/android/widget/ScrollView.java;l=930;drc=2fe301db7555bccf53e465436d4cb7442c803df3;bpv=0;bpt=0 * Post api 26 scroll factor code - https://cs.android.com/android/_/android/platform/frameworks/base/+/main:core/java/android/widget/ScrollView.java;l=361;drc=406e0f655387f27dda874c2b975fb0ddbd61aa13;bpv=0;bpt=0 - https://developer.android.com/reference/android/view/ViewConfiguration#getScaledVerticalScrollFactor()
1 parent 21437d3 commit 28b8bd5

File tree

4 files changed

+554
-25
lines changed

4 files changed

+554
-25
lines changed

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

Lines changed: 104 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
package io.flutter.embedding.android;
22

3+
import android.annotation.TargetApi;
4+
import android.content.Context;
35
import android.graphics.Matrix;
46
import android.os.Build;
7+
import android.util.TypedValue;
58
import android.view.InputDevice;
69
import android.view.MotionEvent;
10+
import android.view.ViewConfiguration;
711
import androidx.annotation.IntDef;
812
import androidx.annotation.NonNull;
913
import androidx.annotation.VisibleForTesting;
@@ -80,6 +84,10 @@ public class AndroidTouchProcessor {
8084
private static final int POINTER_DATA_FIELD_COUNT = 35;
8185
@VisibleForTesting static final int BYTES_PER_FIELD = 8;
8286

87+
// Default if context is null, chosen to ensure reasonable speed scrolling.
88+
@VisibleForTesting static final int DEFAULT_VERTICAL_SCROLL_FACTOR = 48;
89+
@VisibleForTesting static final int DEFAULT_HORIZONTAL_SCROLL_FACTOR = 48;
90+
8391
// This value must match the value in framework's platform_view.dart.
8492
// This flag indicates whether the original Android pointer events were batched together.
8593
private static final int POINTER_DATA_FLAG_BATCHED = 1;
@@ -93,6 +101,9 @@ public class AndroidTouchProcessor {
93101

94102
private final Map<Integer, float[]> ongoingPans = new HashMap<>();
95103

104+
// Only used on api 25 and below to avoid requerying display metrics.
105+
private int cachedVerticalScrollFactor;
106+
96107
/**
97108
* Constructs an {@code AndroidTouchProcessor} that will send touch event data to the Flutter
98109
* execution context represented by the given {@link FlutterRenderer}.
@@ -181,9 +192,10 @@ public boolean onTouchEvent(@NonNull MotionEvent event, @NonNull Matrix transfor
181192
* wheel movements, etc.
182193
*
183194
* @param event The generic motion event being processed.
195+
* @param context For use by ViewConfiguration.get(context) to scale input.
184196
* @return True if the event was handled.
185197
*/
186-
public boolean onGenericMotionEvent(@NonNull MotionEvent event) {
198+
public boolean onGenericMotionEvent(@NonNull MotionEvent event, @NonNull Context context) {
187199
// Method isFromSource is only available in API 18+ (Jelly Bean MR2)
188200
// Mouse hover support is not implemented for API < 18.
189201
boolean isPointerEvent =
@@ -192,7 +204,9 @@ public boolean onGenericMotionEvent(@NonNull MotionEvent event) {
192204
boolean isMovementEvent =
193205
(event.getActionMasked() == MotionEvent.ACTION_HOVER_MOVE
194206
|| event.getActionMasked() == MotionEvent.ACTION_SCROLL);
195-
if (!isPointerEvent || !isMovementEvent) {
207+
if (isPointerEvent && isMovementEvent) {
208+
// Continue.
209+
} else {
196210
return false;
197211
}
198212

@@ -203,26 +217,43 @@ public boolean onGenericMotionEvent(@NonNull MotionEvent event) {
203217
packet.order(ByteOrder.LITTLE_ENDIAN);
204218

205219
// ACTION_HOVER_MOVE always applies to a single pointer only.
206-
addPointerForIndex(event, event.getActionIndex(), pointerChange, 0, IDENTITY_TRANSFORM, packet);
220+
addPointerForIndex(
221+
event, event.getActionIndex(), pointerChange, 0, IDENTITY_TRANSFORM, packet, context);
207222
if (packet.position() % (POINTER_DATA_FIELD_COUNT * BYTES_PER_FIELD) != 0) {
208223
throw new AssertionError("Packet position is not on field boundary.");
209224
}
210225
renderer.dispatchPointerDataPacket(packet, packet.position());
211226
return true;
212227
}
213228

214-
// TODO(mattcarroll): consider creating a PointerPacket class instead of using a procedure that
215-
// mutates inputs.
229+
/// Calls addPointerForIndex with null for context.
230+
///
231+
/// Without context the scroll wheel will not mimick android's scroll speed.
216232
private void addPointerForIndex(
217233
MotionEvent event,
218234
int pointerIndex,
219235
int pointerChange,
220236
int pointerData,
221237
Matrix transformMatrix,
222238
ByteBuffer packet) {
239+
addPointerForIndex(
240+
event, pointerIndex, pointerChange, pointerData, transformMatrix, packet, null);
241+
}
242+
243+
// TODO: consider creating a PointerPacket class instead of using a procedure that
244+
// mutates inputs. https://github.com/flutter/flutter/issues/132853
245+
private void addPointerForIndex(
246+
MotionEvent event,
247+
int pointerIndex,
248+
int pointerChange,
249+
int pointerData,
250+
Matrix transformMatrix,
251+
ByteBuffer packet,
252+
Context context) {
223253
if (pointerChange == -1) {
224254
return;
225255
}
256+
final int pointerId = event.getPointerId(pointerIndex);
226257

227258
int pointerKind = getPointerDeviceTypeForToolType(event.getToolType(pointerIndex));
228259
// We use this in lieu of using event.getRawX and event.getRawY as we wish to support
@@ -238,16 +269,21 @@ private void addPointerForIndex(
238269
// Some implementations translate trackpad scrolling into a mouse down-move-up event
239270
// sequence with buttons: 0, such as ARC on a Chromebook. See #11420, a legacy
240271
// implementation that uses the same condition but converts differently.
241-
ongoingPans.put(event.getPointerId(pointerIndex), viewToScreenCoords);
272+
ongoingPans.put(pointerId, viewToScreenCoords);
242273
}
243274
} else if (pointerKind == PointerDeviceKind.STYLUS) {
275+
// Returns converted android button state into flutter framework normalized state
276+
// and updates ongoingPans for chromebook trackpad scrolling.
277+
// See
278+
// https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/gestures/events.dart
279+
// for target button constants.
244280
buttons = (event.getButtonState() >> 4) & 0xF;
245281
} else {
246282
buttons = 0;
247283
}
248284

249285
int panZoomType = -1;
250-
boolean isTrackpadPan = ongoingPans.containsKey(event.getPointerId(pointerIndex));
286+
boolean isTrackpadPan = ongoingPans.containsKey(pointerId);
251287
if (isTrackpadPan) {
252288
panZoomType = getPointerChangeForPanZoom(pointerChange);
253289
if (panZoomType == -1) {
@@ -278,13 +314,13 @@ private void addPointerForIndex(
278314
packet.putLong(pointerKind); // kind
279315
}
280316
packet.putLong(signalKind); // signal_kind
281-
packet.putLong(event.getPointerId(pointerIndex)); // device
317+
packet.putLong(pointerId); // device
282318
packet.putLong(0); // pointer_identifier, will be generated in pointer_data_packet_converter.cc.
283319

284320
if (isTrackpadPan) {
285-
float[] panStart = ongoingPans.get(event.getPointerId(pointerIndex));
286-
packet.putDouble(panStart[0]);
287-
packet.putDouble(panStart[1]);
321+
float[] panStart = ongoingPans.get(pointerId);
322+
packet.putDouble(panStart[0]); // physical_x
323+
packet.putDouble(panStart[1]); // physical_y
288324
} else {
289325
packet.putDouble(viewToScreenCoords[0]); // physical_x
290326
packet.putDouble(viewToScreenCoords[1]); // physical_y
@@ -341,16 +377,30 @@ private void addPointerForIndex(
341377

342378
packet.putLong(pointerData); // platformData
343379

380+
// See android scrollview for inspiration.
381+
// 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
344382
if (signalKind == PointerSignalKind.SCROLL) {
345-
packet.putDouble(-event.getAxisValue(MotionEvent.AXIS_HSCROLL)); // scroll_delta_x
346-
packet.putDouble(-event.getAxisValue(MotionEvent.AXIS_VSCROLL)); // scroll_delta_y
383+
double horizontalScaleFactor = DEFAULT_HORIZONTAL_SCROLL_FACTOR;
384+
double verticalScaleFactor = DEFAULT_VERTICAL_SCROLL_FACTOR;
385+
if (context != null) {
386+
horizontalScaleFactor = getHorizontalScrollFactor(context);
387+
verticalScaleFactor = getVerticalScrollFactor(context);
388+
}
389+
// We flip the sign of the scroll value below because it aligns the pixel value with the
390+
// scroll direction in native android.
391+
final double horizontalScrollPixels =
392+
horizontalScaleFactor * -event.getAxisValue(MotionEvent.AXIS_HSCROLL, pointerIndex);
393+
final double verticalScrollPixels =
394+
verticalScaleFactor * -event.getAxisValue(MotionEvent.AXIS_VSCROLL, pointerIndex);
395+
packet.putDouble(horizontalScrollPixels); // scroll_delta_x
396+
packet.putDouble(verticalScrollPixels); // scroll_delta_y
347397
} else {
348398
packet.putDouble(0.0); // scroll_delta_x
349-
packet.putDouble(0.0); // scroll_delta_x
399+
packet.putDouble(0.0); // scroll_delta_y
350400
}
351401

352402
if (isTrackpadPan) {
353-
float[] panStart = ongoingPans.get(event.getPointerId(pointerIndex));
403+
float[] panStart = ongoingPans.get(pointerId);
354404
packet.putDouble(viewToScreenCoords[0] - panStart[0]);
355405
packet.putDouble(viewToScreenCoords[1] - panStart[1]);
356406
} else {
@@ -363,8 +413,46 @@ private void addPointerForIndex(
363413
packet.putDouble(0.0); // rotation
364414

365415
if (isTrackpadPan && (panZoomType == PointerChange.PAN_ZOOM_END)) {
366-
ongoingPans.remove(event.getPointerId(pointerIndex));
416+
ongoingPans.remove(pointerId);
417+
}
418+
}
419+
420+
private float getHorizontalScrollFactor(@NonNull Context context) {
421+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
422+
return ViewConfiguration.get(context).getScaledHorizontalScrollFactor();
423+
} else {
424+
// Vertical scroll factor is not a typo. This is what View.java does in android.
425+
return getVerticalScrollFactorPre26(context);
426+
}
427+
}
428+
429+
private float getVerticalScrollFactor(@NonNull Context context) {
430+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
431+
return getVerticalScrollFactorAbove26(context);
432+
} else {
433+
return getVerticalScrollFactorPre26(context);
434+
}
435+
}
436+
437+
@TargetApi(26)
438+
private float getVerticalScrollFactorAbove26(@NonNull Context context) {
439+
return ViewConfiguration.get(context).getScaledVerticalScrollFactor();
440+
}
441+
442+
// See
443+
// 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
444+
private int getVerticalScrollFactorPre26(@NonNull Context context) {
445+
if (cachedVerticalScrollFactor == 0) {
446+
TypedValue outValue = new TypedValue();
447+
if (!context
448+
.getTheme()
449+
.resolveAttribute(android.R.attr.listPreferredItemHeight, outValue, true)) {
450+
return DEFAULT_VERTICAL_SCROLL_FACTOR;
451+
}
452+
cachedVerticalScrollFactor =
453+
(int) outValue.getDimension(context.getResources().getDisplayMetrics());
367454
}
455+
return cachedVerticalScrollFactor;
368456
}
369457

370458
@PointerChange

shell/platform/android/io/flutter/embedding/android/FlutterView.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -952,7 +952,8 @@ public boolean onTouchEvent(@NonNull MotionEvent event) {
952952
@Override
953953
public boolean onGenericMotionEvent(@NonNull MotionEvent event) {
954954
boolean handled =
955-
isAttachedToFlutterEngine() && androidTouchProcessor.onGenericMotionEvent(event);
955+
isAttachedToFlutterEngine()
956+
&& androidTouchProcessor.onGenericMotionEvent(event, getContext());
956957
return handled ? true : super.onGenericMotionEvent(event);
957958
}
958959

shell/platform/android/io/flutter/view/FlutterView.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -482,7 +482,8 @@ public boolean onHoverEvent(MotionEvent event) {
482482
*/
483483
@Override
484484
public boolean onGenericMotionEvent(MotionEvent event) {
485-
boolean handled = isAttached() && androidTouchProcessor.onGenericMotionEvent(event);
485+
boolean handled =
486+
isAttached() && androidTouchProcessor.onGenericMotionEvent(event, getContext());
486487
return handled ? true : super.onGenericMotionEvent(event);
487488
}
488489

0 commit comments

Comments
 (0)