Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 9 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,11 @@
package io.flutter.embedding.android;

import android.content.Context;
import android.graphics.Matrix;
import android.os.Build;
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 @@ -183,7 +185,7 @@ public boolean onTouchEvent(@NonNull MotionEvent event, @NonNull Matrix transfor
* @param event The generic motion event being processed.
* @return True if the event was handled.
*/
public boolean onGenericMotionEvent(@NonNull MotionEvent event) {
public boolean onGenericMotionEvent(@NonNull MotionEvent event, 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 +194,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,14 +207,28 @@ 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;
}

/// Passes null for context when it is not available. Note that without context scroll wheel will
// not work.
private void addPointerForIndex(
MotionEvent event,
int pointerIndex,
int pointerChange,
int pointerData,
Matrix transformMatrix,
ByteBuffer packet) {
addPointerForIndex(
event, pointerIndex, pointerChange, pointerData, transformMatrix, packet, null);
}

// TODO(mattcarroll): consider creating a PointerPacket class instead of using a procedure that
// mutates inputs.
private void addPointerForIndex(
Expand All @@ -219,7 +237,8 @@ private void addPointerForIndex(
int pointerChange,
int pointerData,
Matrix transformMatrix,
ByteBuffer packet) {
ByteBuffer packet,
Context context) {
if (pointerChange == -1) {
return;
}
Expand Down Expand Up @@ -341,12 +360,27 @@ private void addPointerForIndex(

packet.putLong(pointerData); // platformData

// See android scrollview for insperation.
// 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
// Default if context is null, chosen to ensure reasonable speed scrolling.
double verticalScaleFactor = 48.0;
double horizontalScaleFactor = 48.0;
if (context != null) {
horizontalScaleFactor = ViewConfiguration.get(context).getScaledHorizontalScrollFactor();
verticalScaleFactor = ViewConfiguration.get(context).getScaledVerticalScrollFactor();
}
// 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) {
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
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@
import static org.mockito.Mockito.when;
Copy link
Contributor

@Piinks Piinks Aug 21, 2023

Choose a reason for hiding this comment

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

These tests! 🤌 Thanks for shaving this down and adding all this coverage.


import android.annotation.TargetApi;
import android.content.Context;
import android.view.InputDevice;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import io.flutter.embedding.engine.renderer.FlutterRenderer;
import java.nio.ByteBuffer;
Expand Down Expand Up @@ -58,6 +61,14 @@ private double readPointerPhysicalY(ByteBuffer buffer) {
return buffer.getDouble(8 * AndroidTouchProcessor.BYTES_PER_FIELD);
}

private double readScrollDeltaX(ByteBuffer buffer) {
return buffer.getDouble(27 * AndroidTouchProcessor.BYTES_PER_FIELD);
}

private double readScrollDeltaY(ByteBuffer buffer) {
return buffer.getDouble(28 * AndroidTouchProcessor.BYTES_PER_FIELD);
}

private double readPointerPanX(ByteBuffer buffer) {
return buffer.getDouble(29 * AndroidTouchProcessor.BYTES_PER_FIELD);
}
Expand All @@ -66,6 +77,11 @@ private double readPointerPanY(ByteBuffer buffer) {
return buffer.getDouble(30 * AndroidTouchProcessor.BYTES_PER_FIELD);
}

/// Utility method when trying to write a new test. Prefer named readPointerXXX.
private double readOffset(int offset, ByteBuffer buffer) {
return buffer.getDouble(offset * AndroidTouchProcessor.BYTES_PER_FIELD);
}

private class MotionEventMocker {
int pointerId;
int source;
Expand All @@ -81,6 +97,15 @@ MotionEvent mockEvent(int action, float x, float y, int buttonState) {
MotionEvent event = mock(MotionEvent.class);
when(event.getDevice()).thenReturn(null);
when(event.getSource()).thenReturn(source);
// Ensure that isFromSource does not auto default to false when source is passed in.
when(event.isFromSource(InputDevice.SOURCE_CLASS_POINTER))
.thenReturn(source == InputDevice.SOURCE_CLASS_POINTER);
when(event.isFromSource(InputDevice.SOURCE_MOUSE))
.thenReturn(source == InputDevice.SOURCE_MOUSE);
when(event.isFromSource(InputDevice.SOURCE_STYLUS))
.thenReturn(source == InputDevice.SOURCE_STYLUS);
when(event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN))
.thenReturn(source == InputDevice.SOURCE_TOUCHSCREEN);
when(event.getPointerCount()).thenReturn(1);
when(event.getActionMasked()).thenReturn(action);
when(event.getActionIndex()).thenReturn(0);
Expand All @@ -89,7 +114,8 @@ MotionEvent mockEvent(int action, float x, float y, int buttonState) {
when(event.getX(0)).thenReturn(x);
when(event.getY(0)).thenReturn(y);
when(event.getToolType(0)).thenReturn(toolType);
when(event.isFromSource(InputDevice.SOURCE_CLASS_POINTER)).thenReturn(true);
when(event.getAxisValue(MotionEvent.AXIS_HSCROLL, pointerId)).thenReturn(x);
when(event.getAxisValue(MotionEvent.AXIS_VSCROLL, pointerId)).thenReturn(y);
return event;
}
}
Expand Down Expand Up @@ -220,10 +246,45 @@ public void unexpectedMaskedAction() {
verify(mockRenderer, never()).dispatchPointerDataPacket(ByteBuffer.allocate(0), 0);
}

@Test
public void scrollWheel() {
// Pointer id must be zero to match actionIndex in mocked event.
final int pointerId = 0;
MotionEventMocker mocker =
new MotionEventMocker(
pointerId, InputDevice.SOURCE_CLASS_POINTER, MotionEvent.TOOL_TYPE_MOUSE);
final float horizontalScrollValue = -1f;
final float verticalScrollValue = .5f;
final Context context = ApplicationProvider.getApplicationContext();
final double horizontalScaleFactor =
ViewConfiguration.get(context).getScaledHorizontalScrollFactor();
final double verticalScaleFactor =
ViewConfiguration.get(context).getScaledVerticalScrollFactor();
// Zero verticalScaleFactor will cause this test to miss bugs.
assertEquals("zero horizontal scale factor", true, horizontalScaleFactor != 0);
assertEquals("zero vertical scale factor", true, verticalScaleFactor != 0);

final MotionEvent event =
mocker.mockEvent(MotionEvent.ACTION_SCROLL, horizontalScrollValue, verticalScrollValue, 1);
boolean handled = touchProcessor.onGenericMotionEvent(event, context);

InOrder inOrder = inOrder(mockRenderer);
inOrder
.verify(mockRenderer)
.dispatchPointerDataPacket(packetCaptor.capture(), packetSizeCaptor.capture());
ByteBuffer packet = packetCaptor.getValue();

assertEquals(-horizontalScrollValue * horizontalScaleFactor, readScrollDeltaX(packet));
assertEquals(-verticalScrollValue * verticalScaleFactor, readScrollDeltaY(packet));
verify(event).getAxisValue(MotionEvent.AXIS_HSCROLL, pointerId);
verify(event).getAxisValue(MotionEvent.AXIS_VSCROLL, pointerId);

inOrder.verifyNoMoreInteractions();
}

@Test
public void unexpectedPointerChange() {
// Regression test for https://github.com/flutter/flutter/issues/129765

MotionEventMocker mocker =
new MotionEventMocker(0, InputDevice.SOURCE_MOUSE, MotionEvent.TOOL_TYPE_MOUSE);

Expand Down