blob: 36516ef532b8f4d570c401ab8c609c7e8dab45a9 [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/platform/widget/input/elastic_overscroll_controller_bezier.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"
#include "ui/gfx/geometry/vector2d_conversions.h"
namespace blink {
using gfx::Size;
using gfx::Vector2dF;
using PhaseState = WebGestureEvent::InertialPhaseState;
class MockScrollElasticityHelper : public cc::ScrollElasticityHelper {
public:
MockScrollElasticityHelper() = default;
~MockScrollElasticityHelper() override = default;
// cc::ScrollElasticityHelper implementation:
Size ScrollBounds() const override { return Size(1000, 1000); }
bool IsUserScrollable() const override { return is_user_scrollable_; }
void SetUserScrollable(bool is_user_scrollable) {
is_user_scrollable_ = is_user_scrollable;
}
Vector2dF StretchAmount() const override { return stretch_amount_; }
void SetStretchAmount(const Vector2dF& stretch_amount) override {
stretch_amount_ = stretch_amount;
}
void ScrollBy(const 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:
bool is_user_scrollable_ = true;
Vector2dF stretch_amount_;
gfx::ScrollOffset scroll_offset_, max_scroll_offset_;
};
class ElasticOverscrollControllerBezierTest : public testing::Test {
public:
ElasticOverscrollControllerBezierTest() : controller_(&helper_) {}
~ElasticOverscrollControllerBezierTest() override = default;
void SetUp() override {}
void SendGestureScrollBegin(PhaseState 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(PhaseState inertialPhase,
const Vector2dF& unused_scroll_delta) {
blink::WebGestureEvent event(WebInputEvent::Type::kGestureScrollUpdate,
WebInputEvent::kNoModifiers, base::TimeTicks(),
blink::WebGestureDevice::kTouchpad);
event.data.scroll_update.inertial_phase = inertialPhase;
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(base::TimeTicks time = base::TimeTicks::Now()) {
WebGestureEvent event(WebInputEvent::Type::kGestureScrollEnd,
WebInputEvent::kNoModifiers, time,
WebGestureDevice::kTouchpad);
controller_.ObserveGestureEventAndResult(event,
cc::InputHandlerScrollResult());
}
MockScrollElasticityHelper helper_;
ElasticOverscrollControllerBezier controller_;
};
// Tests that the scroller "stretches" as expected when an overscroll occurs.
TEST_F(ElasticOverscrollControllerBezierTest, VerifyOverscrollStretch) {
// Test vertical overscroll.
SendGestureScrollBegin(PhaseState::kNonMomentum);
EXPECT_EQ(Vector2dF(0, 0), helper_.StretchAmount());
SendGestureScrollUpdate(PhaseState::kNonMomentum, Vector2dF(0, -100));
EXPECT_EQ(Vector2dF(0, -19), helper_.StretchAmount());
SendGestureScrollUpdate(PhaseState::kNonMomentum, Vector2dF(0, 100));
EXPECT_EQ(Vector2dF(0, 0), helper_.StretchAmount());
SendGestureScrollEnd();
// Test horizontal overscroll.
SendGestureScrollBegin(PhaseState::kNonMomentum);
EXPECT_EQ(Vector2dF(0, 0), helper_.StretchAmount());
SendGestureScrollUpdate(PhaseState::kNonMomentum, Vector2dF(-100, 0));
EXPECT_EQ(Vector2dF(-19, 0), helper_.StretchAmount());
SendGestureScrollUpdate(PhaseState::kNonMomentum, Vector2dF(100, 0));
EXPECT_EQ(Vector2dF(0, 0), helper_.StretchAmount());
SendGestureScrollEnd();
}
// Verify that ReconcileStretchAndScroll reduces the overscrolled delta.
TEST_F(ElasticOverscrollControllerBezierTest, ReconcileStretchAndScroll) {
// Test vertical overscroll.
SendGestureScrollBegin(PhaseState::kNonMomentum);
helper_.SetScrollOffsetAndMaxScrollOffset(gfx::ScrollOffset(0, 0),
gfx::ScrollOffset(100, 100));
SendGestureScrollUpdate(PhaseState::kNonMomentum, Vector2dF(0, -100));
EXPECT_EQ(Vector2dF(0, -19), helper_.StretchAmount());
helper_.ScrollBy(Vector2dF(0, 1));
controller_.ReconcileStretchAndScroll();
EXPECT_EQ(Vector2dF(0, -18), helper_.StretchAmount());
// Reset vertical overscroll.
helper_.SetStretchAmount(Vector2dF(0, 0));
SendGestureScrollEnd(base::TimeTicks::Now());
// Test horizontal overscroll.
SendGestureScrollBegin(PhaseState::kNonMomentum);
helper_.SetScrollOffsetAndMaxScrollOffset(gfx::ScrollOffset(0, 0),
gfx::ScrollOffset(100, 100));
SendGestureScrollUpdate(PhaseState::kNonMomentum, Vector2dF(-100, 0));
EXPECT_EQ(Vector2dF(-19, 0), helper_.StretchAmount());
helper_.ScrollBy(Vector2dF(1, 0));
controller_.ReconcileStretchAndScroll();
EXPECT_EQ(Vector2dF(-18, 0), helper_.StretchAmount());
}
// Tests that momentum_animation_start_time_ doesn't get reset when the
// overscroll animation is ticking and the scroller is diagonally overscrolled.
TEST_F(ElasticOverscrollControllerBezierTest, VerifyInitialStretchDelta) {
// Set up the state to be in kStateMomentumAnimated with some amount of
// diagonal stretch.
controller_.state_ =
ElasticOverscrollController::State::kStateMomentumAnimated;
helper_.SetStretchAmount(Vector2dF(5, 10));
helper_.SetScrollOffsetAndMaxScrollOffset(gfx::ScrollOffset(0, 20),
gfx::ScrollOffset(100, 100));
controller_.ReconcileStretchAndScroll();
controller_.bounce_forwards_duration_x_ =
base::TimeDelta::FromMilliseconds(1000);
controller_.bounce_forwards_duration_y_ =
base::TimeDelta::FromMilliseconds(1000);
controller_.momentum_animation_initial_stretch_ = gfx::Vector2dF(10.f, 10.f);
// Verify that the momentum_animation_start_time_ doesn't get reset when the
// animation ticks.
const base::TimeTicks animation_start_time =
base::TimeTicks() + base::TimeDelta::FromMilliseconds(32);
// After 2 frames.
controller_.Animate(animation_start_time);
helper_.ScrollBy(Vector2dF(0, 2));
EXPECT_NE(controller_.momentum_animation_start_time_, animation_start_time);
EXPECT_EQ(controller_.state_,
ElasticOverscrollController::State::kStateMomentumAnimated);
// After 8 frames.
controller_.Animate(animation_start_time +
base::TimeDelta::FromMilliseconds(128));
helper_.ScrollBy(Vector2dF(0, 8));
EXPECT_NE(controller_.momentum_animation_start_time_, animation_start_time);
EXPECT_EQ(controller_.state_,
ElasticOverscrollController::State::kStateMomentumAnimated);
// After 64 frames the forward animation should no longer be active.
controller_.Animate(animation_start_time +
base::TimeDelta::FromMilliseconds(1024));
helper_.ScrollBy(Vector2dF(0, 64));
EXPECT_NE(controller_.momentum_animation_start_time_, animation_start_time);
EXPECT_EQ(controller_.state_,
ElasticOverscrollController::State::kStateInactive);
EXPECT_EQ(Vector2dF(), helper_.StretchAmount());
}
// Tests if the overscrolled delta maps correctly to the actual amount that the
// scroller gets stretched.
TEST_F(ElasticOverscrollControllerBezierTest, VerifyOverscrollBounceDistance) {
Vector2dF overscroll_bounce_distance(
controller_.StretchAmountForAccumulatedOverscroll(Vector2dF(0, -100)));
EXPECT_EQ(overscroll_bounce_distance.y(), -19);
overscroll_bounce_distance =
controller_.StretchAmountForAccumulatedOverscroll(Vector2dF(-100, 0));
EXPECT_EQ(overscroll_bounce_distance.x(), -19);
}
// Tests that the bounce back animation ticks as expected. If the animation was
// successfully created, the call to OverscrollBounceController::Animate should
// tick the animation as expected. When the stretch amount is near 0, the
// scroller should treat the bounce as "completed".
TEST_F(ElasticOverscrollControllerBezierTest, VerifyBackwardAnimationTick) {
// Test vertical overscroll.
EXPECT_EQ(controller_.state_, ElasticOverscrollController::kStateInactive);
SendGestureScrollBegin(PhaseState::kNonMomentum);
EXPECT_EQ(Vector2dF(0, 0), helper_.StretchAmount());
SendGestureScrollUpdate(PhaseState::kNonMomentum, Vector2dF(0, -100));
// This signals that the finger has lifted off which triggers the bounce back
// animation.
const base::TimeTicks now = base::TimeTicks::Now();
SendGestureScrollEnd(now);
// Frame 2.
controller_.Animate(now + base::TimeDelta::FromMilliseconds(32));
EXPECT_EQ(controller_.state_,
ElasticOverscrollController::kStateMomentumAnimated);
ASSERT_FLOAT_EQ(helper_.StretchAmount().y(), -14);
// Frame 5. The stretch amount moving closer to 0 proves that we're animating.
controller_.Animate(now + base::TimeDelta::FromMilliseconds(80));
ASSERT_FLOAT_EQ(helper_.StretchAmount().y(), -8);
// Frame 15. StretchAmount < abs(1), so snap to 0. state_ is kStateInactive.
controller_.Animate(now + base::TimeDelta::FromMilliseconds(240));
EXPECT_EQ(Vector2dF(0, 0), helper_.StretchAmount());
EXPECT_EQ(controller_.state_, ElasticOverscrollController::kStateInactive);
// Test horizontal overscroll.
SendGestureScrollBegin(PhaseState::kNonMomentum);
EXPECT_EQ(Vector2dF(0, 0), helper_.StretchAmount());
SendGestureScrollUpdate(PhaseState::kNonMomentum, Vector2dF(-80, 0));
SendGestureScrollEnd(now);
// Frame 2.
controller_.Animate(now + base::TimeDelta::FromMilliseconds(32));
ASSERT_FLOAT_EQ(helper_.StretchAmount().x(), -10);
// Frame 5. The stretch amount moving closer to 0 proves that we're animating.
controller_.Animate(now + base::TimeDelta::FromMilliseconds(80));
EXPECT_EQ(controller_.state_,
ElasticOverscrollController::kStateMomentumAnimated);
ASSERT_FLOAT_EQ(helper_.StretchAmount().x(), -5);
// Frame 15. StretchAmount < abs(1), so snap to 0. state_ is kStateInactive.
controller_.Animate(now + base::TimeDelta::FromMilliseconds(240));
EXPECT_EQ(Vector2dF(0, 0), helper_.StretchAmount());
EXPECT_EQ(controller_.state_, ElasticOverscrollController::kStateInactive);
}
// Tests that the bounce forward animation ticks as expected.
TEST_F(ElasticOverscrollControllerBezierTest, VerifyForwardAnimationTick) {
// Test vertical forward bounce animations.
EXPECT_EQ(controller_.state_, ElasticOverscrollController::kStateInactive);
SendGestureScrollBegin(PhaseState::kNonMomentum);
EXPECT_EQ(Vector2dF(0, 0), helper_.StretchAmount());
SendGestureScrollUpdate(PhaseState::kNonMomentum, Vector2dF(0, -100));
controller_.scroll_velocity_ = gfx::Vector2dF(0.f, -4000.f);
// This signals that the finger has lifted off which triggers a fling.
const base::TimeTicks now = base::TimeTicks::Now();
SendGestureScrollEnd(now);
const int TOTAL_FRAMES = 28;
const int stretch_amount_y[TOTAL_FRAMES] = {
-19, -41, -55, -65, -72, -78, -82, -85, -88, -89, -78, -64, -53, -44,
-37, -30, -25, -20, -16, -13, -10, -7, -5, -4, -2, -1, -1, 0};
for (int i = 0; i < TOTAL_FRAMES; i++) {
controller_.Animate(now + base::TimeDelta::FromMilliseconds(i * 16));
EXPECT_EQ(controller_.state_,
(stretch_amount_y[i] == 0
? ElasticOverscrollController::kStateInactive
: ElasticOverscrollController::kStateMomentumAnimated));
ASSERT_FLOAT_EQ(helper_.StretchAmount().y(), stretch_amount_y[i]);
}
// Test horizontal forward bounce animations.
SendGestureScrollBegin(PhaseState::kNonMomentum);
SendGestureScrollUpdate(PhaseState::kNonMomentum, Vector2dF(-50, 0));
controller_.scroll_velocity_ = gfx::Vector2dF(-3000.f, 0.f);
SendGestureScrollEnd(now);
const int stretch_amount_x[TOTAL_FRAMES] = {
-9, -24, -34, -42, -48, -54, -58, -62, -66, -69, -62, -52, -43, -36,
-30, -25, -20, -17, -13, -10, -8, -6, -4, -3, -2, -1, 0, 0};
for (int i = 0; i < TOTAL_FRAMES; i++) {
controller_.Animate(now + base::TimeDelta::FromMilliseconds(i * 16));
EXPECT_EQ(controller_.state_,
(stretch_amount_x[i] == 0
? ElasticOverscrollController::kStateInactive
: ElasticOverscrollController::kStateMomentumAnimated));
ASSERT_FLOAT_EQ(helper_.StretchAmount().x(), stretch_amount_x[i]);
}
}
// Tests that the bounce forward animation is *not* played when the velocity is
// less than kIgnoreForwardBounceVelocityThreshold. This can be verified by
// checking bounce_forwards_distance_ (since it is a function of velocity)
TEST_F(ElasticOverscrollControllerBezierTest,
VerifyForwardAnimationIsNotPlayed) {
EXPECT_EQ(Vector2dF(), helper_.StretchAmount());
controller_.scroll_velocity_ = gfx::Vector2dF(0.f, -199.f);
controller_.DidEnterMomentumAnimatedState();
EXPECT_TRUE(controller_.bounce_forwards_distance_.IsZero());
controller_.scroll_velocity_ = gfx::Vector2dF(-199.f, 0.f);
controller_.DidEnterMomentumAnimatedState();
EXPECT_TRUE(controller_.bounce_forwards_distance_.IsZero());
// When velocity > 200, forward animation is expected to be played.
controller_.scroll_velocity_ = gfx::Vector2dF(0.f, -201.f);
controller_.DidEnterMomentumAnimatedState();
EXPECT_EQ(gfx::Vector2dF(0, -16),
gfx::ToRoundedVector2d(controller_.bounce_forwards_distance_));
controller_.scroll_velocity_ = gfx::Vector2dF(-201.f, 0.f);
controller_.DidEnterMomentumAnimatedState();
EXPECT_EQ(gfx::Vector2dF(-16, 0),
gfx::ToRoundedVector2d(controller_.bounce_forwards_distance_));
}
// Tests initiating a scroll when a bounce back animation is in progress works
// as expected.
TEST_F(ElasticOverscrollControllerBezierTest, VerifyScrollDuringBounceBack) {
// Test vertical overscroll.
SendGestureScrollBegin(PhaseState::kNonMomentum);
EXPECT_EQ(Vector2dF(0, 0), helper_.StretchAmount());
SendGestureScrollUpdate(PhaseState::kNonMomentum, Vector2dF(0, -100));
// This signals that the finger has lifted off which triggers the bounce back
// animation.
const base::TimeTicks now = base::TimeTicks::Now();
SendGestureScrollEnd(now);
EXPECT_EQ(Vector2dF(0, -19), helper_.StretchAmount());
// Frame 2.
controller_.Animate(now + base::TimeDelta::FromMilliseconds(32));
ASSERT_FLOAT_EQ(helper_.StretchAmount().y(), -14);
// Frame 5. The stretch amount moving closer to 0 proves that we're animating.
controller_.Animate(now + base::TimeDelta::FromMilliseconds(80));
ASSERT_FLOAT_EQ(helper_.StretchAmount().y(), -8);
// While the animation is still ticking, initiate a scroll.
SendGestureScrollBegin(PhaseState::kNonMomentum);
SendGestureScrollUpdate(PhaseState::kNonMomentum, Vector2dF(0, -50));
ASSERT_FLOAT_EQ(helper_.StretchAmount().y(), -17);
}
// Tests that animation doesn't get created when unused_delta is 0.
TEST_F(ElasticOverscrollControllerBezierTest, VerifyAnimationNotCreated) {
// Test vertical and horizontal overscroll.
SendGestureScrollBegin(PhaseState::kNonMomentum);
EXPECT_EQ(Vector2dF(0, 0), helper_.StretchAmount());
// state_ is kStateActiveScroll. unused_delta is 0 so overscroll should not
// take place.
SendGestureScrollUpdate(PhaseState::kNonMomentum, Vector2dF(0, 0));
// This signals that the finger has lifted off which triggers the bounce back
// animation.
const base::TimeTicks now = base::TimeTicks::Now();
SendGestureScrollEnd(now);
// Frame 2.
controller_.Animate(now + base::TimeDelta::FromMilliseconds(32));
EXPECT_EQ(Vector2dF(0, 0), helper_.StretchAmount());
}
} // namespace blink