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"
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
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.
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
,
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(
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
));
89 InputScrollElasticityController::InputScrollElasticityController(
90 cc::ScrollElasticityHelper
* helper
)
92 state_(kStateInactive
),
93 momentum_animation_reset_at_next_frame_(false),
97 InputScrollElasticityController::~InputScrollElasticityController() {
100 base::WeakPtr
<InputScrollElasticityController
>
101 InputScrollElasticityController::GetWeakPtr() {
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();
123 gfx::Vector2dF
event_delta(-wheel_event
.deltaX
, -wheel_event
.deltaY
);
124 base::TimeTicks event_timestamp
=
126 base::TimeDelta::FromSecondsD(wheel_event
.timeStampSeconds
);
128 case kStateInactive
: {
129 // The PhaseMayBegin and PhaseBegan cases are handled at the top of the
131 if (wheel_event
.momentumPhase
== blink::WebMouseWheelEvent::PhaseBegan
)
132 state_
= kStateMomentumScroll
;
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
||
141 blink::WebMouseWheelEvent::PhaseCancelled
) {
142 if (helper_
->StretchAmount().IsZero()) {
143 EnterStateInactive();
145 EnterStateMomentumAnimated(event_timestamp
);
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
168 void InputScrollElasticityController::UpdateVelocity(
169 const gfx::Vector2dF
& event_delta
,
170 const base::TimeTicks
& event_timestamp
) {
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
);
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())
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);
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
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());
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());
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())
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
)
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;
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_
,
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();
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()));
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()));
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();
331 return scroll_offset
.x() <= 0;
333 return scroll_offset
.x() >= max_scroll_offset
.x();
337 bool InputScrollElasticityController::PinnedVertically(float direction
) const {
338 gfx::ScrollOffset scroll_offset
= helper_
->ScrollOffset();
339 gfx::ScrollOffset max_scroll_offset
= helper_
->MaxScrollOffset();
341 return scroll_offset
.y() <= 0;
343 return scroll_offset
.y() >= max_scroll_offset
.y();
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())
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(
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(
382 static_cast<float>(scroll_offset
.y() - max_scroll_offset
.y())));
385 if (stretch_adjustment
.IsZero())
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
395 case kStateActiveScroll
:
396 stretch_scroll_force_
=
397 StretchScrollForceForStretchAmount(new_stretch_amount
);
399 case kStateMomentumAnimated
:
400 momentum_animation_reset_at_next_frame_
= true;
403 // These cases should not be hit because the stretch must be zero in the
404 // Inactive and MomentumScroll states.
410 } // namespace content