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

Commit 7ded18a

Browse files
authored
Scroll inertia cancel for Win32 (#34452)
1 parent 43dae0c commit 7ded18a

7 files changed

+179
-8
lines changed

shell/platform/windows/direct_manipulation.cc

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
#include "flutter/fml/logging.h"
66

7+
#include <algorithm>
8+
79
#include "flutter/shell/platform/windows/direct_manipulation.h"
810
#include "flutter/shell/platform/windows/window.h"
911
#include "flutter/shell/platform/windows/window_binding_handler_delegate.h"
@@ -24,6 +26,10 @@
2426

2527
namespace flutter {
2628

29+
int32_t DirectManipulationEventHandler::GetDeviceId() {
30+
return (int32_t) reinterpret_cast<int64_t>(this);
31+
}
32+
2733
STDMETHODIMP DirectManipulationEventHandler::QueryInterface(REFIID iid,
2834
void** ppv) {
2935
if ((iid == IID_IUnknown) ||
@@ -43,26 +49,39 @@ HRESULT DirectManipulationEventHandler::OnViewportStatusChanged(
4349
IDirectManipulationViewport* viewport,
4450
DIRECTMANIPULATION_STATUS current,
4551
DIRECTMANIPULATION_STATUS previous) {
52+
during_inertia_ = current == DIRECTMANIPULATION_INERTIA;
4653
if (during_synthesized_reset_ && previous == DIRECTMANIPULATION_RUNNING) {
4754
during_synthesized_reset_ = false;
4855
} else if (current == DIRECTMANIPULATION_RUNNING) {
4956
if (!during_synthesized_reset_) {
5057
// Not a false event.
5158
if (owner_->binding_handler_delegate) {
52-
owner_->binding_handler_delegate->OnPointerPanZoomStart(
53-
(int32_t) reinterpret_cast<int64_t>(this));
59+
owner_->binding_handler_delegate->OnPointerPanZoomStart(GetDeviceId());
5460
}
5561
}
56-
} else if (previous == DIRECTMANIPULATION_RUNNING) {
62+
}
63+
if (previous == DIRECTMANIPULATION_RUNNING) {
64+
// Reset deltas to ensure only inertia values will be compared later.
65+
last_pan_delta_x_ = 0.0;
66+
last_pan_delta_y_ = 0.0;
5767
if (owner_->binding_handler_delegate) {
58-
owner_->binding_handler_delegate->OnPointerPanZoomEnd(
59-
(int32_t) reinterpret_cast<int64_t>(this));
68+
owner_->binding_handler_delegate->OnPointerPanZoomEnd(GetDeviceId());
69+
}
70+
} else if (previous == DIRECTMANIPULATION_INERTIA) {
71+
if (owner_->binding_handler_delegate &&
72+
(std::max)(std::abs(last_pan_delta_x_), std::abs(last_pan_delta_y_)) >
73+
0.01) {
74+
owner_->binding_handler_delegate->OnScrollInertiaCancel(GetDeviceId());
6075
}
6176
// Need to reset the content transform to its original position
6277
// so that we are ready for the next gesture.
6378
// Use during_synthesized_reset_ flag to prevent sending reset also to the
6479
// framework.
6580
during_synthesized_reset_ = true;
81+
last_pan_x_ = 0.0;
82+
last_pan_y_ = 0.0;
83+
last_pan_delta_x_ = 0.0;
84+
last_pan_delta_y_ = 0.0;
6685
RECT rect;
6786
HRESULT hr = viewport->GetViewportRect(&rect);
6887
if (FAILED(hr)) {
@@ -104,9 +123,13 @@ HRESULT DirectManipulationEventHandler::OnContentUpdated(
104123
float scale = c - (c - transform[0]);
105124
float pan_x = transform[4];
106125
float pan_y = transform[5];
107-
if (owner_->binding_handler_delegate) {
126+
last_pan_delta_x_ = pan_x - last_pan_x_;
127+
last_pan_delta_y_ = pan_y - last_pan_y_;
128+
last_pan_x_ = pan_x;
129+
last_pan_y_ = pan_y;
130+
if (owner_->binding_handler_delegate && !during_inertia_) {
108131
owner_->binding_handler_delegate->OnPointerPanZoomUpdate(
109-
(int32_t) reinterpret_cast<int64_t>(this), pan_x, pan_y, scale, 0);
132+
GetDeviceId(), pan_x, pan_y, scale, 0);
110133
}
111134
}
112135
return S_OK;
@@ -144,7 +167,8 @@ int DirectManipulationOwner::Init(unsigned int width, unsigned int height) {
144167
DIRECTMANIPULATION_CONFIGURATION_INTERACTION |
145168
DIRECTMANIPULATION_CONFIGURATION_TRANSLATION_X |
146169
DIRECTMANIPULATION_CONFIGURATION_TRANSLATION_Y |
147-
DIRECTMANIPULATION_CONFIGURATION_SCALING;
170+
DIRECTMANIPULATION_CONFIGURATION_SCALING |
171+
DIRECTMANIPULATION_CONFIGURATION_TRANSLATION_INERTIA;
148172
RETURN_IF_FAILED(viewport_->ActivateConfiguration(configuration));
149173
RETURN_IF_FAILED(viewport_->SetViewportOptions(
150174
DIRECTMANIPULATION_VIEWPORT_OPTIONS_MANUALUPDATE));

shell/platform/windows/direct_manipulation.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,12 +106,23 @@ class DirectManipulationEventHandler
106106
DIRECTMANIPULATION_INTERACTION_TYPE interaction) override;
107107

108108
private:
109+
// Unique identifier to associate with all gesture event updates.
110+
int32_t GetDeviceId();
109111
// Parent object, used to store the target for gesture event updates.
110112
DirectManipulationOwner* owner_;
111113
// We need to reset some parts of DirectManipulation after each gesture
112114
// A flag is needed to ensure that false events created as the reset occurs
113115
// are not sent to the flutter framework.
114116
bool during_synthesized_reset_ = false;
117+
// Store whether current events are from synthetic inertia rather than user
118+
// input.
119+
bool during_inertia_ = false;
120+
// Store the difference between the last pan offsets to determine if inertia
121+
// has been cancelled in the middle of an animation.
122+
float last_pan_x_ = 0.0;
123+
float last_pan_y_ = 0.0;
124+
float last_pan_delta_x_ = 0.0;
125+
float last_pan_delta_y_ = 0.0;
115126
};
116127

117128
} // namespace flutter

shell/platform/windows/direct_manipulation_unittests.cc

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,5 +259,110 @@ TEST(DirectManipulationTest, TestRounding) {
259259
DIRECTMANIPULATION_INERTIA);
260260
}
261261

262+
TEST(DirectManipulationTest, TestInertiaCancelSentForUserCancel) {
263+
MockIDirectManipulationContent content;
264+
MockWindowBindingHandlerDelegate delegate;
265+
MockIDirectManipulationViewport viewport;
266+
const int DISPLAY_WIDTH = 800;
267+
const int DISPLAY_HEIGHT = 600;
268+
auto owner = std::make_unique<DirectManipulationOwner>(nullptr);
269+
owner->SetBindingHandlerDelegate(&delegate);
270+
auto handler =
271+
fml::MakeRefCounted<DirectManipulationEventHandler>(owner.get());
272+
int32_t device_id = (int32_t) reinterpret_cast<int64_t>(handler.get());
273+
// No need to mock the actual gesture, just start at the end.
274+
EXPECT_CALL(viewport, GetViewportRect(_))
275+
.WillOnce(::testing::Invoke([DISPLAY_WIDTH, DISPLAY_HEIGHT](RECT* rect) {
276+
rect->left = 0;
277+
rect->top = 0;
278+
rect->right = DISPLAY_WIDTH;
279+
rect->bottom = DISPLAY_HEIGHT;
280+
return S_OK;
281+
}));
282+
EXPECT_CALL(viewport, ZoomToRect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT, false))
283+
.WillOnce(::testing::Return(S_OK));
284+
EXPECT_CALL(delegate, OnPointerPanZoomEnd(device_id));
285+
handler->OnViewportStatusChanged((IDirectManipulationViewport*)&viewport,
286+
DIRECTMANIPULATION_INERTIA,
287+
DIRECTMANIPULATION_RUNNING);
288+
// Have pan_y change by 10 between inertia updates.
289+
EXPECT_CALL(content, GetContentTransform(_, 6))
290+
.WillOnce(::testing::Invoke([](float* transform, DWORD size) {
291+
transform[0] = 1;
292+
transform[4] = 0;
293+
transform[5] = 100;
294+
return S_OK;
295+
}));
296+
handler->OnContentUpdated((IDirectManipulationViewport*)&viewport,
297+
(IDirectManipulationContent*)&content);
298+
EXPECT_CALL(content, GetContentTransform(_, 6))
299+
.WillOnce(::testing::Invoke([](float* transform, DWORD size) {
300+
transform[0] = 1;
301+
transform[4] = 0;
302+
transform[5] = 110;
303+
return S_OK;
304+
}));
305+
handler->OnContentUpdated((IDirectManipulationViewport*)&viewport,
306+
(IDirectManipulationContent*)&content);
307+
// This looks like an interruption in the middle of synthetic inertia because
308+
// of user input.
309+
EXPECT_CALL(delegate, OnScrollInertiaCancel(device_id));
310+
handler->OnViewportStatusChanged((IDirectManipulationViewport*)&viewport,
311+
DIRECTMANIPULATION_READY,
312+
DIRECTMANIPULATION_INERTIA);
313+
}
314+
315+
TEST(DirectManipulationTest, TestInertiaCamcelNotSentAtInertiaEnd) {
316+
MockIDirectManipulationContent content;
317+
MockWindowBindingHandlerDelegate delegate;
318+
MockIDirectManipulationViewport viewport;
319+
const int DISPLAY_WIDTH = 800;
320+
const int DISPLAY_HEIGHT = 600;
321+
auto owner = std::make_unique<DirectManipulationOwner>(nullptr);
322+
owner->SetBindingHandlerDelegate(&delegate);
323+
auto handler =
324+
fml::MakeRefCounted<DirectManipulationEventHandler>(owner.get());
325+
int32_t device_id = (int32_t) reinterpret_cast<int64_t>(handler.get());
326+
// No need to mock the actual gesture, just start at the end.
327+
EXPECT_CALL(viewport, GetViewportRect(_))
328+
.WillOnce(::testing::Invoke([DISPLAY_WIDTH, DISPLAY_HEIGHT](RECT* rect) {
329+
rect->left = 0;
330+
rect->top = 0;
331+
rect->right = DISPLAY_WIDTH;
332+
rect->bottom = DISPLAY_HEIGHT;
333+
return S_OK;
334+
}));
335+
EXPECT_CALL(viewport, ZoomToRect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT, false))
336+
.WillOnce(::testing::Return(S_OK));
337+
EXPECT_CALL(delegate, OnPointerPanZoomEnd(device_id));
338+
handler->OnViewportStatusChanged((IDirectManipulationViewport*)&viewport,
339+
DIRECTMANIPULATION_INERTIA,
340+
DIRECTMANIPULATION_RUNNING);
341+
// Have no change in pan between events.
342+
EXPECT_CALL(content, GetContentTransform(_, 6))
343+
.WillOnce(::testing::Invoke([](float* transform, DWORD size) {
344+
transform[0] = 1;
345+
transform[4] = 0;
346+
transform[5] = 140;
347+
return S_OK;
348+
}));
349+
handler->OnContentUpdated((IDirectManipulationViewport*)&viewport,
350+
(IDirectManipulationContent*)&content);
351+
EXPECT_CALL(content, GetContentTransform(_, 6))
352+
.WillOnce(::testing::Invoke([](float* transform, DWORD size) {
353+
transform[0] = 1;
354+
transform[4] = 0;
355+
transform[5] = 140;
356+
return S_OK;
357+
}));
358+
handler->OnContentUpdated((IDirectManipulationViewport*)&viewport,
359+
(IDirectManipulationContent*)&content);
360+
// OnScrollInertiaCancel should not be called.
361+
EXPECT_CALL(delegate, OnScrollInertiaCancel(device_id)).Times(0);
362+
handler->OnViewportStatusChanged((IDirectManipulationViewport*)&viewport,
363+
DIRECTMANIPULATION_READY,
364+
DIRECTMANIPULATION_INERTIA);
365+
}
366+
262367
} // namespace testing
263368
} // namespace flutter

shell/platform/windows/flutter_windows_view.cc

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,11 @@ void FlutterWindowsView::OnScroll(double x,
264264
device_id);
265265
}
266266

267+
void FlutterWindowsView::OnScrollInertiaCancel(int32_t device_id) {
268+
PointerLocation point = binding_handler_->GetPrimaryPointerLocation();
269+
SendScrollInertiaCancel(device_id, point.x, point.y);
270+
}
271+
267272
void FlutterWindowsView::OnUpdateSemanticsEnabled(bool enabled) {
268273
engine_->UpdateSemanticsEnabled(enabled);
269274
}
@@ -500,6 +505,21 @@ void FlutterWindowsView::SendScroll(double x,
500505
SendPointerEventWithData(event, state);
501506
}
502507

508+
void FlutterWindowsView::SendScrollInertiaCancel(int32_t device_id,
509+
double x,
510+
double y) {
511+
auto state =
512+
GetOrCreatePointerState(kFlutterPointerDeviceKindTrackpad, device_id);
513+
514+
FlutterPointerEvent event = {};
515+
event.x = x;
516+
event.y = y;
517+
event.signal_kind =
518+
FlutterPointerSignalKind::kFlutterPointerSignalKindScrollInertiaCancel;
519+
SetEventPhaseFromCursorButtonState(&event, state);
520+
SendPointerEventWithData(event, state);
521+
}
522+
503523
void FlutterWindowsView::SendPointerEventWithData(
504524
const FlutterPointerEvent& event_data,
505525
PointerState* state) {

shell/platform/windows/flutter_windows_view.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,9 @@ class FlutterWindowsView : public WindowBindingHandlerDelegate,
180180
FlutterPointerDeviceKind device_kind,
181181
int32_t device_id) override;
182182

183+
// |WindowBindingHandlerDelegate|
184+
void OnScrollInertiaCancel(int32_t device_id) override;
185+
183186
// |WindowBindingHandlerDelegate|
184187
virtual void OnUpdateSemanticsEnabled(bool enabled) override;
185188

@@ -331,6 +334,9 @@ class FlutterWindowsView : public WindowBindingHandlerDelegate,
331334
FlutterPointerDeviceKind device_kind,
332335
int32_t device_id);
333336

337+
// Reports scroll inertia cancel events to Flutter engine.
338+
void SendScrollInertiaCancel(int32_t device_id, double x, double y);
339+
334340
// Creates a PointerState object unless it already exists.
335341
PointerState* GetOrCreatePointerState(FlutterPointerDeviceKind device_kind,
336342
int32_t device_id);

shell/platform/windows/testing/mock_window_binding_handler_delegate.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ class MockWindowBindingHandlerDelegate : public WindowBindingHandlerDelegate {
6060
int,
6161
FlutterPointerDeviceKind,
6262
int32_t));
63+
MOCK_METHOD1(OnScrollInertiaCancel, void(int32_t));
6364
MOCK_METHOD0(OnPlatformBrightnessChanged, void());
6465
MOCK_METHOD1(UpdateHighContrastEnabled, void(bool enabled));
6566
};

shell/platform/windows/window_binding_handler_delegate.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,10 @@ class WindowBindingHandlerDelegate {
122122
FlutterPointerDeviceKind device_kind,
123123
int32_t device_id) = 0;
124124

125+
// Notifies delegate that scroll inertia should be cancelled.
126+
// Typically called by DirectManipulationEventHandler
127+
virtual void OnScrollInertiaCancel(int32_t device_id) = 0;
128+
125129
// Notifies delegate that the Flutter semantics tree should be enabled or
126130
// disabled.
127131
virtual void OnUpdateSemanticsEnabled(bool enabled) = 0;

0 commit comments

Comments
 (0)