diff --git a/shell/platform/windows/direct_manipulation.cc b/shell/platform/windows/direct_manipulation.cc index 02e0167fbf4e2..41ddf07b5acec 100644 --- a/shell/platform/windows/direct_manipulation.cc +++ b/shell/platform/windows/direct_manipulation.cc @@ -4,6 +4,8 @@ #include "flutter/fml/logging.h" +#include + #include "flutter/shell/platform/windows/direct_manipulation.h" #include "flutter/shell/platform/windows/window.h" #include "flutter/shell/platform/windows/window_binding_handler_delegate.h" @@ -24,6 +26,10 @@ namespace flutter { +int32_t DirectManipulationEventHandler::GetDeviceId() { + return (int32_t) reinterpret_cast(this); +} + STDMETHODIMP DirectManipulationEventHandler::QueryInterface(REFIID iid, void** ppv) { if ((iid == IID_IUnknown) || @@ -43,26 +49,39 @@ HRESULT DirectManipulationEventHandler::OnViewportStatusChanged( IDirectManipulationViewport* viewport, DIRECTMANIPULATION_STATUS current, DIRECTMANIPULATION_STATUS previous) { + during_inertia_ = current == DIRECTMANIPULATION_INERTIA; if (during_synthesized_reset_ && previous == DIRECTMANIPULATION_RUNNING) { during_synthesized_reset_ = false; } else if (current == DIRECTMANIPULATION_RUNNING) { if (!during_synthesized_reset_) { // Not a false event. if (owner_->binding_handler_delegate) { - owner_->binding_handler_delegate->OnPointerPanZoomStart( - (int32_t) reinterpret_cast(this)); + owner_->binding_handler_delegate->OnPointerPanZoomStart(GetDeviceId()); } } - } else if (previous == DIRECTMANIPULATION_RUNNING) { + } + if (previous == DIRECTMANIPULATION_RUNNING) { + // Reset deltas to ensure only inertia values will be compared later. + last_pan_delta_x_ = 0.0; + last_pan_delta_y_ = 0.0; if (owner_->binding_handler_delegate) { - owner_->binding_handler_delegate->OnPointerPanZoomEnd( - (int32_t) reinterpret_cast(this)); + owner_->binding_handler_delegate->OnPointerPanZoomEnd(GetDeviceId()); + } + } else if (previous == DIRECTMANIPULATION_INERTIA) { + if (owner_->binding_handler_delegate && + (std::max)(std::abs(last_pan_delta_x_), std::abs(last_pan_delta_y_)) > + 0.01) { + owner_->binding_handler_delegate->OnScrollInertiaCancel(GetDeviceId()); } // Need to reset the content transform to its original position // so that we are ready for the next gesture. // Use during_synthesized_reset_ flag to prevent sending reset also to the // framework. during_synthesized_reset_ = true; + last_pan_x_ = 0.0; + last_pan_y_ = 0.0; + last_pan_delta_x_ = 0.0; + last_pan_delta_y_ = 0.0; RECT rect; HRESULT hr = viewport->GetViewportRect(&rect); if (FAILED(hr)) { @@ -104,9 +123,13 @@ HRESULT DirectManipulationEventHandler::OnContentUpdated( float scale = c - (c - transform[0]); float pan_x = transform[4]; float pan_y = transform[5]; - if (owner_->binding_handler_delegate) { + last_pan_delta_x_ = pan_x - last_pan_x_; + last_pan_delta_y_ = pan_y - last_pan_y_; + last_pan_x_ = pan_x; + last_pan_y_ = pan_y; + if (owner_->binding_handler_delegate && !during_inertia_) { owner_->binding_handler_delegate->OnPointerPanZoomUpdate( - (int32_t) reinterpret_cast(this), pan_x, pan_y, scale, 0); + GetDeviceId(), pan_x, pan_y, scale, 0); } } return S_OK; @@ -144,7 +167,8 @@ int DirectManipulationOwner::Init(unsigned int width, unsigned int height) { DIRECTMANIPULATION_CONFIGURATION_INTERACTION | DIRECTMANIPULATION_CONFIGURATION_TRANSLATION_X | DIRECTMANIPULATION_CONFIGURATION_TRANSLATION_Y | - DIRECTMANIPULATION_CONFIGURATION_SCALING; + DIRECTMANIPULATION_CONFIGURATION_SCALING | + DIRECTMANIPULATION_CONFIGURATION_TRANSLATION_INERTIA; RETURN_IF_FAILED(viewport_->ActivateConfiguration(configuration)); RETURN_IF_FAILED(viewport_->SetViewportOptions( DIRECTMANIPULATION_VIEWPORT_OPTIONS_MANUALUPDATE)); diff --git a/shell/platform/windows/direct_manipulation.h b/shell/platform/windows/direct_manipulation.h index 40317333f6916..e9aef840fdafa 100644 --- a/shell/platform/windows/direct_manipulation.h +++ b/shell/platform/windows/direct_manipulation.h @@ -106,12 +106,23 @@ class DirectManipulationEventHandler DIRECTMANIPULATION_INTERACTION_TYPE interaction) override; private: + // Unique identifier to associate with all gesture event updates. + int32_t GetDeviceId(); // Parent object, used to store the target for gesture event updates. DirectManipulationOwner* owner_; // We need to reset some parts of DirectManipulation after each gesture // A flag is needed to ensure that false events created as the reset occurs // are not sent to the flutter framework. bool during_synthesized_reset_ = false; + // Store whether current events are from synthetic inertia rather than user + // input. + bool during_inertia_ = false; + // Store the difference between the last pan offsets to determine if inertia + // has been cancelled in the middle of an animation. + float last_pan_x_ = 0.0; + float last_pan_y_ = 0.0; + float last_pan_delta_x_ = 0.0; + float last_pan_delta_y_ = 0.0; }; } // namespace flutter diff --git a/shell/platform/windows/direct_manipulation_unittests.cc b/shell/platform/windows/direct_manipulation_unittests.cc index 5975103e849b8..1d03a0396ddde 100644 --- a/shell/platform/windows/direct_manipulation_unittests.cc +++ b/shell/platform/windows/direct_manipulation_unittests.cc @@ -259,5 +259,110 @@ TEST(DirectManipulationTest, TestRounding) { DIRECTMANIPULATION_INERTIA); } +TEST(DirectManipulationTest, TestInertiaCancelSentForUserCancel) { + MockIDirectManipulationContent content; + MockWindowBindingHandlerDelegate delegate; + MockIDirectManipulationViewport viewport; + const int DISPLAY_WIDTH = 800; + const int DISPLAY_HEIGHT = 600; + auto owner = std::make_unique(nullptr); + owner->SetBindingHandlerDelegate(&delegate); + auto handler = + fml::MakeRefCounted(owner.get()); + int32_t device_id = (int32_t) reinterpret_cast(handler.get()); + // No need to mock the actual gesture, just start at the end. + EXPECT_CALL(viewport, GetViewportRect(_)) + .WillOnce(::testing::Invoke([DISPLAY_WIDTH, DISPLAY_HEIGHT](RECT* rect) { + rect->left = 0; + rect->top = 0; + rect->right = DISPLAY_WIDTH; + rect->bottom = DISPLAY_HEIGHT; + return S_OK; + })); + EXPECT_CALL(viewport, ZoomToRect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT, false)) + .WillOnce(::testing::Return(S_OK)); + EXPECT_CALL(delegate, OnPointerPanZoomEnd(device_id)); + handler->OnViewportStatusChanged((IDirectManipulationViewport*)&viewport, + DIRECTMANIPULATION_INERTIA, + DIRECTMANIPULATION_RUNNING); + // Have pan_y change by 10 between inertia updates. + EXPECT_CALL(content, GetContentTransform(_, 6)) + .WillOnce(::testing::Invoke([](float* transform, DWORD size) { + transform[0] = 1; + transform[4] = 0; + transform[5] = 100; + return S_OK; + })); + handler->OnContentUpdated((IDirectManipulationViewport*)&viewport, + (IDirectManipulationContent*)&content); + EXPECT_CALL(content, GetContentTransform(_, 6)) + .WillOnce(::testing::Invoke([](float* transform, DWORD size) { + transform[0] = 1; + transform[4] = 0; + transform[5] = 110; + return S_OK; + })); + handler->OnContentUpdated((IDirectManipulationViewport*)&viewport, + (IDirectManipulationContent*)&content); + // This looks like an interruption in the middle of synthetic inertia because + // of user input. + EXPECT_CALL(delegate, OnScrollInertiaCancel(device_id)); + handler->OnViewportStatusChanged((IDirectManipulationViewport*)&viewport, + DIRECTMANIPULATION_READY, + DIRECTMANIPULATION_INERTIA); +} + +TEST(DirectManipulationTest, TestInertiaCamcelNotSentAtInertiaEnd) { + MockIDirectManipulationContent content; + MockWindowBindingHandlerDelegate delegate; + MockIDirectManipulationViewport viewport; + const int DISPLAY_WIDTH = 800; + const int DISPLAY_HEIGHT = 600; + auto owner = std::make_unique(nullptr); + owner->SetBindingHandlerDelegate(&delegate); + auto handler = + fml::MakeRefCounted(owner.get()); + int32_t device_id = (int32_t) reinterpret_cast(handler.get()); + // No need to mock the actual gesture, just start at the end. + EXPECT_CALL(viewport, GetViewportRect(_)) + .WillOnce(::testing::Invoke([DISPLAY_WIDTH, DISPLAY_HEIGHT](RECT* rect) { + rect->left = 0; + rect->top = 0; + rect->right = DISPLAY_WIDTH; + rect->bottom = DISPLAY_HEIGHT; + return S_OK; + })); + EXPECT_CALL(viewport, ZoomToRect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT, false)) + .WillOnce(::testing::Return(S_OK)); + EXPECT_CALL(delegate, OnPointerPanZoomEnd(device_id)); + handler->OnViewportStatusChanged((IDirectManipulationViewport*)&viewport, + DIRECTMANIPULATION_INERTIA, + DIRECTMANIPULATION_RUNNING); + // Have no change in pan between events. + EXPECT_CALL(content, GetContentTransform(_, 6)) + .WillOnce(::testing::Invoke([](float* transform, DWORD size) { + transform[0] = 1; + transform[4] = 0; + transform[5] = 140; + return S_OK; + })); + handler->OnContentUpdated((IDirectManipulationViewport*)&viewport, + (IDirectManipulationContent*)&content); + EXPECT_CALL(content, GetContentTransform(_, 6)) + .WillOnce(::testing::Invoke([](float* transform, DWORD size) { + transform[0] = 1; + transform[4] = 0; + transform[5] = 140; + return S_OK; + })); + handler->OnContentUpdated((IDirectManipulationViewport*)&viewport, + (IDirectManipulationContent*)&content); + // OnScrollInertiaCancel should not be called. + EXPECT_CALL(delegate, OnScrollInertiaCancel(device_id)).Times(0); + handler->OnViewportStatusChanged((IDirectManipulationViewport*)&viewport, + DIRECTMANIPULATION_READY, + DIRECTMANIPULATION_INERTIA); +} + } // namespace testing } // namespace flutter diff --git a/shell/platform/windows/flutter_windows_view.cc b/shell/platform/windows/flutter_windows_view.cc index aefff5d522492..e212b6cf6140a 100644 --- a/shell/platform/windows/flutter_windows_view.cc +++ b/shell/platform/windows/flutter_windows_view.cc @@ -264,6 +264,11 @@ void FlutterWindowsView::OnScroll(double x, device_id); } +void FlutterWindowsView::OnScrollInertiaCancel(int32_t device_id) { + PointerLocation point = binding_handler_->GetPrimaryPointerLocation(); + SendScrollInertiaCancel(device_id, point.x, point.y); +} + void FlutterWindowsView::OnUpdateSemanticsEnabled(bool enabled) { engine_->UpdateSemanticsEnabled(enabled); } @@ -500,6 +505,21 @@ void FlutterWindowsView::SendScroll(double x, SendPointerEventWithData(event, state); } +void FlutterWindowsView::SendScrollInertiaCancel(int32_t device_id, + double x, + double y) { + auto state = + GetOrCreatePointerState(kFlutterPointerDeviceKindTrackpad, device_id); + + FlutterPointerEvent event = {}; + event.x = x; + event.y = y; + event.signal_kind = + FlutterPointerSignalKind::kFlutterPointerSignalKindScrollInertiaCancel; + SetEventPhaseFromCursorButtonState(&event, state); + SendPointerEventWithData(event, state); +} + void FlutterWindowsView::SendPointerEventWithData( const FlutterPointerEvent& event_data, PointerState* state) { diff --git a/shell/platform/windows/flutter_windows_view.h b/shell/platform/windows/flutter_windows_view.h index 45dc6fdb6a40c..6075903309479 100644 --- a/shell/platform/windows/flutter_windows_view.h +++ b/shell/platform/windows/flutter_windows_view.h @@ -180,6 +180,9 @@ class FlutterWindowsView : public WindowBindingHandlerDelegate, FlutterPointerDeviceKind device_kind, int32_t device_id) override; + // |WindowBindingHandlerDelegate| + void OnScrollInertiaCancel(int32_t device_id) override; + // |WindowBindingHandlerDelegate| virtual void OnUpdateSemanticsEnabled(bool enabled) override; @@ -331,6 +334,9 @@ class FlutterWindowsView : public WindowBindingHandlerDelegate, FlutterPointerDeviceKind device_kind, int32_t device_id); + // Reports scroll inertia cancel events to Flutter engine. + void SendScrollInertiaCancel(int32_t device_id, double x, double y); + // Creates a PointerState object unless it already exists. PointerState* GetOrCreatePointerState(FlutterPointerDeviceKind device_kind, int32_t device_id); diff --git a/shell/platform/windows/testing/mock_window_binding_handler_delegate.h b/shell/platform/windows/testing/mock_window_binding_handler_delegate.h index a7ad72cbe2623..03534b2e5ecfd 100644 --- a/shell/platform/windows/testing/mock_window_binding_handler_delegate.h +++ b/shell/platform/windows/testing/mock_window_binding_handler_delegate.h @@ -60,6 +60,7 @@ class MockWindowBindingHandlerDelegate : public WindowBindingHandlerDelegate { int, FlutterPointerDeviceKind, int32_t)); + MOCK_METHOD1(OnScrollInertiaCancel, void(int32_t)); MOCK_METHOD0(OnPlatformBrightnessChanged, void()); MOCK_METHOD1(UpdateHighContrastEnabled, void(bool enabled)); }; diff --git a/shell/platform/windows/window_binding_handler_delegate.h b/shell/platform/windows/window_binding_handler_delegate.h index 38569fda296bb..485fe4384f3cc 100644 --- a/shell/platform/windows/window_binding_handler_delegate.h +++ b/shell/platform/windows/window_binding_handler_delegate.h @@ -122,6 +122,10 @@ class WindowBindingHandlerDelegate { FlutterPointerDeviceKind device_kind, int32_t device_id) = 0; + // Notifies delegate that scroll inertia should be cancelled. + // Typically called by DirectManipulationEventHandler + virtual void OnScrollInertiaCancel(int32_t device_id) = 0; + // Notifies delegate that the Flutter semantics tree should be enabled or // disabled. virtual void OnUpdateSemanticsEnabled(bool enabled) = 0;