Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

i82973 scroll mouse wheel support #44724

Merged
merged 29 commits into from
Aug 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
4fc9130
Pair progress
Piinks Aug 9, 2023
17faa0d
Working mouse scroll wheel, note flutter takes longer to "see" a mous…
reidbaker Aug 15, 2023
95f5aa0
refactor packet method to have an override when context is null
reidbaker Aug 15, 2023
2b9686c
Add unit test for scroll wheel
reidbaker Aug 16, 2023
43619cc
Add unit test for scroll wheel
reidbaker Aug 16, 2023
aa01e98
Add unit test for scroll wheel
reidbaker Aug 16, 2023
05420ee
make pointer logic easier to read
reidbaker Aug 16, 2023
e14b961
revert unrelated file
reidbaker Aug 16, 2023
9efded6
Merge branch 'main' into i82973-scroll-mouse-wheel-support
reidbaker Aug 16, 2023
2ea445d
Fix merge, add timestamp test, code does not compile
reidbaker Aug 16, 2023
5140201
Merge branch 'main' into i82973-scroll-mouse-wheel-support
reidbaker Aug 16, 2023
e76fec0
Make tests compile
reidbaker Aug 16, 2023
22e8a77
Make tests pass
reidbaker Aug 17, 2023
e7dfa9d
add device test
reidbaker Aug 17, 2023
53ac277
Add tests for pressure, obscured and synth
reidbaker Aug 17, 2023
02e1176
Add tests for pressure min and max
reidbaker Aug 17, 2023
2c823c7
Add test for stylus distance
reidbaker Aug 17, 2023
c2f3c56
Add test for stylus distance
reidbaker Aug 17, 2023
168164f
Add tests for size and radius
reidbaker Aug 17, 2023
06cda9a
Add tests for pan deltax and rotation and scale
reidbaker Aug 17, 2023
bb157fb
formatting
reidbaker Aug 17, 2023
01e8ebb
Add test for buttons, specifically stylus
reidbaker Aug 18, 2023
8f3aa72
formatting
reidbaker Aug 18, 2023
4df1fee
Merge branch 'main' into i82973-scroll-mouse-wheel-support
reidbaker Aug 18, 2023
eb4dbb0
extract mock event values into function
reidbaker Aug 18, 2023
33be4af
formatting
reidbaker Aug 18, 2023
2cd1f46
Tests for pre 26 scroll behavior
reidbaker Aug 18, 2023
3848a28
Merge branch 'main' into i82973-scroll-mouse-wheel-support
reidbaker Aug 21, 2023
ae7439f
Update shell/platform/android/io/flutter/embedding/android/AndroidTou…
Piinks Aug 21, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package io.flutter.embedding.android;

import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Matrix;
import android.os.Build;
import android.util.TypedValue;
import android.view.InputDevice;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
Expand Down Expand Up @@ -80,6 +84,10 @@ public class AndroidTouchProcessor {
private static final int POINTER_DATA_FIELD_COUNT = 35;
@VisibleForTesting static final int BYTES_PER_FIELD = 8;

// Default if context is null, chosen to ensure reasonable speed scrolling.
@VisibleForTesting static final int DEFAULT_VERTICAL_SCROLL_FACTOR = 48;
@VisibleForTesting static final int DEFAULT_HORIZONTAL_SCROLL_FACTOR = 48;

// This value must match the value in framework's platform_view.dart.
// This flag indicates whether the original Android pointer events were batched together.
private static final int POINTER_DATA_FLAG_BATCHED = 1;
Expand All @@ -93,6 +101,9 @@ public class AndroidTouchProcessor {

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

// Only used on api 25 and below to avoid requerying display metrics.
private int cachedVerticalScrollFactor;

/**
* Constructs an {@code AndroidTouchProcessor} that will send touch event data to the Flutter
* execution context represented by the given {@link FlutterRenderer}.
Expand Down Expand Up @@ -181,9 +192,10 @@ public boolean onTouchEvent(@NonNull MotionEvent event, @NonNull Matrix transfor
* wheel movements, etc.
*
* @param event The generic motion event being processed.
* @param context For use by ViewConfiguration.get(context) to scale input.
* @return True if the event was handled.
*/
public boolean onGenericMotionEvent(@NonNull MotionEvent event) {
public boolean onGenericMotionEvent(@NonNull MotionEvent event, @NonNull Context context) {
// Method isFromSource is only available in API 18+ (Jelly Bean MR2)
// Mouse hover support is not implemented for API < 18.
boolean isPointerEvent =
Expand All @@ -192,7 +204,9 @@ public boolean onGenericMotionEvent(@NonNull MotionEvent event) {
boolean isMovementEvent =
(event.getActionMasked() == MotionEvent.ACTION_HOVER_MOVE
|| event.getActionMasked() == MotionEvent.ACTION_SCROLL);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change below was made for readability since @camsim99 and I both thought the implementation was confusing. It should be functionally identical and easier to follow. If a reviewer disagree please comment here and let me know.

if (!isPointerEvent || !isMovementEvent) {
if (isPointerEvent && isMovementEvent) {
// Continue.
} else {
return false;
}

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

// ACTION_HOVER_MOVE always applies to a single pointer only.
addPointerForIndex(event, event.getActionIndex(), pointerChange, 0, IDENTITY_TRANSFORM, packet);
addPointerForIndex(
event, event.getActionIndex(), pointerChange, 0, IDENTITY_TRANSFORM, packet, context);
if (packet.position() % (POINTER_DATA_FIELD_COUNT * BYTES_PER_FIELD) != 0) {
throw new AssertionError("Packet position is not on field boundary.");
}
renderer.dispatchPointerDataPacket(packet, packet.position());
return true;
}

// TODO(mattcarroll): consider creating a PointerPacket class instead of using a procedure that
// mutates inputs.
/// Calls addPointerForIndex with null for context.
///
/// Without context the scroll wheel will not mimick android's scroll speed.
private void addPointerForIndex(
MotionEvent event,
int pointerIndex,
int pointerChange,
int pointerData,
Matrix transformMatrix,
ByteBuffer packet) {
addPointerForIndex(
event, pointerIndex, pointerChange, pointerData, transformMatrix, packet, null);
}

// TODO: consider creating a PointerPacket class instead of using a procedure that
// mutates inputs. https://github.com/flutter/flutter/issues/132853
private void addPointerForIndex(
MotionEvent event,
int pointerIndex,
int pointerChange,
int pointerData,
Matrix transformMatrix,
ByteBuffer packet,
Context context) {
if (pointerChange == -1) {
return;
}
final int pointerId = event.getPointerId(pointerIndex);

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

int panZoomType = -1;
boolean isTrackpadPan = ongoingPans.containsKey(event.getPointerId(pointerIndex));
boolean isTrackpadPan = ongoingPans.containsKey(pointerId);
if (isTrackpadPan) {
panZoomType = getPointerChangeForPanZoom(pointerChange);
if (panZoomType == -1) {
Expand Down Expand Up @@ -278,13 +314,13 @@ private void addPointerForIndex(
packet.putLong(pointerKind); // kind
}
packet.putLong(signalKind); // signal_kind
packet.putLong(event.getPointerId(pointerIndex)); // device
packet.putLong(pointerId); // device
packet.putLong(0); // pointer_identifier, will be generated in pointer_data_packet_converter.cc.

if (isTrackpadPan) {
float[] panStart = ongoingPans.get(event.getPointerId(pointerIndex));
packet.putDouble(panStart[0]);
packet.putDouble(panStart[1]);
float[] panStart = ongoingPans.get(pointerId);
packet.putDouble(panStart[0]); // physical_x
packet.putDouble(panStart[1]); // physical_y
} else {
packet.putDouble(viewToScreenCoords[0]); // physical_x
packet.putDouble(viewToScreenCoords[1]); // physical_y
Expand Down Expand Up @@ -341,16 +377,30 @@ private void addPointerForIndex(

packet.putLong(pointerData); // platformData

// See android scrollview for inspiration.
// 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
if (signalKind == PointerSignalKind.SCROLL) {
packet.putDouble(-event.getAxisValue(MotionEvent.AXIS_HSCROLL)); // scroll_delta_x
packet.putDouble(-event.getAxisValue(MotionEvent.AXIS_VSCROLL)); // scroll_delta_y
double horizontalScaleFactor = DEFAULT_HORIZONTAL_SCROLL_FACTOR;
double verticalScaleFactor = DEFAULT_VERTICAL_SCROLL_FACTOR;
if (context != null) {
horizontalScaleFactor = getHorizontalScrollFactor(context);
verticalScaleFactor = getVerticalScrollFactor(context);
}
// We flip the sign of the scroll value below because it aligns the pixel value with the
// scroll direction in native android.
final double horizontalScrollPixels =
horizontalScaleFactor * -event.getAxisValue(MotionEvent.AXIS_HSCROLL, pointerIndex);
final double verticalScrollPixels =
verticalScaleFactor * -event.getAxisValue(MotionEvent.AXIS_VSCROLL, pointerIndex);
packet.putDouble(horizontalScrollPixels); // scroll_delta_x
packet.putDouble(verticalScrollPixels); // scroll_delta_y
} else {
packet.putDouble(0.0); // scroll_delta_x
packet.putDouble(0.0); // scroll_delta_x
packet.putDouble(0.0); // scroll_delta_y
}

if (isTrackpadPan) {
float[] panStart = ongoingPans.get(event.getPointerId(pointerIndex));
float[] panStart = ongoingPans.get(pointerId);
packet.putDouble(viewToScreenCoords[0] - panStart[0]);
packet.putDouble(viewToScreenCoords[1] - panStart[1]);
} else {
Expand All @@ -363,8 +413,46 @@ private void addPointerForIndex(
packet.putDouble(0.0); // rotation

if (isTrackpadPan && (panZoomType == PointerChange.PAN_ZOOM_END)) {
ongoingPans.remove(event.getPointerId(pointerIndex));
ongoingPans.remove(pointerId);
}
}

private float getHorizontalScrollFactor(@NonNull Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return ViewConfiguration.get(context).getScaledHorizontalScrollFactor();
} else {
// Vertical scroll factor is not a typo. This is what View.java does in android.
return getVerticalScrollFactorPre26(context);
}
}

private float getVerticalScrollFactor(@NonNull Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return getVerticalScrollFactorAbove26(context);
} else {
return getVerticalScrollFactorPre26(context);
}
}

@TargetApi(26)
private float getVerticalScrollFactorAbove26(@NonNull Context context) {
return ViewConfiguration.get(context).getScaledVerticalScrollFactor();
}

// See
// 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
private int getVerticalScrollFactorPre26(@NonNull Context context) {
if (cachedVerticalScrollFactor == 0) {
TypedValue outValue = new TypedValue();
if (!context
.getTheme()
.resolveAttribute(android.R.attr.listPreferredItemHeight, outValue, true)) {
return DEFAULT_VERTICAL_SCROLL_FACTOR;
}
cachedVerticalScrollFactor =
(int) outValue.getDimension(context.getResources().getDisplayMetrics());
}
return cachedVerticalScrollFactor;
}

@PointerChange
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -952,7 +952,8 @@ public boolean onTouchEvent(@NonNull MotionEvent event) {
@Override
public boolean onGenericMotionEvent(@NonNull MotionEvent event) {
boolean handled =
isAttachedToFlutterEngine() && androidTouchProcessor.onGenericMotionEvent(event);
isAttachedToFlutterEngine()
&& androidTouchProcessor.onGenericMotionEvent(event, getContext());
return handled ? true : super.onGenericMotionEvent(event);
}

Expand Down
3 changes: 2 additions & 1 deletion shell/platform/android/io/flutter/view/FlutterView.java
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,8 @@ public boolean onHoverEvent(MotionEvent event) {
*/
@Override
public boolean onGenericMotionEvent(MotionEvent event) {
boolean handled = isAttached() && androidTouchProcessor.onGenericMotionEvent(event);
boolean handled =
isAttached() && androidTouchProcessor.onGenericMotionEvent(event, getContext());
return handled ? true : super.onGenericMotionEvent(event);
}

Expand Down
Loading