Commit 12bb4250 authored by Rahul Arakeri's avatar Rahul Arakeri Committed by Commit Bot

New Windows root scroller overscroll - Part 3.

This CL implements the bounce forwards animation for when user performs
a fling. When the scroller reaches the bounds, the residual velocity
determines the extent of the overscroll. This then gets applied to the
scroller followed by the usual bounce back animation.

Bug: 1058071
Change-Id: Id14cfff5e99843bd9849d7122058dc16d929e74c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2337940
Commit-Queue: Rahul Arakeri <arakeri@microsoft.com>
Reviewed-by: default avatarRobert Flack <flackr@chromium.org>
Reviewed-by: default avatarPhilip Rogers <pdr@chromium.org>
Cr-Commit-Position: refs/heads/master@{#795630}
parent c1577e45
......@@ -96,7 +96,8 @@ class PLATFORM_EXPORT ElasticOverscrollController {
virtual gfx::Vector2d AccumulatedOverscrollForStretchAmount(
const gfx::Vector2dF& stretch_amount) const = 0;
gfx::Size GetScrollBounds() const { return helper_->ScrollBounds(); }
gfx::Size scroll_bounds() const { return helper_->ScrollBounds(); }
gfx::Vector2dF scroll_velocity() const { return scroll_velocity_; }
// TODO (arakeri): Need to be cleared when we leave MomentumAnimated.
// Momentum animation state. This state is valid only while the state is
......@@ -106,7 +107,9 @@ class PLATFORM_EXPORT ElasticOverscrollController {
private:
FRIEND_TEST_ALL_PREFIXES(ElasticOverscrollControllerBezierTest,
VerifyAnimationTick);
VerifyBackwardAnimationTick);
FRIEND_TEST_ALL_PREFIXES(ElasticOverscrollControllerBezierTest,
VerifyForwardAnimationTick);
enum State {
// The initial state, during which the overscroll amount is zero and
......
......@@ -19,12 +19,30 @@ constexpr double kBounceBackMaxDurationMilliseconds = 300.0;
// Time taken by the bounce back animation (in milliseconds) to scroll 1 px.
constexpr double kBounceBackMillisecondsPerPixel = 15.0;
// Control points for the Cubic Bezier curve.
const double kOverbounceMaxDurationMilliseconds = 150.0;
const double kOverbounceMillisecondsPerPixel = 2.5;
const float kOverbounceDistanceMultiplier = 35.f;
// Control points for the bounce forward Cubic Bezier curve.
constexpr double kBounceForwardsX1 = 0.25;
constexpr double kBounceForwardsY1 = 1.0;
constexpr double kBounceForwardsX2 = 0.99;
constexpr double kBounceForwardsY2 = 1.0;
// Control points for the bounce back Cubic Bezier curve.
constexpr double kBounceBackwardsX1 = 0.05;
constexpr double kBounceBackwardsY1 = 0.7;
constexpr double kBounceBackwardsX2 = 0.25;
constexpr double kBounceBackwardsY2 = 1.0;
base::TimeDelta CalculateBounceForwardsDuration(
double bounce_forwards_distance) {
return base::TimeDelta::FromMillisecondsD(
std::min(kOverbounceMaxDurationMilliseconds,
kOverbounceMillisecondsPerPixel * bounce_forwards_distance));
}
base::TimeDelta CalculateBounceBackDuration(double bounce_back_distance) {
return base::TimeDelta::FromMillisecondsD(std::min(
kBounceBackMaxDurationMilliseconds,
......@@ -35,6 +53,10 @@ base::TimeDelta CalculateBounceBackDuration(double bounce_back_distance) {
ElasticOverscrollControllerBezier::ElasticOverscrollControllerBezier(
cc::ScrollElasticityHelper* helper)
: ElasticOverscrollController(helper),
bounce_forwards_curve_(gfx::CubicBezier(kBounceForwardsX1,
kBounceForwardsY1,
kBounceForwardsX2,
kBounceForwardsY2)),
bounce_backwards_curve_(gfx::CubicBezier(kBounceBackwardsX1,
kBounceBackwardsY1,
kBounceBackwardsX2,
......@@ -49,32 +71,108 @@ gfx::Vector2dF ElasticOverscrollControllerBezier::OverscrollBoundary(
}
void ElasticOverscrollControllerBezier::DidEnterMomentumAnimatedState() {
// Express velocity in terms of milliseconds.
const gfx::Vector2dF velocity(scroll_velocity().x() / 1000.f,
scroll_velocity().y() / 1000.f);
gfx::Vector2dF bounce_forwards_delta(gfx::Vector2dF(
sqrt(std::abs(velocity.x())), sqrt(std::abs(velocity.y()))));
bounce_forwards_delta.Scale(kOverbounceDistanceMultiplier);
const gfx::Vector2dF max_stretch_amount = OverscrollBoundary(scroll_bounds());
bounce_forwards_distance_.set_x(
std::min(max_stretch_amount.x(),
std::abs(momentum_animation_initial_stretch_.x()) +
bounce_forwards_delta.x()));
bounce_forwards_distance_.set_y(
std::min(max_stretch_amount.y(),
std::abs(momentum_animation_initial_stretch_.y()) +
bounce_forwards_delta.y()));
// If we're flinging towards the edge, the sign of the distance will match
// that of the velocity. Otherwise, it will match that of the current
// stretch amount.
bounce_forwards_distance_.set_x(
(momentum_animation_initial_stretch_.x() == 0)
? std::copysign(bounce_forwards_distance_.x(), velocity.x())
: std::copysign(bounce_forwards_distance_.x(),
momentum_animation_initial_stretch_.x()));
bounce_forwards_distance_.set_y(
(momentum_animation_initial_stretch_.y() == 0)
? std::copysign(bounce_forwards_distance_.y(), velocity.y())
: std::copysign(bounce_forwards_distance_.y(),
momentum_animation_initial_stretch_.y()));
bounce_forwards_duration_y_ =
CalculateBounceForwardsDuration(bounce_forwards_delta.y());
bounce_backwards_duration_x_ =
CalculateBounceBackDuration(momentum_animation_initial_stretch_.x());
CalculateBounceBackDuration(bounce_forwards_distance_.x());
bounce_backwards_duration_y_ =
CalculateBounceBackDuration(momentum_animation_initial_stretch_.y());
CalculateBounceBackDuration(bounce_forwards_distance_.y());
}
gfx::Vector2d ElasticOverscrollControllerBezier::StretchAmountForTimeDelta(
const base::TimeDelta& delta) const {
// Handle the case where the animation is in the bounce-back stage.
double stretch_x, stretch_y;
stretch_x = stretch_y = 0.f;
if (delta < bounce_backwards_duration_x_) {
double curve_progress = delta.InMillisecondsF() /
bounce_backwards_duration_x_.InMillisecondsF();
double progress = bounce_backwards_curve_.Solve(curve_progress);
stretch_x = momentum_animation_initial_stretch_.x() * (1 - progress);
double ElasticOverscrollControllerBezier::StretchAmountForForwardBounce(
const base::TimeDelta& delta,
const base::TimeDelta& bounce_forwards_duration,
const double velocity,
const double initial_stretch,
const double bounce_forwards_distance) const {
const bool is_velocity_in_overscroll_direction =
(velocity < 0) == (initial_stretch < 0);
if (is_velocity_in_overscroll_direction) {
if (delta < bounce_forwards_duration) {
double curve_progress =
delta.InMillisecondsF() / bounce_forwards_duration.InMillisecondsF();
double progress = bounce_forwards_curve_.Solve(curve_progress);
return initial_stretch * (1 - progress) +
bounce_forwards_distance * progress;
}
}
return 0.f;
}
if (delta < bounce_backwards_duration_y_) {
double curve_progress = delta.InMillisecondsF() /
bounce_backwards_duration_y_.InMillisecondsF();
double ElasticOverscrollControllerBezier::StretchAmountForBackwardBounce(
const base::TimeDelta& delta,
const base::TimeDelta& bounce_backwards_duration,
const double bounce_forwards_distance) const {
if (delta < bounce_backwards_duration) {
double curve_progress =
delta.InMillisecondsF() / bounce_backwards_duration.InMillisecondsF();
double progress = bounce_backwards_curve_.Solve(curve_progress);
stretch_y = momentum_animation_initial_stretch_.y() * (1 - progress);
return bounce_forwards_distance * (1 - progress);
}
return 0.f;
}
return gfx::ToRoundedVector2d(gfx::Vector2dF(stretch_x, stretch_y));
gfx::Vector2d ElasticOverscrollControllerBezier::StretchAmountForTimeDelta(
const base::TimeDelta& delta) const {
// Check if a bounce forward animation needs to be created. This is needed
// when user "flings" a scroller. By the time the scroller reaches its bounds,
// if the velocity isn't 0, a bounce forwards animation will need to be
// played.
base::TimeDelta time_delta = delta;
const gfx::Vector2d forward_animation(gfx::ToRoundedVector2d(gfx::Vector2dF(
StretchAmountForForwardBounce(time_delta, bounce_forwards_duration_x_,
scroll_velocity().x(),
momentum_animation_initial_stretch_.x(),
bounce_forwards_distance_.x()),
StretchAmountForForwardBounce(time_delta, bounce_forwards_duration_y_,
scroll_velocity().y(),
momentum_animation_initial_stretch_.y(),
bounce_forwards_distance_.y()))));
if (!forward_animation.IsZero())
return forward_animation;
// Handle the case where the animation is in the bounce-back stage.
time_delta -= bounce_forwards_duration_x_;
time_delta -= bounce_forwards_duration_y_;
return gfx::ToRoundedVector2d(gfx::Vector2dF(
StretchAmountForBackwardBounce(time_delta, bounce_backwards_duration_x_,
bounce_forwards_distance_.x()),
StretchAmountForBackwardBounce(time_delta, bounce_backwards_duration_y_,
bounce_forwards_distance_.y())));
}
// The goal of this calculation is to map the distance the user has scrolled
......@@ -83,27 +181,26 @@ gfx::Vector2d
ElasticOverscrollControllerBezier::StretchAmountForAccumulatedOverscroll(
const gfx::Vector2dF& accumulated_overscroll) const {
// TODO(arakeri): This should change as you pinch zoom in.
const gfx::Size& scroller_bounds = GetScrollBounds();
const gfx::Vector2dF overscroll_boundary =
OverscrollBoundary(scroller_bounds);
OverscrollBoundary(scroll_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
// Also, it may happen that the scroll_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) {
if (scroll_bounds().width() > 0.f) {
overbounce_distance.set_x(
tanh(2 * accumulated_overscroll.x() / scroller_bounds.width()) *
tanh(2 * accumulated_overscroll.x() / scroll_bounds().width()) *
overscroll_boundary.x());
}
if (scroller_bounds.height() > 0.f) {
if (scroll_bounds().height() > 0.f) {
overbounce_distance.set_y(
tanh(2 * accumulated_overscroll.y() / scroller_bounds.height()) *
tanh(2 * accumulated_overscroll.y() / scroll_bounds().height()) *
overscroll_boundary.y());
}
......@@ -117,22 +214,21 @@ ElasticOverscrollControllerBezier::StretchAmountForAccumulatedOverscroll(
gfx::Vector2d
ElasticOverscrollControllerBezier::AccumulatedOverscrollForStretchAmount(
const gfx::Vector2dF& stretch_amount) const {
const gfx::Size& scroller_bounds = GetScrollBounds();
const gfx::Vector2dF overscroll_boundary =
OverscrollBoundary(scroller_bounds);
OverscrollBoundary(scroll_bounds());
// It may happen that the scroller_bounds are 0 if the viewport scroll
// It may happen that the scroll_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 overscrolled_amount;
if (overscroll_boundary.x() > 0.f) {
float atanh_value = atanh(stretch_amount.x() / overscroll_boundary.x());
overscrolled_amount.set_x((atanh_value / 2) * scroller_bounds.width());
overscrolled_amount.set_x((atanh_value / 2) * scroll_bounds().width());
}
if (overscroll_boundary.y() > 0.f) {
float atanh_value = atanh(stretch_amount.y() / overscroll_boundary.y());
overscrolled_amount.set_y((atanh_value / 2) * scroller_bounds.height());
overscrolled_amount.set_y((atanh_value / 2) * scroll_bounds().height());
}
return overscrolled_amount;
......
......@@ -33,11 +33,24 @@ class PLATFORM_EXPORT ElasticOverscrollControllerBezier
gfx::Vector2d AccumulatedOverscrollForStretchAmount(
const gfx::Vector2dF& delta) const override;
gfx::Vector2dF OverscrollBoundary(const gfx::Size& scroller_bounds) const;
double StretchAmountForForwardBounce(
const base::TimeDelta& delta,
const base::TimeDelta& bounce_forwards_duration,
const double velocity,
const double initial_stretch,
const double bounce_forwards_distance) const;
double StretchAmountForBackwardBounce(
const base::TimeDelta& delta,
const base::TimeDelta& bounce_backwards_duration,
const double bounce_forwards_distance) const;
private:
const gfx::CubicBezier bounce_backwards_curve_;
const gfx::CubicBezier bounce_forwards_curve_;
base::TimeDelta bounce_forwards_duration_x_;
base::TimeDelta bounce_forwards_duration_y_;
gfx::Vector2dF bounce_forwards_distance_;
// The following are used to track the duration of the bounce back animation.
const gfx::CubicBezier bounce_backwards_curve_;
base::TimeDelta bounce_backwards_duration_x_;
base::TimeDelta bounce_backwards_duration_y_;
DISALLOW_COPY_AND_ASSIGN(ElasticOverscrollControllerBezier);
......
......@@ -70,15 +70,11 @@ class ElasticOverscrollControllerBezierTest : public testing::Test {
}
void SendGestureScrollUpdate(PhaseState inertialPhase,
const Vector2dF& scroll_delta,
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;
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;
......@@ -103,22 +99,18 @@ TEST_F(ElasticOverscrollControllerBezierTest, VerifyOverscrollStretch) {
// Test vertical overscroll.
SendGestureScrollBegin(PhaseState::kNonMomentum);
EXPECT_EQ(Vector2dF(0, 0), helper_.StretchAmount());
SendGestureScrollUpdate(PhaseState::kNonMomentum, Vector2dF(0, -10),
Vector2dF(0, -100));
SendGestureScrollUpdate(PhaseState::kNonMomentum, Vector2dF(0, -100));
EXPECT_EQ(Vector2dF(0, -19), helper_.StretchAmount());
SendGestureScrollUpdate(PhaseState::kNonMomentum, Vector2dF(0, 10),
Vector2dF(0, 100));
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(-10, 0),
Vector2dF(-100, 0));
SendGestureScrollUpdate(PhaseState::kNonMomentum, Vector2dF(-100, 0));
EXPECT_EQ(Vector2dF(-19, 0), helper_.StretchAmount());
SendGestureScrollUpdate(PhaseState::kNonMomentum, Vector2dF(10, 0),
Vector2dF(100, 0));
SendGestureScrollUpdate(PhaseState::kNonMomentum, Vector2dF(100, 0));
EXPECT_EQ(Vector2dF(0, 0), helper_.StretchAmount());
SendGestureScrollEnd();
}
......@@ -129,8 +121,7 @@ TEST_F(ElasticOverscrollControllerBezierTest, ReconcileStretchAndScroll) {
SendGestureScrollBegin(PhaseState::kNonMomentum);
helper_.SetScrollOffsetAndMaxScrollOffset(gfx::ScrollOffset(0, 0),
gfx::ScrollOffset(100, 100));
SendGestureScrollUpdate(PhaseState::kNonMomentum, Vector2dF(0, -50),
Vector2dF(0, -100));
SendGestureScrollUpdate(PhaseState::kNonMomentum, Vector2dF(0, -100));
EXPECT_EQ(Vector2dF(0, -19), helper_.StretchAmount());
helper_.ScrollBy(Vector2dF(0, 1));
controller_.ReconcileStretchAndScroll();
......@@ -144,8 +135,7 @@ TEST_F(ElasticOverscrollControllerBezierTest, ReconcileStretchAndScroll) {
SendGestureScrollBegin(PhaseState::kNonMomentum);
helper_.SetScrollOffsetAndMaxScrollOffset(gfx::ScrollOffset(0, 0),
gfx::ScrollOffset(100, 100));
SendGestureScrollUpdate(PhaseState::kNonMomentum, Vector2dF(-50, 0),
Vector2dF(-100, 0));
SendGestureScrollUpdate(PhaseState::kNonMomentum, Vector2dF(-100, 0));
EXPECT_EQ(Vector2dF(-19, 0), helper_.StretchAmount());
helper_.ScrollBy(Vector2dF(1, 0));
controller_.ReconcileStretchAndScroll();
......@@ -168,13 +158,12 @@ TEST_F(ElasticOverscrollControllerBezierTest, VerifyOverscrollBounceDistance) {
// 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, VerifyAnimationTick) {
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, -50),
Vector2dF(0, -100));
SendGestureScrollUpdate(PhaseState::kNonMomentum, Vector2dF(0, -100));
// This signals that the finger has lifted off which triggers the bounce back
// animation.
......@@ -200,8 +189,7 @@ TEST_F(ElasticOverscrollControllerBezierTest, VerifyAnimationTick) {
// Test horizontal overscroll.
SendGestureScrollBegin(PhaseState::kNonMomentum);
EXPECT_EQ(Vector2dF(0, 0), helper_.StretchAmount());
SendGestureScrollUpdate(PhaseState::kNonMomentum, Vector2dF(-25, 0),
Vector2dF(-80, 0));
SendGestureScrollUpdate(PhaseState::kNonMomentum, Vector2dF(-80, 0));
SendGestureScrollEnd(now);
// Frame 2.
......@@ -220,14 +208,40 @@ TEST_F(ElasticOverscrollControllerBezierTest, VerifyAnimationTick) {
EXPECT_EQ(controller_.state_, ElasticOverscrollController::kStateInactive);
}
// Tests that the bounce forward animation ticks as expected.
TEST_F(ElasticOverscrollControllerBezierTest, VerifyForwardAnimationTick) {
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, -500.f);
// This signals that the finger has lifted off which triggers a fling.
const base::TimeTicks now = base::TimeTicks::Now();
SendGestureScrollEnd(now);
const int SAMPLES = 7;
const int frames[SAMPLES] = {1, 2, 3, 4, 5, 8, 19};
const int stretch_amount[SAMPLES] = {-33, -40, -43, -40, -26, -11, 0};
for (int i = 0; i < SAMPLES; i++) {
controller_.Animate(now +
base::TimeDelta::FromMilliseconds(frames[i] * 16));
EXPECT_EQ(controller_.state_,
(stretch_amount[i] == 0
? ElasticOverscrollController::kStateInactive
: ElasticOverscrollController::kStateMomentumAnimated));
ASSERT_FLOAT_EQ(helper_.StretchAmount().y(), stretch_amount[i]);
}
}
// 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(),
Vector2dF(0, -100));
SendGestureScrollUpdate(PhaseState::kNonMomentum, Vector2dF(0, -100));
// This signals that the finger has lifted off which triggers the bounce back
// animation.
......@@ -245,8 +259,7 @@ TEST_F(ElasticOverscrollControllerBezierTest, VerifyScrollDuringBounceBack) {
// While the animation is still ticking, initiate a scroll.
SendGestureScrollBegin(PhaseState::kNonMomentum);
SendGestureScrollUpdate(PhaseState::kNonMomentum, Vector2dF(),
Vector2dF(0, -50));
SendGestureScrollUpdate(PhaseState::kNonMomentum, Vector2dF(0, -50));
ASSERT_FLOAT_EQ(helper_.StretchAmount().y(), -13);
}
......@@ -258,8 +271,7 @@ TEST_F(ElasticOverscrollControllerBezierTest, VerifyAnimationNotCreated) {
// state_ is kStateActiveScroll. unused_delta is 0 so overscroll should not
// take place.
Vector2dF delta(-25, -50);
SendGestureScrollUpdate(PhaseState::kNonMomentum, delta, Vector2dF(0, 0));
SendGestureScrollUpdate(PhaseState::kNonMomentum, Vector2dF(0, 0));
// This signals that the finger has lifted off which triggers the bounce back
// animation.
......
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