// Copyright 2017 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/fling_booster.h" using blink::WebGestureEvent; using blink::WebInputEvent; namespace { // Minimum fling velocity required for the active fling and new fling for the // two to accumulate. const double kMinBoostFlingSpeedSquare = 350. * 350.; // Minimum velocity for the active touch scroll to preserve (boost) an active // fling for which cancellation has been deferred. const double kMinBoostTouchScrollSpeedSquare = 150 * 150.; // Timeout window after which the active fling will be cancelled if no animation // ticks, scrolls or flings of sufficient velocity relative to the current fling // are received. The default value on Android native views is 40ms, but we use a // slightly increased value to accomodate small IPC message delays. const double kFlingBoostTimeoutDelaySeconds = 0.05; } // namespace namespace ui { FlingBooster::FlingBooster(const gfx::Vector2dF& fling_velocity, blink::WebGestureDevice source_device, int modifiers) : current_fling_velocity_(fling_velocity), source_device_(source_device), modifiers_(modifiers), deferred_fling_cancel_time_seconds_(0), last_fling_animate_time_seconds_(0), fling_boosted_(false) {} bool FlingBooster::FilterGestureEventForFlingBoosting( const WebGestureEvent& gesture_event, bool* out_cancel_current_fling) { DCHECK(out_cancel_current_fling); *out_cancel_current_fling = false; if (gesture_event.GetType() == WebInputEvent::kGestureFlingCancel) { if (gesture_event.data.fling_cancel.prevent_boosting) return false; if (current_fling_velocity_.LengthSquared() < kMinBoostFlingSpeedSquare) return false; deferred_fling_cancel_time_seconds_ = gesture_event.TimeStampSeconds() + kFlingBoostTimeoutDelaySeconds; return true; } // A fling is either inactive or is "free spinning", i.e., has yet to be // interrupted by a touch gesture, in which case there is nothing to filter. if (!deferred_fling_cancel_time_seconds_) return false; // Gestures from a different source should immediately interrupt the fling. if (gesture_event.SourceDevice() != source_device_) { *out_cancel_current_fling = true; return false; } switch (gesture_event.GetType()) { case WebInputEvent::kGestureTapCancel: case WebInputEvent::kGestureTapDown: return false; case WebInputEvent::kGestureScrollBegin: // TODO(jdduke): Use |gesture_event.data.scrollBegin.delta{X,Y}Hint| to // determine if the ScrollBegin should immediately cancel the fling. ExtendBoostedFlingTimeout(gesture_event); return true; case WebInputEvent::kGestureScrollUpdate: { if (gesture_event.data.scroll_update.inertial_phase == WebGestureEvent::kMomentumPhase) { // GSU events in momentum phase are generated by FlingController to // progress fling and should not interfere with fling boosting. return false; } if (ShouldSuppressScrollForFlingBoosting(gesture_event)) { ExtendBoostedFlingTimeout(gesture_event); return true; } *out_cancel_current_fling = true; return false; } case WebInputEvent::kGestureScrollEnd: // Clear the last fling boost event *prior* to fling cancellation, // preventing insertion of a synthetic GestureScrollBegin. last_fling_boost_event_ = WebGestureEvent(); *out_cancel_current_fling = true; return true; case WebInputEvent::kGestureFlingStart: { DCHECK_EQ(source_device_, gesture_event.SourceDevice()); gfx::Vector2dF new_fling_velocity( gesture_event.data.fling_start.velocity_x, gesture_event.data.fling_start.velocity_y); DCHECK(!new_fling_velocity.IsZero()); fling_boosted_ = ShouldBoostFling(gesture_event); if (fling_boosted_) current_fling_velocity_ += new_fling_velocity; else current_fling_velocity_ = new_fling_velocity; deferred_fling_cancel_time_seconds_ = 0; last_fling_boost_event_ = WebGestureEvent(); return true; } default: // All other types of gestures (taps, presses, etc...) will complete the // deferred fling cancellation. *out_cancel_current_fling = true; return false; } } bool FlingBooster::MustCancelDeferredFling() const { return deferred_fling_cancel_time_seconds_ && last_fling_animate_time_seconds_ > deferred_fling_cancel_time_seconds_; } bool FlingBooster::ShouldBoostFling(const WebGestureEvent& fling_start_event) { DCHECK_EQ(WebInputEvent::kGestureFlingStart, fling_start_event.GetType()); gfx::Vector2dF new_fling_velocity( fling_start_event.data.fling_start.velocity_x, fling_start_event.data.fling_start.velocity_y); if (gfx::DotProduct(current_fling_velocity_, new_fling_velocity) <= 0) return false; if (current_fling_velocity_.LengthSquared() < kMinBoostFlingSpeedSquare) return false; if (new_fling_velocity.LengthSquared() < kMinBoostFlingSpeedSquare) return false; if (modifiers_ != fling_start_event.GetModifiers()) return false; return true; } bool FlingBooster::ShouldSuppressScrollForFlingBoosting( const WebGestureEvent& scroll_update_event) { DCHECK_EQ(WebInputEvent::kGestureScrollUpdate, scroll_update_event.GetType()); gfx::Vector2dF dx(scroll_update_event.data.scroll_update.delta_x, scroll_update_event.data.scroll_update.delta_y); if (gfx::DotProduct(current_fling_velocity_, dx) <= 0) return false; const double time_since_last_fling_animate = std::max(0.0, scroll_update_event.TimeStampSeconds() - last_fling_animate_time_seconds_); if (time_since_last_fling_animate > kFlingBoostTimeoutDelaySeconds) return false; const double time_since_last_boost_event = scroll_update_event.TimeStampSeconds() - last_fling_boost_event_.TimeStampSeconds(); if (time_since_last_boost_event < 0.001) return true; // TODO(jdduke): Use |scroll_update_event.data.scrollUpdate.velocity{X,Y}|. // The scroll must be of sufficient velocity to maintain the active fling. const gfx::Vector2dF scroll_velocity = gfx::ScaleVector2d(dx, 1. / time_since_last_boost_event); if (scroll_velocity.LengthSquared() < kMinBoostTouchScrollSpeedSquare) return false; return true; } void FlingBooster::ExtendBoostedFlingTimeout( const blink::WebGestureEvent& event) { deferred_fling_cancel_time_seconds_ = event.TimeStampSeconds() + kFlingBoostTimeoutDelaySeconds; last_fling_boost_event_ = event; } } // namespace ui