// Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "ui/events/blink/input_scroll_elasticity_controller.h" #include #include #include "base/bind.h" #include "cc/input/input_handler.h" #include "ui/gfx/geometry/vector2d_conversions.h" // InputScrollElasticityController is based on // WebKit/Source/platform/mac/InputScrollElasticityController.mm /* * Copyright (C) 2011 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. */ namespace ui { namespace { const float kScrollVelocityZeroingTimeout = 0.10f; const float kRubberbandMinimumRequiredDeltaBeforeStretch = 10; const float kRubberbandStiffness = 20; const float kRubberbandAmplitude = 0.31f; const float kRubberbandPeriod = 1.6f; // For these functions which compute the stretch amount, always return a // rounded value, instead of a floating-point value. The reason for this is // that Blink's scrolling can become erratic with fractional scroll amounts (in // particular, if you have a scroll offset of 0.5, Blink will never actually // bring that value back to 0, which breaks the logic used to determine if a // layer is pinned in a direction). gfx::Vector2d StretchAmountForTimeDelta(const gfx::Vector2dF& initial_position, const gfx::Vector2dF& initial_velocity, float elapsed_time) { // Compute the stretch amount at a given time after some initial conditions. // Do this by first computing an intermediary position given the initial // position, initial velocity, time elapsed, and no external forces. Then // take the intermediary position and damp it towards zero by multiplying // against a negative exponential. float amplitude = kRubberbandAmplitude; float period = kRubberbandPeriod; float critical_dampening_factor = expf((-elapsed_time * kRubberbandStiffness) / period); return gfx::ToRoundedVector2d(gfx::ScaleVector2d( initial_position + gfx::ScaleVector2d(initial_velocity, elapsed_time * amplitude), critical_dampening_factor)); } gfx::Vector2d StretchAmountForReboundDelta(const gfx::Vector2dF& delta) { float stiffness = std::max(kRubberbandStiffness, 1.0f); return gfx::ToRoundedVector2d(gfx::ScaleVector2d(delta, 1.0f / stiffness)); } gfx::Vector2d StretchScrollForceForStretchAmount(const gfx::Vector2dF& delta) { return gfx::ToRoundedVector2d( gfx::ScaleVector2d(delta, kRubberbandStiffness)); } } // namespace InputScrollElasticityController::InputScrollElasticityController( cc::ScrollElasticityHelper* helper) : helper_(helper), state_(kStateInactive), momentum_animation_reset_at_next_frame_(false), weak_factory_(this) { } InputScrollElasticityController::~InputScrollElasticityController() { } base::WeakPtr InputScrollElasticityController::GetWeakPtr() { if (helper_) return weak_factory_.GetWeakPtr(); return base::WeakPtr(); } void InputScrollElasticityController::ObserveWheelEventAndResult( const blink::WebMouseWheelEvent& wheel_event, const cc::InputHandlerScrollResult& scroll_result) { // We should only get PhaseMayBegin or PhaseBegan events while in the // Inactive or MomentumAnimated states, but in case we get bad input (e.g, // abbreviated by tab-switch), always re-set the state to ActiveScrolling // when those events are received. if (wheel_event.phase == blink::WebMouseWheelEvent::PhaseMayBegin || wheel_event.phase == blink::WebMouseWheelEvent::PhaseBegan) { scroll_velocity = gfx::Vector2dF(); last_scroll_event_timestamp_ = base::TimeTicks(); state_ = kStateActiveScroll; pending_overscroll_delta_ = gfx::Vector2dF(); return; } gfx::Vector2dF event_delta(-wheel_event.deltaX, -wheel_event.deltaY); base::TimeTicks event_timestamp = base::TimeTicks() + base::TimeDelta::FromSecondsD(wheel_event.timeStampSeconds); switch (state_) { case kStateInactive: { // The PhaseMayBegin and PhaseBegan cases are handled at the top of the // function. if (wheel_event.momentumPhase == blink::WebMouseWheelEvent::PhaseBegan) state_ = kStateMomentumScroll; break; } case kStateActiveScroll: if (wheel_event.phase == blink::WebMouseWheelEvent::PhaseChanged) { UpdateVelocity(event_delta, event_timestamp); Overscroll(event_delta, scroll_result.unused_scroll_delta); } else if (wheel_event.phase == blink::WebMouseWheelEvent::PhaseEnded || wheel_event.phase == blink::WebMouseWheelEvent::PhaseCancelled) { if (helper_->StretchAmount().IsZero()) { EnterStateInactive(); } else { EnterStateMomentumAnimated(event_timestamp); } } break; case kStateMomentumScroll: if (wheel_event.momentumPhase == blink::WebMouseWheelEvent::PhaseChanged) { UpdateVelocity(event_delta, event_timestamp); Overscroll(event_delta, scroll_result.unused_scroll_delta); if (!helper_->StretchAmount().IsZero()) { EnterStateMomentumAnimated(event_timestamp); } } else if (wheel_event.momentumPhase == blink::WebMouseWheelEvent::PhaseEnded) { EnterStateInactive(); } case kStateMomentumAnimated: // The PhaseMayBegin and PhaseBegan cases are handled at the top of the // function. break; } } void InputScrollElasticityController::ObserveGestureEventAndResult( const blink::WebGestureEvent& gesture_event, const cc::InputHandlerScrollResult& scroll_result) { base::TimeTicks event_timestamp = base::TimeTicks() + base::TimeDelta::FromSecondsD(gesture_event.timeStampSeconds); switch (gesture_event.type) { case blink::WebInputEvent::GestureScrollBegin: { if (gesture_event.data.scrollBegin.synthetic) return; if (gesture_event.data.scrollBegin.inertial) { if (state_ == kStateInactive) state_ = kStateMomentumScroll; } else if (gesture_event.data.scrollBegin.deltaHintUnits == blink::WebGestureEvent::PrecisePixels) { scroll_velocity = gfx::Vector2dF(); last_scroll_event_timestamp_ = base::TimeTicks(); state_ = kStateActiveScroll; pending_overscroll_delta_ = gfx::Vector2dF(); } break; } case blink::WebInputEvent::GestureScrollUpdate: { gfx::Vector2dF event_delta(-gesture_event.data.scrollUpdate.deltaX, -gesture_event.data.scrollUpdate.deltaY); switch (state_) { case kStateMomentumAnimated: case kStateInactive: break; case kStateActiveScroll: case kStateMomentumScroll: UpdateVelocity(event_delta, event_timestamp); Overscroll(event_delta, scroll_result.unused_scroll_delta); if (gesture_event.data.scrollUpdate.inertial && !helper_->StretchAmount().IsZero()) { EnterStateMomentumAnimated(event_timestamp); } break; } break; } case blink::WebInputEvent::GestureScrollEnd: { if (gesture_event.data.scrollEnd.synthetic) return; switch (state_) { case kStateMomentumAnimated: case kStateInactive: break; case kStateActiveScroll: case kStateMomentumScroll: if (helper_->StretchAmount().IsZero()) { EnterStateInactive(); } else { EnterStateMomentumAnimated(event_timestamp); } break; } break; } default: break; } } void InputScrollElasticityController::UpdateVelocity( const gfx::Vector2dF& event_delta, const base::TimeTicks& event_timestamp) { float time_delta = (event_timestamp - last_scroll_event_timestamp_).InSecondsF(); if (time_delta < kScrollVelocityZeroingTimeout && time_delta > 0) { scroll_velocity = gfx::Vector2dF(event_delta.x() / time_delta, event_delta.y() / time_delta); } else { scroll_velocity = gfx::Vector2dF(); } last_scroll_event_timestamp_ = event_timestamp; } void InputScrollElasticityController::Overscroll( const gfx::Vector2dF& input_delta, const gfx::Vector2dF& overscroll_delta) { // The effect can be dynamically disabled by setting disallowing user // scrolling. When disabled, disallow active or momentum overscrolling, but // allow any current overscroll to animate back. if (!helper_->IsUserScrollable()) return; gfx::Vector2dF adjusted_overscroll_delta = pending_overscroll_delta_ + overscroll_delta; pending_overscroll_delta_ = gfx::Vector2dF(); // Only allow one direction to overscroll at a time, and slightly prefer // scrolling vertically by applying the equal case to delta_y. if (fabsf(input_delta.y()) >= fabsf(input_delta.x())) adjusted_overscroll_delta.set_x(0); else adjusted_overscroll_delta.set_y(0); // Don't allow overscrolling in a direction where scrolling is possible. if (!PinnedHorizontally(adjusted_overscroll_delta.x())) adjusted_overscroll_delta.set_x(0); if (!PinnedVertically(adjusted_overscroll_delta.y())) { adjusted_overscroll_delta.set_y(0); } // Require a minimum of 10 units of overscroll before starting the rubber-band // stretch effect, so that small stray motions don't trigger it. If that // minimum isn't met, save what remains in |pending_overscroll_delta_| for // the next event. gfx::Vector2dF old_stretch_amount = helper_->StretchAmount(); gfx::Vector2dF stretch_scroll_force_delta; if (old_stretch_amount.x() != 0 || fabsf(adjusted_overscroll_delta.x()) >= kRubberbandMinimumRequiredDeltaBeforeStretch) { stretch_scroll_force_delta.set_x(adjusted_overscroll_delta.x()); } else { pending_overscroll_delta_.set_x(adjusted_overscroll_delta.x()); } if (old_stretch_amount.y() != 0 || fabsf(adjusted_overscroll_delta.y()) >= kRubberbandMinimumRequiredDeltaBeforeStretch) { stretch_scroll_force_delta.set_y(adjusted_overscroll_delta.y()); } else { pending_overscroll_delta_.set_y(adjusted_overscroll_delta.y()); } // Update the stretch amount according to the spring equations. if (stretch_scroll_force_delta.IsZero()) return; stretch_scroll_force_ += stretch_scroll_force_delta; gfx::Vector2dF new_stretch_amount = StretchAmountForReboundDelta(stretch_scroll_force_); helper_->SetStretchAmount(new_stretch_amount); } void InputScrollElasticityController::EnterStateInactive() { DCHECK_NE(kStateInactive, state_); DCHECK(helper_->StretchAmount().IsZero()); state_ = kStateInactive; stretch_scroll_force_ = gfx::Vector2dF(); } void InputScrollElasticityController::EnterStateMomentumAnimated( const base::TimeTicks& triggering_event_timestamp) { DCHECK_NE(kStateMomentumAnimated, state_); state_ = kStateMomentumAnimated; momentum_animation_start_time_ = triggering_event_timestamp; momentum_animation_initial_stretch_ = helper_->StretchAmount(); momentum_animation_initial_velocity_ = scroll_velocity; momentum_animation_reset_at_next_frame_ = false; // Similarly to the logic in Overscroll, prefer vertical scrolling to // horizontal scrolling. if (fabsf(momentum_animation_initial_velocity_.y()) >= fabsf(momentum_animation_initial_velocity_.x())) momentum_animation_initial_velocity_.set_x(0); if (!CanScrollHorizontally()) momentum_animation_initial_velocity_.set_x(0); if (!CanScrollVertically()) momentum_animation_initial_velocity_.set_y(0); // TODO(crbug.com/394562): This can go away once input is batched to the front // of the frame? Then Animate() would always happen after this, so it would // have a chance to tick the animation there and would return if any // animations were active. helper_->RequestOneBeginFrame(); } void InputScrollElasticityController::Animate(base::TimeTicks time) { if (state_ != kStateMomentumAnimated) return; if (momentum_animation_reset_at_next_frame_) { momentum_animation_start_time_ = time; momentum_animation_initial_stretch_ = helper_->StretchAmount(); momentum_animation_initial_velocity_ = gfx::Vector2dF(); momentum_animation_reset_at_next_frame_ = false; } float time_delta = std::max((time - momentum_animation_start_time_).InSecondsF(), 0.0); gfx::Vector2dF old_stretch_amount = helper_->StretchAmount(); gfx::Vector2dF new_stretch_amount = StretchAmountForTimeDelta( momentum_animation_initial_stretch_, momentum_animation_initial_velocity_, time_delta); gfx::Vector2dF stretch_delta = new_stretch_amount - old_stretch_amount; // If the new stretch amount is near zero, set it directly to zero and enter // the inactive state. if (fabs(new_stretch_amount.x()) < 1 && fabs(new_stretch_amount.y()) < 1) { helper_->SetStretchAmount(gfx::Vector2dF()); EnterStateInactive(); return; } // If we are not pinned in the direction of the delta, then the delta is only // allowed to decrease the existing stretch -- it cannot increase a stretch // until it is pinned. if (!PinnedHorizontally(stretch_delta.x())) { if (stretch_delta.x() > 0 && old_stretch_amount.x() < 0) stretch_delta.set_x(std::min(stretch_delta.x(), -old_stretch_amount.x())); else if (stretch_delta.x() < 0 && old_stretch_amount.x() > 0) stretch_delta.set_x(std::max(stretch_delta.x(), -old_stretch_amount.x())); else stretch_delta.set_x(0); } if (!PinnedVertically(stretch_delta.y())) { if (stretch_delta.y() > 0 && old_stretch_amount.y() < 0) stretch_delta.set_y(std::min(stretch_delta.y(), -old_stretch_amount.y())); else if (stretch_delta.y() < 0 && old_stretch_amount.y() > 0) stretch_delta.set_y(std::max(stretch_delta.y(), -old_stretch_amount.y())); else stretch_delta.set_y(0); } new_stretch_amount = old_stretch_amount + stretch_delta; stretch_scroll_force_ = StretchScrollForceForStretchAmount(new_stretch_amount); helper_->SetStretchAmount(new_stretch_amount); // TODO(danakj): Make this a return value back to the compositor to have it // schedule another frame and/or a draw. (Also, crbug.com/551138.) helper_->RequestOneBeginFrame(); } bool InputScrollElasticityController::PinnedHorizontally( float direction) const { gfx::ScrollOffset scroll_offset = helper_->ScrollOffset(); gfx::ScrollOffset max_scroll_offset = helper_->MaxScrollOffset(); if (direction < 0) return scroll_offset.x() <= 0; if (direction > 0) return scroll_offset.x() >= max_scroll_offset.x(); return false; } bool InputScrollElasticityController::PinnedVertically(float direction) const { gfx::ScrollOffset scroll_offset = helper_->ScrollOffset(); gfx::ScrollOffset max_scroll_offset = helper_->MaxScrollOffset(); if (direction < 0) return scroll_offset.y() <= 0; if (direction > 0) return scroll_offset.y() >= max_scroll_offset.y(); return false; } bool InputScrollElasticityController::CanScrollHorizontally() const { return helper_->MaxScrollOffset().x() > 0; } bool InputScrollElasticityController::CanScrollVertically() const { return helper_->MaxScrollOffset().y() > 0; } void InputScrollElasticityController::ReconcileStretchAndScroll() { gfx::Vector2dF stretch = helper_->StretchAmount(); if (stretch.IsZero()) return; gfx::ScrollOffset scroll_offset = helper_->ScrollOffset(); gfx::ScrollOffset max_scroll_offset = helper_->MaxScrollOffset(); // Compute stretch_adjustment which will be added to |stretch| and subtracted // from the |scroll_offset|. gfx::Vector2dF stretch_adjustment; if (stretch.x() < 0 && scroll_offset.x() > 0) { stretch_adjustment.set_x( std::min(-stretch.x(), static_cast(scroll_offset.x()))); } if (stretch.x() > 0 && scroll_offset.x() < max_scroll_offset.x()) { stretch_adjustment.set_x(std::max( -stretch.x(), static_cast(scroll_offset.x() - max_scroll_offset.x()))); } if (stretch.y() < 0 && scroll_offset.y() > 0) { stretch_adjustment.set_y( std::min(-stretch.y(), static_cast(scroll_offset.y()))); } if (stretch.y() > 0 && scroll_offset.y() < max_scroll_offset.y()) { stretch_adjustment.set_y(std::max( -stretch.y(), static_cast(scroll_offset.y() - max_scroll_offset.y()))); } if (stretch_adjustment.IsZero()) return; gfx::Vector2dF new_stretch_amount = stretch + stretch_adjustment; helper_->ScrollBy(-stretch_adjustment); helper_->SetStretchAmount(new_stretch_amount); // Update the internal state for the active scroll or animation to avoid // discontinuities. switch (state_) { case kStateActiveScroll: stretch_scroll_force_ = StretchScrollForceForStretchAmount(new_stretch_amount); break; case kStateMomentumAnimated: momentum_animation_reset_at_next_frame_ = true; break; default: // These cases should not be hit because the stretch must be zero in the // Inactive and MomentumScroll states. NOTREACHED(); break; } } } // namespace ui