11package io .flutter .embedding .android ;
22
3+ import android .annotation .TargetApi ;
4+ import android .content .Context ;
35import android .graphics .Matrix ;
46import android .os .Build ;
7+ import android .util .TypedValue ;
58import android .view .InputDevice ;
69import android .view .MotionEvent ;
10+ import android .view .ViewConfiguration ;
711import androidx .annotation .IntDef ;
812import androidx .annotation .NonNull ;
913import 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
0 commit comments