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

Scroll inertia cancel for Win32 #34452

Merged
merged 4 commits into from
Oct 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
40 changes: 32 additions & 8 deletions shell/platform/windows/direct_manipulation.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

#include "flutter/fml/logging.h"

#include <algorithm>

#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"
Expand All @@ -24,6 +26,10 @@

namespace flutter {

int32_t DirectManipulationEventHandler::GetDeviceId() {
return (int32_t) reinterpret_cast<int64_t>(this);
}

STDMETHODIMP DirectManipulationEventHandler::QueryInterface(REFIID iid,
void** ppv) {
if ((iid == IID_IUnknown) ||
Expand All @@ -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<int64_t>(this));
owner_->binding_handler_delegate->OnPointerPanZoomStart(GetDeviceId());
}
}
} else if (previous == DIRECTMANIPULATION_RUNNING) {
}
if (previous == DIRECTMANIPULATION_RUNNING) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we combine the previous if clause and this one into

Suggested change
if (previous == DIRECTMANIPULATION_RUNNING) {
if (previous == DIRECTMANIPULATION_RUNNING) {
if (during_synthesized_reset_) {
during_synthesized_reset_ = false;
} else if (current == DIRECTMANIPULATION_RUNNING) {
...
}
// Reset deltas to ensure only inertia values will be compared later.
...

(Did I suggest the current way when I first reviewed this file? Sorry if I did.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can't do that since the else if would no longer make any sense (previous and current state will not ever be the same).

// 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<int64_t>(this));
owner_->binding_handler_delegate->OnPointerPanZoomEnd(GetDeviceId());
}
} else if (previous == DIRECTMANIPULATION_INERTIA) {
Copy link
Contributor

Choose a reason for hiding this comment

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

We only compare the previous status but not the current, does it mean if the inertia movement dies out naturally it will also dispatch a cancel event?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Current should always be DIRECTMANIPULATION_READY whether it dies out naturally or is cancelled. We can only tell if it's cancelled prematurely by seeing if the last event still had meaningful velocity left (hadn't already decelerated to near-zero).

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)) {
Expand Down Expand Up @@ -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<int64_t>(this), pan_x, pan_y, scale, 0);
GetDeviceId(), pan_x, pan_y, scale, 0);
}
}
return S_OK;
Expand Down Expand Up @@ -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));
Expand Down
11 changes: 11 additions & 0 deletions shell/platform/windows/direct_manipulation.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
105 changes: 105 additions & 0 deletions shell/platform/windows/direct_manipulation_unittests.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<DirectManipulationOwner>(nullptr);
owner->SetBindingHandlerDelegate(&delegate);
auto handler =
fml::MakeRefCounted<DirectManipulationEventHandler>(owner.get());
int32_t device_id = (int32_t) reinterpret_cast<int64_t>(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<DirectManipulationOwner>(nullptr);
owner->SetBindingHandlerDelegate(&delegate);
auto handler =
fml::MakeRefCounted<DirectManipulationEventHandler>(owner.get());
int32_t device_id = (int32_t) reinterpret_cast<int64_t>(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
20 changes: 20 additions & 0 deletions shell/platform/windows/flutter_windows_view.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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) {
Expand Down
6 changes: 6 additions & 0 deletions shell/platform/windows/flutter_windows_view.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
};
Expand Down
4 changes: 4 additions & 0 deletions shell/platform/windows/window_binding_handler_delegate.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down