Commit a675fe0f authored by Rahul Arakeri's avatar Rahul Arakeri Committed by Commit Bot

New Windows root scroller overscroll - Part 1.

This CL allows the root scroller to be overscrolled beyond the scroll
bounds. The OverscrollBounceController drives this. The basic principle
here is: the scroll deltas on GSU are fed into a hyperbolic tangent
function (ranging [0, 2]). This tells how far the scroller bounds can
be "stretched" when the user overscrolls. See OverscrollBounceDistance
for details. Animating the bounce back is not in the scope of this CL.

Bug: 1058071
Change-Id: I3023c69078ffc6dff0ecfe3554d699ea3b9d1553
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2198596
Commit-Queue: Rahul Arakeri <arakeri@microsoft.com>
Reviewed-by: default avatarDavid Bokan <bokan@chromium.org>
Cr-Commit-Position: refs/heads/master@{#772484}
parent 784ed213
......@@ -18,6 +18,7 @@ class ScrollElasticityHelperImpl : public ScrollElasticityHelper {
bool IsUserScrollable() const override;
gfx::Vector2dF StretchAmount() const override;
gfx::Size ScrollBounds() const override;
void SetStretchAmount(const gfx::Vector2dF& stretch_amount) override;
gfx::ScrollOffset ScrollOffset() const override;
gfx::ScrollOffset MaxScrollOffset() const override;
......@@ -46,6 +47,12 @@ gfx::Vector2dF ScrollElasticityHelperImpl::StretchAmount() const {
return host_impl_->active_tree()->elastic_overscroll()->Current(true);
}
gfx::Size ScrollElasticityHelperImpl::ScrollBounds() const {
return host_impl_->OuterViewportScrollNode()
? host_impl_->OuterViewportScrollNode()->container_bounds
: gfx::Size();
}
void ScrollElasticityHelperImpl::SetStretchAmount(
const gfx::Vector2dF& stretch_amount) {
if (stretch_amount == StretchAmount())
......
......@@ -8,6 +8,7 @@
#include "base/time/time.h"
#include "cc/cc_export.h"
#include "ui/gfx/geometry/scroll_offset.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/geometry/vector2d_f.h"
namespace cc {
......@@ -54,6 +55,9 @@ class CC_EXPORT ScrollElasticityHelper {
virtual bool IsUserScrollable() const = 0;
// The bounds of the root scroller.
virtual gfx::Size ScrollBounds() const = 0;
// The amount that the view is stretched past the normal allowable bounds.
virtual gfx::Vector2dF StretchAmount() const = 0;
virtual void SetStretchAmount(const gfx::Vector2dF& stretch_amount) = 0;
......
......@@ -2014,6 +2014,7 @@ jumbo_source_set("blink_platform_unittests_sources") {
"widget/compositing/test/stub_layer_tree_view_delegate.h",
"widget/input/input_handler_proxy_unittest.cc",
"widget/input/input_scroll_elasticity_controller_unittest.cc",
"widget/input/overscroll_bounce_controller_unittest.cc",
"widget/input/prediction/empty_filter_unittests.cc",
"widget/input/prediction/filter_factory_unittests.cc",
"widget/input/prediction/input_filter_unittest_helpers.cc",
......
......@@ -46,6 +46,8 @@ class MockScrollElasticityHelper : public cc::ScrollElasticityHelper {
set_stretch_amount_count_ += 1;
stretch_amount_ = stretch_amount;
}
gfx::Size ScrollBounds() const override { return gfx::Size(800, 600); }
gfx::ScrollOffset ScrollOffset() const override { return scroll_offset_; }
gfx::ScrollOffset MaxScrollOffset() const override {
return max_scroll_offset_;
......
......@@ -10,9 +10,11 @@
// TODO(arakeri): This is where all the overscroll specific code will go.
namespace blink {
constexpr float kOverscrollBoundaryMultiplier = 0.1f;
OverscrollBounceController::OverscrollBounceController(
cc::ScrollElasticityHelper* helper)
: weak_factory_(this) {}
: state_(kStateInactive), helper_(helper), weak_factory_(this) {}
OverscrollBounceController::~OverscrollBounceController() = default;
......@@ -21,12 +23,149 @@ OverscrollBounceController::GetWeakPtr() {
return weak_factory_.GetWeakPtr();
}
void OverscrollBounceController::ObserveGestureEventAndResult(
const blink::WebGestureEvent& gesture_event,
const cc::InputHandlerScrollResult& scroll_result) {}
void OverscrollBounceController::Animate(base::TimeTicks time) {}
void OverscrollBounceController::ReconcileStretchAndScroll() {}
// TODO(arakeri): ReconcileStretchAndScroll implementations in both the classes
// InputScrollElasticityController and OverscrollBounceController have common
// code that needs to be evaluated and moved up into the base class.
void OverscrollBounceController::ReconcileStretchAndScroll() {
const gfx::Vector2dF stretch = helper_->StretchAmount();
if (stretch.IsZero())
return;
const gfx::ScrollOffset scroll_offset = helper_->ScrollOffset();
const gfx::ScrollOffset max_scroll_offset = helper_->MaxScrollOffset();
float scroll_adjustment_x = 0;
if (stretch.x() < 0.f)
scroll_adjustment_x = scroll_offset.x();
else if (stretch.x() > 0.f)
scroll_adjustment_x = max_scroll_offset.x() - scroll_offset.x();
float scroll_adjustment_y = 0;
if (stretch.y() < 0.f)
scroll_adjustment_y = scroll_offset.y();
else if (stretch.y() > 0.f)
scroll_adjustment_y = max_scroll_offset.y() - scroll_offset.y();
if (state_ == kStateActiveScroll) {
// During an active scroll, we want to reduce |accumulated_scroll_delta_| by
// the amount that was scrolled (but we don't want to over-consume, so limit
// it by the amount of |accumulated_scroll_delta_|).
scroll_adjustment_x = std::copysign(
std::min(std::abs(accumulated_scroll_delta_.x()), scroll_adjustment_x),
stretch.x());
scroll_adjustment_y = std::copysign(
std::min(std::abs(accumulated_scroll_delta_.y()), scroll_adjustment_y),
stretch.y());
accumulated_scroll_delta_ -=
gfx::Vector2dF(scroll_adjustment_x, scroll_adjustment_y);
helper_->SetStretchAmount(OverscrollBounceDistance(
accumulated_scroll_delta_, helper_->ScrollBounds()));
}
helper_->ScrollBy(gfx::Vector2dF(scroll_adjustment_x, scroll_adjustment_y));
}
// Returns the maximum amount to be overscrolled.
gfx::Vector2dF OverscrollBounceController::OverscrollBoundary(
const gfx::Size& scroller_bounds) const {
return gfx::Vector2dF(
scroller_bounds.width() * kOverscrollBoundaryMultiplier,
scroller_bounds.height() * kOverscrollBoundaryMultiplier);
}
// The goal of this calculation is to map the distance the user has scrolled
// past the boundary into the distance to actually scroll the elastic scroller.
gfx::Vector2d OverscrollBounceController::OverscrollBounceDistance(
const gfx::Vector2dF& distance_overscrolled,
const gfx::Size& scroller_bounds) const {
// TODO(arakeri): This should change as you pinch zoom in.
gfx::Vector2dF overscroll_boundary = OverscrollBoundary(scroller_bounds);
// We use the tanh function in addition to the mapping, which gives it more of
// a spring effect. However, we want to use tanh's range from [0, 2], so we
// multiply the value we provide to tanh by 2.
// Also, it may happen that the scroller_bounds are 0 if the viewport scroll
// nodes are null (see: ScrollElasticityHelper::ScrollBounds). We therefore
// have to check in order to avoid a divide by 0.
gfx::Vector2d overbounce_distance;
if (scroller_bounds.width() > 0.f) {
overbounce_distance.set_x(
tanh(2 * distance_overscrolled.x() / scroller_bounds.width()) *
overscroll_boundary.x());
}
if (scroller_bounds.height() > 0.f) {
overbounce_distance.set_y(
tanh(2 * distance_overscrolled.y() / scroller_bounds.height()) *
overscroll_boundary.y());
}
return overbounce_distance;
}
void OverscrollBounceController::EnterStateActiveScroll() {
state_ = kStateActiveScroll;
}
void OverscrollBounceController::ObserveRealScrollBegin(
const blink::WebGestureEvent& gesture_event) {
if (gesture_event.data.scroll_begin.inertial_phase ==
blink::WebGestureEvent::InertialPhaseState::kNonMomentum &&
gesture_event.data.scroll_begin.delta_hint_units ==
ui::ScrollGranularity::kScrollByPrecisePixel) {
EnterStateActiveScroll();
}
}
void OverscrollBounceController::ObserveRealScrollEnd() {
state_ = kStateInactive;
}
void OverscrollBounceController::OverscrollIfNecessary(
const gfx::Vector2dF& overscroll_delta) {
accumulated_scroll_delta_ += overscroll_delta;
gfx::Vector2d overbounce_distance = OverscrollBounceDistance(
accumulated_scroll_delta_, helper_->ScrollBounds());
helper_->SetStretchAmount(overbounce_distance);
}
void OverscrollBounceController::ObserveScrollUpdate(
const gfx::Vector2dF& unused_scroll_delta) {
if (state_ == kStateInactive)
return;
if (state_ == kStateActiveScroll) {
// TODO(arakeri): Implement animate back.
OverscrollIfNecessary(unused_scroll_delta);
}
}
void OverscrollBounceController::ObserveGestureEventAndResult(
const blink::WebGestureEvent& gesture_event,
const cc::InputHandlerScrollResult& scroll_result) {
switch (gesture_event.GetType()) {
case blink::WebInputEvent::Type::kGestureScrollBegin: {
if (gesture_event.data.scroll_begin.synthetic)
return;
ObserveRealScrollBegin(gesture_event);
break;
}
case blink::WebInputEvent::Type::kGestureScrollUpdate: {
gfx::Vector2dF event_delta(-gesture_event.data.scroll_update.delta_x,
-gesture_event.data.scroll_update.delta_y);
ObserveScrollUpdate(scroll_result.unused_scroll_delta);
break;
}
case blink::WebInputEvent::Type::kGestureScrollEnd: {
ObserveRealScrollEnd();
break;
}
default:
break;
}
}
} // namespace blink
......@@ -7,19 +7,17 @@
#include "base/macros.h"
#include "base/memory/weak_ptr.h"
#include "cc/input/input_handler.h"
#include "cc/input/overscroll_behavior.h"
#include "cc/input/scroll_elasticity_helper.h"
#include "third_party/blink/public/common/input/web_gesture_event.h"
#include "third_party/blink/public/platform/input/elastic_overscroll_controller.h"
namespace cc {
struct InputHandlerScrollResult;
} // namespace cc
namespace blink {
// The overbounce version of elastic overscrolling mimics Windows style
// overscroll animations.
class OverscrollBounceController : public ElasticOverscrollController {
class BLINK_PLATFORM_EXPORT OverscrollBounceController
: public ElasticOverscrollController {
public:
explicit OverscrollBounceController(cc::ScrollElasticityHelper* helper);
~OverscrollBounceController() override;
......@@ -31,8 +29,37 @@ class OverscrollBounceController : public ElasticOverscrollController {
const cc::InputHandlerScrollResult& scroll_result) override;
void Animate(base::TimeTicks time) override;
void ReconcileStretchAndScroll() override;
gfx::Vector2d OverscrollBounceDistance(
const gfx::Vector2dF& distance_overscrolled,
const gfx::Size& scroller_bounds) const;
private:
void ObserveRealScrollBegin(const blink::WebGestureEvent& gesture_event);
void ObserveRealScrollEnd();
gfx::Vector2dF OverscrollBoundary(const gfx::Size& scroller_bounds) const;
void EnterStateActiveScroll();
void OverscrollIfNecessary(const gfx::Vector2dF& overscroll_delta);
void ObserveScrollUpdate(const gfx::Vector2dF& unused_scroll_delta);
enum State {
// The initial state, during which the overscroll amount is zero.
kStateInactive,
// ActiveScroll indicates that this controller is listening to future GSU
// events, and those events may or may not update the overscroll amount.
// This occurs when the user is actively panning either via a touchscreen or
// touchpad, or is an active fling that has not triggered an overscroll.
kStateActiveScroll,
};
State state_;
cc::ScrollElasticityHelper* helper_;
// This is the accumulated raw delta in pixels that's been overscrolled. It
// will be fed into a tanh function (ranging [0, 2]) that decides the stretch
// bounds.
gfx::Vector2dF accumulated_scroll_delta_;
base::WeakPtrFactory<OverscrollBounceController> weak_factory_;
DISALLOW_COPY_AND_ASSIGN(OverscrollBounceController);
};
......
// Copyright 2019 The Chromium Authors. All rights reserved.
// Copyright (C) Microsoft Corporation. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// This is a fork of the input_scroll_elasticity_controller_unittest.cc.
#include "third_party/blink/renderer/platform/widget/input/overscroll_bounce_controller.h"
#include "cc/input/input_handler.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/input/web_input_event.h"
#include "third_party/blink/public/common/input/web_mouse_wheel_event.h"
namespace blink {
namespace test {
class MockScrollElasticityHelper : public cc::ScrollElasticityHelper {
public:
MockScrollElasticityHelper() = default;
~MockScrollElasticityHelper() override = default;
// cc::ScrollElasticityHelper implementation:
gfx::Size ScrollBounds() const override { return gfx::Size(1000, 1000); }
bool IsUserScrollable() const override { return false; }
gfx::Vector2dF StretchAmount() const override { return stretch_amount_; }
void SetStretchAmount(const gfx::Vector2dF& stretch_amount) override {
stretch_amount_ = stretch_amount;
}
void ScrollBy(const gfx::Vector2dF& delta) override {
scroll_offset_ += gfx::ScrollOffset(delta);
}
void RequestOneBeginFrame() override {}
gfx::ScrollOffset ScrollOffset() const override { return scroll_offset_; }
gfx::ScrollOffset MaxScrollOffset() const override {
return max_scroll_offset_;
}
void SetScrollOffsetAndMaxScrollOffset(
const gfx::ScrollOffset& scroll_offset,
const gfx::ScrollOffset& max_scroll_offset) {
scroll_offset_ = scroll_offset;
max_scroll_offset_ = max_scroll_offset;
}
private:
gfx::Vector2dF stretch_amount_;
gfx::ScrollOffset scroll_offset_, max_scroll_offset_;
};
class OverscrollBounceControllerTest : public testing::Test {
public:
OverscrollBounceControllerTest() : controller_(&helper_) {}
~OverscrollBounceControllerTest() override = default;
void SetUp() override {}
void SendGestureScrollBegin(
WebGestureEvent::InertialPhaseState inertialPhase) {
WebGestureEvent event(WebInputEvent::Type::kGestureScrollBegin,
WebInputEvent::kNoModifiers, base::TimeTicks(),
WebGestureDevice::kTouchpad);
event.data.scroll_begin.inertial_phase = inertialPhase;
controller_.ObserveGestureEventAndResult(event,
cc::InputHandlerScrollResult());
}
void SendGestureScrollUpdate(
WebGestureEvent::InertialPhaseState inertialPhase,
const gfx::Vector2dF& scroll_delta,
const gfx::Vector2dF& unused_scroll_delta) {
blink::WebGestureEvent event(WebInputEvent::Type::kGestureScrollUpdate,
WebInputEvent::kNoModifiers, base::TimeTicks(),
blink::WebGestureDevice::kTouchpad);
event.data.scroll_update.inertial_phase = inertialPhase;
event.data.scroll_update.delta_x = -scroll_delta.x();
event.data.scroll_update.delta_y = -scroll_delta.y();
cc::InputHandlerScrollResult scroll_result;
scroll_result.did_overscroll_root = !unused_scroll_delta.IsZero();
scroll_result.unused_scroll_delta = unused_scroll_delta;
controller_.ObserveGestureEventAndResult(event, scroll_result);
}
void SendGestureScrollEnd() {
WebGestureEvent event(WebInputEvent::Type::kGestureScrollEnd,
WebInputEvent::kNoModifiers, base::TimeTicks(),
WebGestureDevice::kTouchpad);
controller_.ObserveGestureEventAndResult(event,
cc::InputHandlerScrollResult());
}
MockScrollElasticityHelper helper_;
OverscrollBounceController controller_;
};
// Tests the bounds of the overscroll and that the "StretchAmount" returns back
// to 0 once the overscroll is done.
TEST_F(OverscrollBounceControllerTest, VerifyOverscrollStretch) {
// Test vertical overscroll.
SendGestureScrollBegin(WebGestureEvent::InertialPhaseState::kNonMomentum);
gfx::Vector2dF delta(0, -50);
EXPECT_EQ(gfx::Vector2dF(0, 0), helper_.StretchAmount());
SendGestureScrollUpdate(WebGestureEvent::InertialPhaseState::kNonMomentum,
delta, gfx::Vector2dF(0, -100));
EXPECT_EQ(gfx::Vector2dF(0, -19), helper_.StretchAmount());
SendGestureScrollUpdate(WebGestureEvent::InertialPhaseState::kNonMomentum,
delta, gfx::Vector2dF(0, 100));
EXPECT_EQ(gfx::Vector2dF(0, 0), helper_.StretchAmount());
SendGestureScrollEnd();
// Test horizontal overscroll.
SendGestureScrollBegin(WebGestureEvent::InertialPhaseState::kNonMomentum);
delta = gfx::Vector2dF(-50, 0);
EXPECT_EQ(gfx::Vector2dF(0, 0), helper_.StretchAmount());
SendGestureScrollUpdate(WebGestureEvent::InertialPhaseState::kNonMomentum,
delta, gfx::Vector2dF(-100, 0));
EXPECT_EQ(gfx::Vector2dF(-19, 0), helper_.StretchAmount());
SendGestureScrollUpdate(WebGestureEvent::InertialPhaseState::kNonMomentum,
delta, gfx::Vector2dF(100, 0));
EXPECT_EQ(gfx::Vector2dF(0, 0), helper_.StretchAmount());
SendGestureScrollEnd();
}
// Verify that ReconcileStretchAndScroll reduces the overscrolled delta.
TEST_F(OverscrollBounceControllerTest, ReconcileStretchAndScroll) {
// Test overscroll in both directions.
gfx::Vector2dF delta(0, -50);
SendGestureScrollBegin(WebGestureEvent::InertialPhaseState::kNonMomentum);
helper_.SetScrollOffsetAndMaxScrollOffset(gfx::ScrollOffset(5, 8),
gfx::ScrollOffset(100, 100));
SendGestureScrollUpdate(WebGestureEvent::InertialPhaseState::kNonMomentum,
delta, gfx::Vector2dF(-100, -100));
EXPECT_EQ(gfx::Vector2dF(-19, -19), helper_.StretchAmount());
controller_.ReconcileStretchAndScroll();
EXPECT_EQ(gfx::Vector2dF(-18, -18), helper_.StretchAmount());
// Adjustment of gfx::ScrollOffset(-5, -8) should bring back the
// scroll_offset_ to 0.
EXPECT_EQ(helper_.ScrollOffset(), gfx::ScrollOffset(0, 0));
}
// Tests if the overscrolled delta maps correctly to the actual amount that the
// scroller gets stretched.
TEST_F(OverscrollBounceControllerTest, VerifyOverscrollBounceDistance) {
gfx::Vector2dF overscroll_bounce_distance(
controller_.OverscrollBounceDistance(gfx::Vector2dF(0, -100),
helper_.ScrollBounds()));
EXPECT_EQ(overscroll_bounce_distance.y(), -19);
overscroll_bounce_distance = controller_.OverscrollBounceDistance(
gfx::Vector2dF(-100, 0), helper_.ScrollBounds());
EXPECT_EQ(overscroll_bounce_distance.x(), -19);
}
} // namespace test
} // namespace blink
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment