Skip to content

Commit 39648ab

Browse files
committed
GestureConverter: Stop flings with fake fingers
Currently, the only way to stop a fling (the scrolling that occurs due to "momentum" after you've finished a two-finger swipe on the touchpad) is to put two fingers down again and move them slightly. This is an awkward gesture, compared to most platforms where you just touch the pad again and scrolling stops. The trouble is that there is currently no API by which we can tell apps to stop a fling. Instead, make a fake finger at the current cursor location, then immediately cancel it. This stops flings without side effects (e.g. activating UI elements, inking in drawing apps, moving the insertion point in text fields, etc.) in every app I've tried it in [0], except for a number of issues in Google Docs, Sheets, and Slides (both the Android apps and when opened in Chrome). We'll have to talk to the relevant teams about this. Follow-up CLs will make an API change to make it easier to distinguish these fling stops from actual clicks. [0]: Calendar, Chrome [1, 2], Docs, Files, Firefox Beta [1, 2], Gmail, Google, Keep, Maps, News, Photos, Play Store, Podcasts, Settings, Sheets, Slides, Word, YouTube [1]: https://www.longestjokeintheworld.com/ [2]: https://codepen.io/caraya/embed/ZyQMWd Bug: 281106755 Test: start flings in a number of apps [0], touch the touchpad, and check that the fling stops without causing other side effects Change-Id: Ie8504dc047ee23b5524d5372f65adfaa75a76cd8
1 parent 2db1957 commit 39648ab

4 files changed

Lines changed: 122 additions & 10 deletions

File tree

libs/input/input_flags.aconfig

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,10 @@ flag {
118118
description: "Controls the API to provide InputDevice view behavior."
119119
bug: "246946631"
120120
}
121+
122+
flag {
123+
name: "enable_touchpad_fling_stop"
124+
namespace: "input"
125+
description: "Enable fling scrolling to be stopped by putting a finger on the touchpad again"
126+
bug: "281106755"
127+
}

services/inputflinger/reader/mapper/gestures/GestureConverter.cpp

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ GestureConverter::GestureConverter(InputReaderContext& readerContext,
6767
: mDeviceId(deviceId),
6868
mReaderContext(readerContext),
6969
mPointerController(readerContext.getPointerController(deviceId)),
70-
mEnablePointerChoreographer(input_flags::enable_pointer_choreographer()) {
70+
mEnablePointerChoreographer(input_flags::enable_pointer_choreographer()),
71+
mEnableFlingStop(input_flags::enable_touchpad_fling_stop()) {
7172
deviceContext.getAbsoluteAxisInfo(ABS_MT_POSITION_X, &mXAxisInfo);
7273
deviceContext.getAbsoluteAxisInfo(ABS_MT_POSITION_Y, &mYAxisInfo);
7374
}
@@ -400,20 +401,55 @@ std::list<NotifyArgs> GestureConverter::handleFling(nsecs_t when, nsecs_t readTi
400401
// ensure consistency between touchscreen and touchpad flings), so we're just using
401402
// the "start fling" gestures as a marker for the end of a two-finger scroll
402403
// gesture.
404+
mFlingMayBeInProgress = true;
403405
return endScroll(when, readTime);
404406
}
405407
break;
406408
case GESTURES_FLING_TAP_DOWN:
407409
if (mCurrentClassification == MotionClassification::NONE) {
408-
// Use the tap down state of a fling gesture as an indicator that a contact
409-
// has been initiated with the touchpad. We treat this as a move event with zero
410-
// magnitude, which will also result in the pointer icon being updated.
411-
// TODO(b/282023644): Add a signal in libgestures for when a stable contact has been
412-
// initiated with a touchpad.
413-
return handleMove(when, readTime, gestureStartTime,
414-
Gesture(kGestureMove, gesture.start_time, gesture.end_time,
415-
/*dx=*/0.f,
416-
/*dy=*/0.f));
410+
if (mEnableFlingStop && mFlingMayBeInProgress) {
411+
// The user has just touched the pad again after ending a two-finger scroll
412+
// motion, which might have started a fling. We want to stop the fling, but
413+
// unfortunately there's currently no API for doing so. Instead, send and
414+
// immediately cancel a fake finger to cause the scrolling to stop and hopefully
415+
// avoid side effects (e.g. activation of UI elements).
416+
// TODO(b/326056750): add an API for fling stops.
417+
mFlingMayBeInProgress = false;
418+
const auto [xCursorPosition, yCursorPosition] = mEnablePointerChoreographer
419+
? FloatPoint{0, 0}
420+
: mPointerController->getPosition();
421+
PointerCoords coords;
422+
coords.clear();
423+
coords.setAxisValue(AMOTION_EVENT_AXIS_X, xCursorPosition);
424+
coords.setAxisValue(AMOTION_EVENT_AXIS_Y, yCursorPosition);
425+
coords.setAxisValue(AMOTION_EVENT_AXIS_RELATIVE_X, 0);
426+
coords.setAxisValue(AMOTION_EVENT_AXIS_RELATIVE_Y, 0);
427+
428+
std::list<NotifyArgs> out;
429+
mDownTime = when;
430+
out += exitHover(when, readTime, xCursorPosition, yCursorPosition);
431+
// TODO(b/281106755): add a MotionClassification value for fling stops.
432+
out.push_back(makeMotionArgs(when, readTime, AMOTION_EVENT_ACTION_DOWN,
433+
/*actionButton=*/0, /*buttonState=*/0,
434+
/*pointerCount=*/1, &coords, xCursorPosition,
435+
yCursorPosition));
436+
out.push_back(makeMotionArgs(when, readTime, AMOTION_EVENT_ACTION_CANCEL,
437+
/*actionButton=*/0, /*buttonState=*/0,
438+
/*pointerCount=*/1, &coords, xCursorPosition,
439+
yCursorPosition));
440+
out += enterHover(when, readTime, xCursorPosition, yCursorPosition);
441+
return out;
442+
} else {
443+
// Use the tap down state of a fling gesture as an indicator that a contact
444+
// has been initiated with the touchpad. We treat this as a move event with zero
445+
// magnitude, which will also result in the pointer icon being updated.
446+
// TODO(b/282023644): Add a signal in libgestures for when a stable contact has
447+
// been initiated with a touchpad.
448+
return handleMove(when, readTime, gestureStartTime,
449+
Gesture(kGestureMove, gesture.start_time, gesture.end_time,
450+
/*dx=*/0.f,
451+
/*dy=*/0.f));
452+
}
417453
}
418454
break;
419455
default:

services/inputflinger/reader/mapper/gestures/GestureConverter.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ class GestureConverter {
106106
InputReaderContext& mReaderContext;
107107
std::shared_ptr<PointerControllerInterface> mPointerController;
108108
const bool mEnablePointerChoreographer;
109+
const bool mEnableFlingStop;
109110

110111
std::optional<int32_t> mDisplayId;
111112
FloatRect mBoundsInLogicalDisplay{};
@@ -120,6 +121,9 @@ class GestureConverter {
120121
// Whether we are currently in a hover state (i.e. a HOVER_ENTER event has been sent without a
121122
// matching HOVER_EXIT).
122123
bool mIsHovering = false;
124+
// Whether we've received a "fling start" gesture (i.e. the end of a scroll) but no "fling tap
125+
// down" gesture to match it yet.
126+
bool mFlingMayBeInProgress = false;
123127

124128
MotionClassification mCurrentClassification = MotionClassification::NONE;
125129
// Only used when mCurrentClassification is MULTI_FINGER_SWIPE.

services/inputflinger/tests/GestureConverter_test.cpp

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ const auto TOUCHPAD_PALM_REJECTION_V2 =
4747
} // namespace
4848

4949
using testing::AllOf;
50+
using testing::Each;
5051
using testing::ElementsAre;
5152
using testing::VariantWith;
5253

@@ -1229,6 +1230,38 @@ TEST_F(GestureConverterTest, FlingTapDown) {
12291230
ASSERT_TRUE(mFakePointerController->isPointerShown());
12301231
}
12311232

1233+
TEST_F(GestureConverterTest, FlingTapDownAfterScrollStopsFling) {
1234+
InputDeviceContext deviceContext(*mDevice, EVENTHUB_ID);
1235+
input_flags::enable_touchpad_fling_stop(true);
1236+
GestureConverter converter(*mReader->getContext(), deviceContext, DEVICE_ID);
1237+
converter.setDisplayId(ADISPLAY_ID_DEFAULT);
1238+
1239+
Gesture scrollGesture(kGestureScroll, ARBITRARY_GESTURE_TIME, ARBITRARY_GESTURE_TIME, 0, -10);
1240+
std::list<NotifyArgs> args =
1241+
converter.handleGesture(ARBITRARY_TIME, READ_TIME, ARBITRARY_TIME, scrollGesture);
1242+
Gesture flingGesture(kGestureFling, ARBITRARY_GESTURE_TIME, ARBITRARY_GESTURE_TIME, 1, 1,
1243+
GESTURES_FLING_START);
1244+
args = converter.handleGesture(ARBITRARY_TIME, READ_TIME, ARBITRARY_TIME, flingGesture);
1245+
1246+
Gesture tapDownGesture(kGestureFling, ARBITRARY_GESTURE_TIME, ARBITRARY_GESTURE_TIME,
1247+
/*vx=*/0.f, /*vy=*/0.f, GESTURES_FLING_TAP_DOWN);
1248+
args = converter.handleGesture(ARBITRARY_TIME, READ_TIME, ARBITRARY_TIME, tapDownGesture);
1249+
ASSERT_THAT(args,
1250+
ElementsAre(VariantWith<NotifyMotionArgs>(
1251+
WithMotionAction(AMOTION_EVENT_ACTION_HOVER_EXIT)),
1252+
VariantWith<NotifyMotionArgs>(
1253+
WithMotionAction(AMOTION_EVENT_ACTION_DOWN)),
1254+
VariantWith<NotifyMotionArgs>(
1255+
WithMotionAction(AMOTION_EVENT_ACTION_CANCEL)),
1256+
VariantWith<NotifyMotionArgs>(
1257+
AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_ENTER),
1258+
WithMotionClassification(MotionClassification::NONE)))));
1259+
ASSERT_THAT(args,
1260+
Each(VariantWith<NotifyMotionArgs>(AllOf(WithCoords(POINTER_X, POINTER_Y),
1261+
WithToolType(ToolType::FINGER),
1262+
WithDisplayId(ADISPLAY_ID_DEFAULT)))));
1263+
}
1264+
12321265
TEST_F(GestureConverterTest, Tap) {
12331266
// Tap should produce button press/release events
12341267
InputDeviceContext deviceContext(*mDevice, EVENTHUB_ID);
@@ -2717,6 +2750,38 @@ TEST_F(GestureConverterTestWithChoreographer, FlingTapDown) {
27172750
WithButtonState(0), WithPressure(0.0f), WithDisplayId(ADISPLAY_ID_DEFAULT)));
27182751
}
27192752

2753+
TEST_F(GestureConverterTestWithChoreographer, FlingTapDownAfterScrollStopsFling) {
2754+
InputDeviceContext deviceContext(*mDevice, EVENTHUB_ID);
2755+
input_flags::enable_touchpad_fling_stop(true);
2756+
GestureConverter converter(*mReader->getContext(), deviceContext, DEVICE_ID);
2757+
converter.setDisplayId(ADISPLAY_ID_DEFAULT);
2758+
2759+
Gesture scrollGesture(kGestureScroll, ARBITRARY_GESTURE_TIME, ARBITRARY_GESTURE_TIME, 0, -10);
2760+
std::list<NotifyArgs> args =
2761+
converter.handleGesture(ARBITRARY_TIME, READ_TIME, ARBITRARY_TIME, scrollGesture);
2762+
Gesture flingGesture(kGestureFling, ARBITRARY_GESTURE_TIME, ARBITRARY_GESTURE_TIME, 1, 1,
2763+
GESTURES_FLING_START);
2764+
args = converter.handleGesture(ARBITRARY_TIME, READ_TIME, ARBITRARY_TIME, flingGesture);
2765+
2766+
Gesture tapDownGesture(kGestureFling, ARBITRARY_GESTURE_TIME, ARBITRARY_GESTURE_TIME,
2767+
/*vx=*/0.f, /*vy=*/0.f, GESTURES_FLING_TAP_DOWN);
2768+
args = converter.handleGesture(ARBITRARY_TIME, READ_TIME, ARBITRARY_TIME, tapDownGesture);
2769+
ASSERT_THAT(args,
2770+
ElementsAre(VariantWith<NotifyMotionArgs>(
2771+
WithMotionAction(AMOTION_EVENT_ACTION_HOVER_EXIT)),
2772+
VariantWith<NotifyMotionArgs>(
2773+
WithMotionAction(AMOTION_EVENT_ACTION_DOWN)),
2774+
VariantWith<NotifyMotionArgs>(
2775+
WithMotionAction(AMOTION_EVENT_ACTION_CANCEL)),
2776+
VariantWith<NotifyMotionArgs>(
2777+
AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_ENTER),
2778+
WithMotionClassification(MotionClassification::NONE)))));
2779+
ASSERT_THAT(args,
2780+
Each(VariantWith<NotifyMotionArgs>(AllOf(WithCoords(0, 0),
2781+
WithToolType(ToolType::FINGER),
2782+
WithDisplayId(ADISPLAY_ID_DEFAULT)))));
2783+
}
2784+
27202785
TEST_F(GestureConverterTestWithChoreographer, Tap) {
27212786
// Tap should produce button press/release events
27222787
InputDeviceContext deviceContext(*mDevice, EVENTHUB_ID);

0 commit comments

Comments
 (0)