OLD | NEW |
| (Empty) |
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. | |
4 | |
5 #include "content/renderer/input/input_scroll_elasticity_controller.h" | |
6 | |
7 #include <math.h> | |
8 | |
9 #include "base/bind.h" | |
10 #include "cc/input/input_handler.h" | |
11 #include "ui/gfx/geometry/vector2d_conversions.h" | |
12 | |
13 // InputScrollElasticityController is based on | |
14 // WebKit/Source/platform/mac/InputScrollElasticityController.mm | |
15 /* | |
16 * Copyright (C) 2011 Apple Inc. All rights reserved. | |
17 * | |
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. | |
26 * | |
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. | |
38 */ | |
39 | |
40 namespace content { | |
41 | |
42 namespace { | |
43 | |
44 const float kScrollVelocityZeroingTimeout = 0.10f; | |
45 const float kRubberbandMinimumRequiredDeltaBeforeStretch = 10; | |
46 | |
47 const float kRubberbandStiffness = 20; | |
48 const float kRubberbandAmplitude = 0.31f; | |
49 const float kRubberbandPeriod = 1.6f; | |
50 | |
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). | |
57 | |
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); | |
70 | |
71 return gfx::ToRoundedVector2d(gfx::ScaleVector2d( | |
72 initial_position + | |
73 gfx::ScaleVector2d(initial_velocity, elapsed_time * amplitude), | |
74 critical_dampening_factor)); | |
75 } | |
76 | |
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)); | |
80 } | |
81 | |
82 gfx::Vector2d StretchScrollForceForStretchAmount(const gfx::Vector2dF& delta) { | |
83 return gfx::ToRoundedVector2d( | |
84 gfx::ScaleVector2d(delta, kRubberbandStiffness)); | |
85 } | |
86 | |
87 } // namespace | |
88 | |
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) { | |
95 } | |
96 | |
97 InputScrollElasticityController::~InputScrollElasticityController() { | |
98 } | |
99 | |
100 base::WeakPtr<InputScrollElasticityController> | |
101 InputScrollElasticityController::GetWeakPtr() { | |
102 if (helper_) | |
103 return weak_factory_.GetWeakPtr(); | |
104 return base::WeakPtr<InputScrollElasticityController>(); | |
105 } | |
106 | |
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; | |
121 } | |
122 | |
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; | |
134 } | |
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); | |
146 } | |
147 } | |
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); | |
156 } | |
157 } else if (wheel_event.momentumPhase == | |
158 blink::WebMouseWheelEvent::PhaseEnded) { | |
159 EnterStateInactive(); | |
160 } | |
161 case kStateMomentumAnimated: | |
162 // The PhaseMayBegin and PhaseBegan cases are handled at the top of the | |
163 // function. | |
164 break; | |
165 } | |
166 } | |
167 | |
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(); | |
178 } | |
179 last_scroll_event_timestamp_ = event_timestamp; | |
180 } | |
181 | |
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; | |
190 | |
191 gfx::Vector2dF adjusted_overscroll_delta = | |
192 pending_overscroll_delta_ + overscroll_delta; | |
193 pending_overscroll_delta_ = gfx::Vector2dF(); | |
194 | |
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); | |
201 | |
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); | |
207 } | |
208 | |
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()); | |
221 } | |
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()); | |
228 } | |
229 | |
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); | |
237 } | |
238 | |
239 void InputScrollElasticityController::EnterStateInactive() { | |
240 DCHECK_NE(kStateInactive, state_); | |
241 DCHECK(helper_->StretchAmount().IsZero()); | |
242 state_ = kStateInactive; | |
243 stretch_scroll_force_ = gfx::Vector2dF(); | |
244 } | |
245 | |
246 void InputScrollElasticityController::EnterStateMomentumAnimated( | |
247 const base::TimeTicks& triggering_event_timestamp) { | |
248 DCHECK_NE(kStateMomentumAnimated, state_); | |
249 state_ = kStateMomentumAnimated; | |
250 | |
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; | |
255 | |
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); | |
261 | |
262 if (!CanScrollHorizontally()) | |
263 momentum_animation_initial_velocity_.set_x(0); | |
264 | |
265 if (!CanScrollVertically()) | |
266 momentum_animation_initial_velocity_.set_y(0); | |
267 | |
268 helper_->RequestAnimate(); | |
269 } | |
270 | |
271 void InputScrollElasticityController::Animate(base::TimeTicks time) { | |
272 if (state_ != kStateMomentumAnimated) | |
273 return; | |
274 | |
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; | |
280 } | |
281 | |
282 float time_delta = | |
283 std::max((time - momentum_animation_start_time_).InSecondsF(), 0.0); | |
284 | |
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; | |
290 | |
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; | |
297 } | |
298 | |
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); | |
309 } | |
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); | |
317 } | |
318 new_stretch_amount = old_stretch_amount + stretch_delta; | |
319 | |
320 stretch_scroll_force_ = | |
321 StretchScrollForceForStretchAmount(new_stretch_amount); | |
322 helper_->SetStretchAmount(new_stretch_amount); | |
323 helper_->RequestAnimate(); | |
324 } | |
325 | |
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; | |
335 } | |
336 | |
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; | |
345 } | |
346 | |
347 bool InputScrollElasticityController::CanScrollHorizontally() const { | |
348 return helper_->MaxScrollOffset().x() > 0; | |
349 } | |
350 | |
351 bool InputScrollElasticityController::CanScrollVertically() const { | |
352 return helper_->MaxScrollOffset().y() > 0; | |
353 } | |
354 | |
355 void InputScrollElasticityController::ReconcileStretchAndScroll() { | |
356 gfx::Vector2dF stretch = helper_->StretchAmount(); | |
357 if (stretch.IsZero()) | |
358 return; | |
359 | |
360 gfx::ScrollOffset scroll_offset = helper_->ScrollOffset(); | |
361 gfx::ScrollOffset max_scroll_offset = helper_->MaxScrollOffset(); | |
362 | |
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()))); | |
369 } | |
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()))); | |
374 } | |
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()))); | |
378 } | |
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()))); | |
383 } | |
384 | |
385 if (stretch_adjustment.IsZero()) | |
386 return; | |
387 | |
388 gfx::Vector2dF new_stretch_amount = stretch + stretch_adjustment; | |
389 helper_->ScrollBy(-stretch_adjustment); | |
390 helper_->SetStretchAmount(new_stretch_amount); | |
391 | |
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; | |
407 } | |
408 } | |
409 | |
410 } // namespace content | |
OLD | NEW |