| 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 |