Re-subimission of https://codereview.chromium.org/1041213003/
[chromium-blink-merge.git] / content / renderer / input / input_scroll_elasticity_controller.cc
blob00d078ff76c84ddb3b870c1ac0dd107bcaec0353
1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "content/renderer/input/input_scroll_elasticity_controller.h"
7 #include <math.h>
9 #include "base/bind.h"
10 #include "cc/input/input_handler.h"
11 #include "ui/gfx/geometry/vector2d_conversions.h"
13 // InputScrollElasticityController is based on
14 // WebKit/Source/platform/mac/InputScrollElasticityController.mm
16 * Copyright (C) 2011 Apple Inc. All rights reserved.
18 * Redistribution and use in source and binary forms, with or without
19 * modification, are permitted provided that the following conditions
20 * are met:
21 * 1. Redistributions of source code must retain the above copyright
22 * notice, this list of conditions and the following disclaimer.
23 * 2. Redistributions in binary form must reproduce the above copyright
24 * notice, this list of conditions and the following disclaimer in the
25 * documentation and/or other materials provided with the distribution.
27 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
28 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
29 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
30 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
31 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
32 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
33 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
34 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
35 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
36 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
37 * THE POSSIBILITY OF SUCH DAMAGE.
40 namespace content {
42 namespace {
44 const float kScrollVelocityZeroingTimeout = 0.10f;
45 const float kRubberbandMinimumRequiredDeltaBeforeStretch = 10;
47 const float kRubberbandStiffness = 20;
48 const float kRubberbandAmplitude = 0.31f;
49 const float kRubberbandPeriod = 1.6f;
51 // For these functions which compute the stretch amount, always return a
52 // rounded value, instead of a floating-point value. The reason for this is
53 // that Blink's scrolling can become erratic with fractional scroll amounts (in
54 // particular, if you have a scroll offset of 0.5, Blink will never actually
55 // bring that value back to 0, which breaks the logic used to determine if a
56 // layer is pinned in a direction).
58 gfx::Vector2d StretchAmountForTimeDelta(const gfx::Vector2dF& initial_position,
59 const gfx::Vector2dF& initial_velocity,
60 float elapsed_time) {
61 // Compute the stretch amount at a given time after some initial conditions.
62 // Do this by first computing an intermediary position given the initial
63 // position, initial velocity, time elapsed, and no external forces. Then
64 // take the intermediary position and damp it towards zero by multiplying
65 // against a negative exponential.
66 float amplitude = kRubberbandAmplitude;
67 float period = kRubberbandPeriod;
68 float critical_dampening_factor =
69 expf((-elapsed_time * kRubberbandStiffness) / period);
71 return gfx::ToRoundedVector2d(gfx::ScaleVector2d(
72 initial_position +
73 gfx::ScaleVector2d(initial_velocity, elapsed_time * amplitude),
74 critical_dampening_factor));
77 gfx::Vector2d StretchAmountForReboundDelta(const gfx::Vector2dF& delta) {
78 float stiffness = std::max(kRubberbandStiffness, 1.0f);
79 return gfx::ToRoundedVector2d(gfx::ScaleVector2d(delta, 1.0f / stiffness));
82 gfx::Vector2d StretchScrollForceForStretchAmount(const gfx::Vector2dF& delta) {
83 return gfx::ToRoundedVector2d(
84 gfx::ScaleVector2d(delta, kRubberbandStiffness));
87 } // namespace
89 InputScrollElasticityController::InputScrollElasticityController(
90 cc::ScrollElasticityHelper* helper)
91 : helper_(helper),
92 state_(kStateInactive),
93 momentum_animation_reset_at_next_frame_(false),
94 weak_factory_(this) {
97 InputScrollElasticityController::~InputScrollElasticityController() {
100 base::WeakPtr<InputScrollElasticityController>
101 InputScrollElasticityController::GetWeakPtr() {
102 if (helper_)
103 return weak_factory_.GetWeakPtr();
104 return base::WeakPtr<InputScrollElasticityController>();
107 void InputScrollElasticityController::ObserveWheelEventAndResult(
108 const blink::WebMouseWheelEvent& wheel_event,
109 const cc::InputHandlerScrollResult& scroll_result) {
110 // We should only get PhaseMayBegin or PhaseBegan events while in the
111 // Inactive or MomentumAnimated states, but in case we get bad input (e.g,
112 // abbreviated by tab-switch), always re-set the state to ActiveScrolling
113 // when those events are received.
114 if (wheel_event.phase == blink::WebMouseWheelEvent::PhaseMayBegin ||
115 wheel_event.phase == blink::WebMouseWheelEvent::PhaseBegan) {
116 scroll_velocity = gfx::Vector2dF();
117 last_scroll_event_timestamp_ = base::TimeTicks();
118 state_ = kStateActiveScroll;
119 pending_overscroll_delta_ = gfx::Vector2dF();
120 return;
123 gfx::Vector2dF event_delta(-wheel_event.deltaX, -wheel_event.deltaY);
124 base::TimeTicks event_timestamp =
125 base::TimeTicks() +
126 base::TimeDelta::FromSecondsD(wheel_event.timeStampSeconds);
127 switch (state_) {
128 case kStateInactive: {
129 // The PhaseMayBegin and PhaseBegan cases are handled at the top of the
130 // function.
131 if (wheel_event.momentumPhase == blink::WebMouseWheelEvent::PhaseBegan)
132 state_ = kStateMomentumScroll;
133 break;
135 case kStateActiveScroll:
136 if (wheel_event.phase == blink::WebMouseWheelEvent::PhaseChanged) {
137 UpdateVelocity(event_delta, event_timestamp);
138 Overscroll(event_delta, scroll_result.unused_scroll_delta);
139 } else if (wheel_event.phase == blink::WebMouseWheelEvent::PhaseEnded ||
140 wheel_event.phase ==
141 blink::WebMouseWheelEvent::PhaseCancelled) {
142 if (helper_->StretchAmount().IsZero()) {
143 EnterStateInactive();
144 } else {
145 EnterStateMomentumAnimated(event_timestamp);
148 break;
149 case kStateMomentumScroll:
150 if (wheel_event.momentumPhase ==
151 blink::WebMouseWheelEvent::PhaseChanged) {
152 UpdateVelocity(event_delta, event_timestamp);
153 Overscroll(event_delta, scroll_result.unused_scroll_delta);
154 if (!helper_->StretchAmount().IsZero()) {
155 EnterStateMomentumAnimated(event_timestamp);
157 } else if (wheel_event.momentumPhase ==
158 blink::WebMouseWheelEvent::PhaseEnded) {
159 EnterStateInactive();
161 case kStateMomentumAnimated:
162 // The PhaseMayBegin and PhaseBegan cases are handled at the top of the
163 // function.
164 break;
168 void InputScrollElasticityController::UpdateVelocity(
169 const gfx::Vector2dF& event_delta,
170 const base::TimeTicks& event_timestamp) {
171 float time_delta =
172 (event_timestamp - last_scroll_event_timestamp_).InSecondsF();
173 if (time_delta < kScrollVelocityZeroingTimeout && time_delta > 0) {
174 scroll_velocity = gfx::Vector2dF(event_delta.x() / time_delta,
175 event_delta.y() / time_delta);
176 } else {
177 scroll_velocity = gfx::Vector2dF();
179 last_scroll_event_timestamp_ = event_timestamp;
182 void InputScrollElasticityController::Overscroll(
183 const gfx::Vector2dF& input_delta,
184 const gfx::Vector2dF& overscroll_delta) {
185 // The effect can be dynamically disabled by setting disallowing user
186 // scrolling. When disabled, disallow active or momentum overscrolling, but
187 // allow any current overscroll to animate back.
188 if (!helper_->IsUserScrollable())
189 return;
191 gfx::Vector2dF adjusted_overscroll_delta =
192 pending_overscroll_delta_ + overscroll_delta;
193 pending_overscroll_delta_ = gfx::Vector2dF();
195 // Only allow one direction to overscroll at a time, and slightly prefer
196 // scrolling vertically by applying the equal case to delta_y.
197 if (fabsf(input_delta.y()) >= fabsf(input_delta.x()))
198 adjusted_overscroll_delta.set_x(0);
199 else
200 adjusted_overscroll_delta.set_y(0);
202 // Don't allow overscrolling in a direction where scrolling is possible.
203 if (!PinnedHorizontally(adjusted_overscroll_delta.x()))
204 adjusted_overscroll_delta.set_x(0);
205 if (!PinnedVertically(adjusted_overscroll_delta.y())) {
206 adjusted_overscroll_delta.set_y(0);
209 // Require a minimum of 10 units of overscroll before starting the rubber-band
210 // stretch effect, so that small stray motions don't trigger it. If that
211 // minimum isn't met, save what remains in |pending_overscroll_delta_| for
212 // the next event.
213 gfx::Vector2dF old_stretch_amount = helper_->StretchAmount();
214 gfx::Vector2dF stretch_scroll_force_delta;
215 if (old_stretch_amount.x() != 0 ||
216 fabsf(adjusted_overscroll_delta.x()) >=
217 kRubberbandMinimumRequiredDeltaBeforeStretch) {
218 stretch_scroll_force_delta.set_x(adjusted_overscroll_delta.x());
219 } else {
220 pending_overscroll_delta_.set_x(adjusted_overscroll_delta.x());
222 if (old_stretch_amount.y() != 0 ||
223 fabsf(adjusted_overscroll_delta.y()) >=
224 kRubberbandMinimumRequiredDeltaBeforeStretch) {
225 stretch_scroll_force_delta.set_y(adjusted_overscroll_delta.y());
226 } else {
227 pending_overscroll_delta_.set_y(adjusted_overscroll_delta.y());
230 // Update the stretch amount according to the spring equations.
231 if (stretch_scroll_force_delta.IsZero())
232 return;
233 stretch_scroll_force_ += stretch_scroll_force_delta;
234 gfx::Vector2dF new_stretch_amount =
235 StretchAmountForReboundDelta(stretch_scroll_force_);
236 helper_->SetStretchAmount(new_stretch_amount);
239 void InputScrollElasticityController::EnterStateInactive() {
240 DCHECK_NE(kStateInactive, state_);
241 DCHECK(helper_->StretchAmount().IsZero());
242 state_ = kStateInactive;
243 stretch_scroll_force_ = gfx::Vector2dF();
246 void InputScrollElasticityController::EnterStateMomentumAnimated(
247 const base::TimeTicks& triggering_event_timestamp) {
248 DCHECK_NE(kStateMomentumAnimated, state_);
249 state_ = kStateMomentumAnimated;
251 momentum_animation_start_time_ = triggering_event_timestamp;
252 momentum_animation_initial_stretch_ = helper_->StretchAmount();
253 momentum_animation_initial_velocity_ = scroll_velocity;
254 momentum_animation_reset_at_next_frame_ = false;
256 // Similarly to the logic in Overscroll, prefer vertical scrolling to
257 // horizontal scrolling.
258 if (fabsf(momentum_animation_initial_velocity_.y()) >=
259 fabsf(momentum_animation_initial_velocity_.x()))
260 momentum_animation_initial_velocity_.set_x(0);
262 if (!CanScrollHorizontally())
263 momentum_animation_initial_velocity_.set_x(0);
265 if (!CanScrollVertically())
266 momentum_animation_initial_velocity_.set_y(0);
268 helper_->RequestAnimate();
271 void InputScrollElasticityController::Animate(base::TimeTicks time) {
272 if (state_ != kStateMomentumAnimated)
273 return;
275 if (momentum_animation_reset_at_next_frame_) {
276 momentum_animation_start_time_ = time;
277 momentum_animation_initial_stretch_ = helper_->StretchAmount();
278 momentum_animation_initial_velocity_ = gfx::Vector2dF();
279 momentum_animation_reset_at_next_frame_ = false;
282 float time_delta =
283 std::max((time - momentum_animation_start_time_).InSecondsF(), 0.0);
285 gfx::Vector2dF old_stretch_amount = helper_->StretchAmount();
286 gfx::Vector2dF new_stretch_amount = StretchAmountForTimeDelta(
287 momentum_animation_initial_stretch_, momentum_animation_initial_velocity_,
288 time_delta);
289 gfx::Vector2dF stretch_delta = new_stretch_amount - old_stretch_amount;
291 // If the new stretch amount is near zero, set it directly to zero and enter
292 // the inactive state.
293 if (fabs(new_stretch_amount.x()) < 1 && fabs(new_stretch_amount.y()) < 1) {
294 helper_->SetStretchAmount(gfx::Vector2dF());
295 EnterStateInactive();
296 return;
299 // If we are not pinned in the direction of the delta, then the delta is only
300 // allowed to decrease the existing stretch -- it cannot increase a stretch
301 // until it is pinned.
302 if (!PinnedHorizontally(stretch_delta.x())) {
303 if (stretch_delta.x() > 0 && old_stretch_amount.x() < 0)
304 stretch_delta.set_x(std::min(stretch_delta.x(), -old_stretch_amount.x()));
305 else if (stretch_delta.x() < 0 && old_stretch_amount.x() > 0)
306 stretch_delta.set_x(std::max(stretch_delta.x(), -old_stretch_amount.x()));
307 else
308 stretch_delta.set_x(0);
310 if (!PinnedVertically(stretch_delta.y())) {
311 if (stretch_delta.y() > 0 && old_stretch_amount.y() < 0)
312 stretch_delta.set_y(std::min(stretch_delta.y(), -old_stretch_amount.y()));
313 else if (stretch_delta.y() < 0 && old_stretch_amount.y() > 0)
314 stretch_delta.set_y(std::max(stretch_delta.y(), -old_stretch_amount.y()));
315 else
316 stretch_delta.set_y(0);
318 new_stretch_amount = old_stretch_amount + stretch_delta;
320 stretch_scroll_force_ =
321 StretchScrollForceForStretchAmount(new_stretch_amount);
322 helper_->SetStretchAmount(new_stretch_amount);
323 helper_->RequestAnimate();
326 bool InputScrollElasticityController::PinnedHorizontally(
327 float direction) const {
328 gfx::ScrollOffset scroll_offset = helper_->ScrollOffset();
329 gfx::ScrollOffset max_scroll_offset = helper_->MaxScrollOffset();
330 if (direction < 0)
331 return scroll_offset.x() <= 0;
332 if (direction > 0)
333 return scroll_offset.x() >= max_scroll_offset.x();
334 return false;
337 bool InputScrollElasticityController::PinnedVertically(float direction) const {
338 gfx::ScrollOffset scroll_offset = helper_->ScrollOffset();
339 gfx::ScrollOffset max_scroll_offset = helper_->MaxScrollOffset();
340 if (direction < 0)
341 return scroll_offset.y() <= 0;
342 if (direction > 0)
343 return scroll_offset.y() >= max_scroll_offset.y();
344 return false;
347 bool InputScrollElasticityController::CanScrollHorizontally() const {
348 return helper_->MaxScrollOffset().x() > 0;
351 bool InputScrollElasticityController::CanScrollVertically() const {
352 return helper_->MaxScrollOffset().y() > 0;
355 void InputScrollElasticityController::ReconcileStretchAndScroll() {
356 gfx::Vector2dF stretch = helper_->StretchAmount();
357 if (stretch.IsZero())
358 return;
360 gfx::ScrollOffset scroll_offset = helper_->ScrollOffset();
361 gfx::ScrollOffset max_scroll_offset = helper_->MaxScrollOffset();
363 // Compute stretch_adjustment which will be added to |stretch| and subtracted
364 // from the |scroll_offset|.
365 gfx::Vector2dF stretch_adjustment;
366 if (stretch.x() < 0 && scroll_offset.x() > 0) {
367 stretch_adjustment.set_x(
368 std::min(-stretch.x(), static_cast<float>(scroll_offset.x())));
370 if (stretch.x() > 0 && scroll_offset.x() < max_scroll_offset.x()) {
371 stretch_adjustment.set_x(std::max(
372 -stretch.x(),
373 static_cast<float>(scroll_offset.x() - max_scroll_offset.x())));
375 if (stretch.y() < 0 && scroll_offset.y() > 0) {
376 stretch_adjustment.set_y(
377 std::min(-stretch.y(), static_cast<float>(scroll_offset.y())));
379 if (stretch.y() > 0 && scroll_offset.y() < max_scroll_offset.y()) {
380 stretch_adjustment.set_y(std::max(
381 -stretch.y(),
382 static_cast<float>(scroll_offset.y() - max_scroll_offset.y())));
385 if (stretch_adjustment.IsZero())
386 return;
388 gfx::Vector2dF new_stretch_amount = stretch + stretch_adjustment;
389 helper_->ScrollBy(-stretch_adjustment);
390 helper_->SetStretchAmount(new_stretch_amount);
392 // Update the internal state for the active scroll or animation to avoid
393 // discontinuities.
394 switch (state_) {
395 case kStateActiveScroll:
396 stretch_scroll_force_ =
397 StretchScrollForceForStretchAmount(new_stretch_amount);
398 break;
399 case kStateMomentumAnimated:
400 momentum_animation_reset_at_next_frame_ = true;
401 break;
402 default:
403 // These cases should not be hit because the stretch must be zero in the
404 // Inactive and MomentumScroll states.
405 NOTREACHED();
406 break;
410 } // namespace content