| // Copyright 2017 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/browser/renderer_host/input/fling_controller.h" |
| |
| #include "base/time/default_tick_clock.h" |
| #include "base/trace_event/trace_event.h" |
| #include "content/browser/renderer_host/input/gesture_event_queue.h" |
| #include "content/public/browser/content_browser_client.h" |
| #include "content/public/common/content_client.h" |
| #include "ui/events/base_event_utils.h" |
| #include "ui/events/gestures/blink/web_gesture_curve_impl.h" |
| |
| using blink::WebInputEvent; |
| using blink::WebGestureEvent; |
| |
| namespace { |
| constexpr base::TimeDelta kFrameDelta = base::Seconds(1.0 / 60.0); |
| |
| // Maximum time between a fling event's timestamp and the first |Progress| call |
| // for the fling curve to use the fling timestamp as the initial animation time. |
| // Two frames allows a minor delay between event creation and the first |
| // progress. |
| constexpr base::TimeDelta kMaxMicrosecondsFromFlingTimestampToFirstProgress = |
| base::Seconds(2.0 / 60.0); |
| |
| // Since the progress fling is called in ProcessGestureFlingStart right after |
| // processing the GFS, it is possible to have a very small delta for the first |
| // event. Don't send an event with deltas smaller than the |
| // |kMinInertialScrollDelta| since the renderer ignores it and the fling gets |
| // cancelled in RenderWidgetHostViewAndroid::GestureEventAck due to an inertial |
| // GSU with ack ignored. |
| const float kMinInertialScrollDelta = 0.1f; |
| |
| const char* kFlingTraceName = "FlingController::HandlingGestureFling"; |
| } // namespace |
| |
| namespace content { |
| |
| FlingController::Config::Config() {} |
| |
| FlingController::FlingController( |
| FlingControllerEventSenderClient* event_sender_client, |
| FlingControllerSchedulerClient* scheduler_client, |
| const Config& config) |
| : event_sender_client_(event_sender_client), |
| scheduler_client_(scheduler_client), |
| touchpad_tap_suppression_controller_( |
| config.touchpad_tap_suppression_config), |
| touchscreen_tap_suppression_controller_( |
| config.touchscreen_tap_suppression_config), |
| clock_(base::DefaultTickClock::GetInstance()) { |
| DCHECK(event_sender_client); |
| DCHECK(scheduler_client); |
| } |
| |
| FlingController::~FlingController() = default; |
| |
| bool FlingController::ObserveAndFilterForTapSuppression( |
| const GestureEventWithLatencyInfo& gesture_event) { |
| switch (gesture_event.event.GetType()) { |
| case WebInputEvent::Type::kGestureFlingCancel: |
| // The controllers' state is affected by the cancel event and assumes |
| // it's actually stopping an ongoing fling. |
| DCHECK(fling_curve_); |
| if (gesture_event.event.SourceDevice() == |
| blink::WebGestureDevice::kTouchscreen) { |
| touchscreen_tap_suppression_controller_ |
| .GestureFlingCancelStoppedFling(); |
| } else if (gesture_event.event.SourceDevice() == |
| blink::WebGestureDevice::kTouchpad) { |
| touchpad_tap_suppression_controller_.GestureFlingCancelStoppedFling(); |
| } |
| return false; |
| case WebInputEvent::Type::kGestureTapDown: |
| case WebInputEvent::Type::kGestureShowPress: |
| case WebInputEvent::Type::kGestureTapUnconfirmed: |
| case WebInputEvent::Type::kGestureTapCancel: |
| case WebInputEvent::Type::kGestureTap: |
| case WebInputEvent::Type::kGestureDoubleTap: |
| case WebInputEvent::Type::kGestureLongPress: |
| case WebInputEvent::Type::kGestureLongTap: |
| case WebInputEvent::Type::kGestureTwoFingerTap: |
| if (gesture_event.event.SourceDevice() == |
| blink::WebGestureDevice::kTouchscreen) { |
| return touchscreen_tap_suppression_controller_.FilterTapEvent( |
| gesture_event); |
| } |
| return false; |
| default: |
| return false; |
| } |
| } |
| |
| bool FlingController::ObserveAndMaybeConsumeGestureEvent( |
| const GestureEventWithLatencyInfo& gesture_event) { |
| TRACE_EVENT0("input", "FlingController::ObserveAndMaybeConsumeGestureEvent"); |
| // FlingCancel events arrive when a finger is touched down regardless of |
| // whether there is an ongoing fling. These can affect state so if there's no |
| // on-going fling we should just discard these without letting the rest of |
| // the fling system see it. |
| if (gesture_event.event.GetType() == |
| WebInputEvent::Type::kGestureFlingCancel && |
| !fling_curve_) { |
| TRACE_EVENT_INSTANT0("input", "NoActiveFling", TRACE_EVENT_SCOPE_THREAD); |
| return true; |
| } |
| |
| if (ObserveAndFilterForTapSuppression(gesture_event)) { |
| TRACE_EVENT_INSTANT0("input", "FilterTapSuppression", |
| TRACE_EVENT_SCOPE_THREAD); |
| return true; |
| } |
| if (gesture_event.event.GetType() == |
| WebInputEvent::Type::kGestureScrollUpdate) { |
| last_seen_scroll_update_ = gesture_event.event.TimeStamp(); |
| } else if (gesture_event.event.GetType() == |
| WebInputEvent::Type::kGestureScrollEnd || |
| gesture_event.event.GetType() == |
| WebInputEvent::Type::kGestureScrollBegin) { |
| // TODO(bokan): We reset this on Begin as well as End since there appear to |
| // be cases where we see an invalid event sequence: |
| // https://crbug.com/928569. |
| last_seen_scroll_update_ = base::TimeTicks(); |
| } |
| |
| fling_booster_.ObserveGestureEvent(gesture_event.event); |
| |
| // fling_controller_ is in charge of handling GFS events and the events are |
| // not sent to the renderer, the controller processes the fling and generates |
| // fling progress events (wheel events for touchpad and GSU events for |
| // touchscreen and autoscroll) which are handled normally. |
| if (gesture_event.event.GetType() == |
| WebInputEvent::Type::kGestureFlingStart) { |
| ProcessGestureFlingStart(gesture_event); |
| return true; |
| } |
| |
| // If the GestureFlingStart event is processed by the fling_controller_, the |
| // GestureFlingCancel event should be the same. |
| if (gesture_event.event.GetType() == |
| WebInputEvent::Type::kGestureFlingCancel) { |
| ProcessGestureFlingCancel(gesture_event); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| void FlingController::ProcessGestureFlingStart( |
| const GestureEventWithLatencyInfo& gesture_event) { |
| // Don't start a touchpad gesture fling if the previous scroll events were |
| // consumed. |
| if (gesture_event.event.SourceDevice() == |
| blink::WebGestureDevice::kTouchpad && |
| last_wheel_event_consumed_) { |
| return; |
| } |
| |
| if (!UpdateCurrentFlingState(gesture_event.event)) |
| return; |
| |
| TRACE_EVENT_ASYNC_BEGIN2("input", kFlingTraceName, this, "vx", |
| current_fling_parameters_.velocity.x(), "vy", |
| current_fling_parameters_.velocity.y()); |
| |
| last_progress_time_ = base::TimeTicks(); |
| |
| // Wait for BeginFrame to call ProgressFling when |
| // SetNeedsBeginFrameForFlingProgress is used to progress flings instead of |
| // compositor animation observer (happens on Android WebView). |
| if (scheduler_client_->NeedsBeginFrameForFlingProgress()) |
| ScheduleFlingProgress(); |
| else |
| ProgressFling(clock_->NowTicks()); |
| } |
| |
| void FlingController::ScheduleFlingProgress() { |
| scheduler_client_->ScheduleFlingProgress(weak_ptr_factory_.GetWeakPtr()); |
| } |
| |
| void FlingController::ProcessGestureFlingCancel( |
| const GestureEventWithLatencyInfo& gesture_event) { |
| DCHECK(fling_curve_); |
| |
| // Note: We don't want to reset the fling booster here because a FlingCancel |
| // will be received when the user puts their finger down for a potential |
| // boost. FlingBooster will process the event stream after the current fling |
| // is ended and decide whether or not to boost any subsequent FlingStart. |
| EndCurrentFling(gesture_event.event.TimeStamp()); |
| } |
| |
| void FlingController::ProgressFling(base::TimeTicks current_time) { |
| if (!fling_curve_) |
| return; |
| |
| TRACE_EVENT_ASYNC_STEP_INTO0("input", kFlingTraceName, this, "ProgressFling"); |
| |
| if (!first_fling_update_sent()) { |
| // Guard against invalid as there are no guarantees fling event and progress |
| // timestamps are compatible. |
| if (current_fling_parameters_.start_time.is_null()) { |
| current_fling_parameters_.start_time = current_time; |
| ScheduleFlingProgress(); |
| return; |
| } |
| |
| // If the first time that progressFling is called is more than two frames |
| // later than the fling start time, delay the fling start time to one frame |
| // prior to the current time. This makes sure that at least one progress |
| // event is sent while the fling is active even when the fling duration is |
| // short (small velocity) and the time delta between its timestamp and its |
| // processing time is big (e.g. When a GFS gets bubbled from an oopif). |
| if (current_time >= current_fling_parameters_.start_time + |
| kMaxMicrosecondsFromFlingTimestampToFirstProgress) { |
| current_fling_parameters_.start_time = current_time - kFrameDelta; |
| } |
| } |
| |
| // ProgressFling is called inside FlingScheduler::OnAnimationStep. Sometimes |
| // the first OnAnimationStep call has the time of the last frame before |
| // AddAnimationObserver call rather than time of the first frame after |
| // AddAnimationObserver call. Do not advance the fling when current_time is |
| // less than last fling progress time or less than the GFS event timestamp. |
| if (current_time < last_progress_time_ || |
| current_time <= current_fling_parameters_.start_time) { |
| ScheduleFlingProgress(); |
| return; |
| } |
| |
| gfx::Vector2dF delta_to_scroll; |
| bool fling_is_active = fling_curve_->Advance( |
| (current_time - current_fling_parameters_.start_time).InSecondsF(), |
| current_fling_parameters_.velocity, delta_to_scroll); |
| |
| if (!fling_is_active && current_fling_parameters_.source_device != |
| blink::WebGestureDevice::kSyntheticAutoscroll) { |
| fling_booster_.Reset(); |
| EndCurrentFling(current_time); |
| return; |
| } |
| |
| if (std::abs(delta_to_scroll.x()) > kMinInertialScrollDelta || |
| std::abs(delta_to_scroll.y()) > kMinInertialScrollDelta) { |
| GenerateAndSendFlingProgressEvents(current_time, delta_to_scroll); |
| last_progress_time_ = current_time; |
| } |
| |
| // As long as the fling curve is active, the fling progress must get |
| // scheduled even when the last delta to scroll was zero. |
| ScheduleFlingProgress(); |
| } |
| |
| void FlingController::StopFling() { |
| fling_booster_.Reset(); |
| if (fling_curve_) |
| EndCurrentFling(clock_->NowTicks()); |
| } |
| |
| void FlingController::GenerateAndSendWheelEvents( |
| base::TimeTicks current_time, |
| const gfx::Vector2dF& delta, |
| blink::WebMouseWheelEvent::Phase phase) { |
| MouseWheelEventWithLatencyInfo synthetic_wheel( |
| WebInputEvent::Type::kMouseWheel, current_fling_parameters_.modifiers, |
| current_time, ui::LatencyInfo(ui::SourceEventType::WHEEL)); |
| synthetic_wheel.event.delta_units = |
| ui::ScrollGranularity::kScrollByPrecisePixel; |
| synthetic_wheel.event.delta_x = delta.x(); |
| synthetic_wheel.event.delta_y = delta.y(); |
| synthetic_wheel.event.momentum_phase = phase; |
| synthetic_wheel.event.has_synthetic_phase = true; |
| synthetic_wheel.event.SetPositionInWidget(current_fling_parameters_.point); |
| synthetic_wheel.event.SetPositionInScreen( |
| current_fling_parameters_.global_point); |
| // Send wheel events nonblocking. |
| synthetic_wheel.event.dispatch_type = |
| WebInputEvent::DispatchType::kEventNonBlocking; |
| |
| event_sender_client_->SendGeneratedWheelEvent(synthetic_wheel); |
| } |
| |
| void FlingController::GenerateAndSendGestureScrollEvents( |
| base::TimeTicks current_time, |
| WebInputEvent::Type type, |
| const gfx::Vector2dF& delta /* = gfx::Vector2dF() */) { |
| GestureEventWithLatencyInfo synthetic_gesture( |
| type, current_fling_parameters_.modifiers, current_time, |
| ui::LatencyInfo(ui::SourceEventType::INERTIAL)); |
| synthetic_gesture.event.SetPositionInWidget(current_fling_parameters_.point); |
| synthetic_gesture.event.SetPositionInScreen( |
| current_fling_parameters_.global_point); |
| synthetic_gesture.event.primary_pointer_type = |
| blink::WebPointerProperties::PointerType::kTouch; |
| synthetic_gesture.event.SetSourceDevice( |
| current_fling_parameters_.source_device); |
| if (type == WebInputEvent::Type::kGestureScrollUpdate) { |
| synthetic_gesture.event.data.scroll_update.delta_x = delta.x(); |
| synthetic_gesture.event.data.scroll_update.delta_y = delta.y(); |
| synthetic_gesture.event.data.scroll_update.inertial_phase = |
| WebGestureEvent::InertialPhaseState::kMomentum; |
| } else { |
| DCHECK_EQ(WebInputEvent::Type::kGestureScrollEnd, type); |
| synthetic_gesture.event.data.scroll_end.inertial_phase = |
| WebGestureEvent::InertialPhaseState::kMomentum; |
| synthetic_gesture.event.data.scroll_end.generated_by_fling_controller = |
| true; |
| } |
| event_sender_client_->SendGeneratedGestureScrollEvents(synthetic_gesture); |
| } |
| |
| void FlingController::GenerateAndSendFlingProgressEvents( |
| base::TimeTicks current_time, |
| const gfx::Vector2dF& delta) { |
| switch (current_fling_parameters_.source_device) { |
| case blink::WebGestureDevice::kTouchpad: { |
| blink::WebMouseWheelEvent::Phase phase = |
| first_fling_update_sent() ? blink::WebMouseWheelEvent::kPhaseChanged |
| : blink::WebMouseWheelEvent::kPhaseBegan; |
| GenerateAndSendWheelEvents(current_time, delta, phase); |
| break; |
| } |
| case blink::WebGestureDevice::kTouchscreen: |
| case blink::WebGestureDevice::kSyntheticAutoscroll: |
| GenerateAndSendGestureScrollEvents( |
| current_time, WebInputEvent::Type::kGestureScrollUpdate, delta); |
| break; |
| case blink::WebGestureDevice::kUninitialized: |
| case blink::WebGestureDevice::kScrollbar: |
| NOTREACHED() |
| << "Fling controller doesn't handle flings with source device:" |
| << static_cast<int>(current_fling_parameters_.source_device); |
| } |
| fling_booster_.ObserveProgressFling(current_fling_parameters_.velocity); |
| } |
| |
| void FlingController::GenerateAndSendFlingEndEvents( |
| base::TimeTicks current_time) { |
| switch (current_fling_parameters_.source_device) { |
| case blink::WebGestureDevice::kTouchpad: |
| GenerateAndSendWheelEvents(current_time, gfx::Vector2d(), |
| blink::WebMouseWheelEvent::kPhaseEnded); |
| break; |
| case blink::WebGestureDevice::kTouchscreen: |
| case blink::WebGestureDevice::kSyntheticAutoscroll: |
| GenerateAndSendGestureScrollEvents( |
| current_time, WebInputEvent::Type::kGestureScrollEnd); |
| break; |
| case blink::WebGestureDevice::kUninitialized: |
| case blink::WebGestureDevice::kScrollbar: |
| NOTREACHED() |
| << "Fling controller doesn't handle flings with source device:" |
| << static_cast<int>(current_fling_parameters_.source_device); |
| } |
| } |
| |
| void FlingController::EndCurrentFling(base::TimeTicks current_time) { |
| last_progress_time_ = base::TimeTicks(); |
| |
| GenerateAndSendFlingEndEvents(current_time); |
| current_fling_parameters_ = ActiveFlingParameters(); |
| |
| if (fling_curve_) { |
| scheduler_client_->DidStopFlingingOnBrowser(weak_ptr_factory_.GetWeakPtr()); |
| TRACE_EVENT_ASYNC_END0("input", kFlingTraceName, this); |
| } |
| |
| fling_curve_.reset(); |
| } |
| |
| bool FlingController::UpdateCurrentFlingState( |
| const WebGestureEvent& fling_start_event) { |
| DCHECK_EQ(WebInputEvent::Type::kGestureFlingStart, |
| fling_start_event.GetType()); |
| |
| const gfx::Vector2dF velocity = |
| fling_booster_.GetVelocityForFlingStart(fling_start_event); |
| |
| current_fling_parameters_.velocity = velocity; |
| current_fling_parameters_.point = fling_start_event.PositionInWidget(); |
| current_fling_parameters_.global_point = fling_start_event.PositionInScreen(); |
| current_fling_parameters_.modifiers = fling_start_event.GetModifiers(); |
| current_fling_parameters_.source_device = fling_start_event.SourceDevice(); |
| |
| if (fling_start_event.SourceDevice() == |
| blink::WebGestureDevice::kSyntheticAutoscroll || |
| last_seen_scroll_update_.is_null()) { |
| current_fling_parameters_.start_time = fling_start_event.TimeStamp(); |
| } else { |
| // To maintain a smooth, continuous transition from a drag scroll to a fling |
| // scroll, the animation should begin at the time of the last update. |
| current_fling_parameters_.start_time = last_seen_scroll_update_; |
| } |
| |
| if (velocity.IsZero() && fling_start_event.SourceDevice() != |
| blink::WebGestureDevice::kSyntheticAutoscroll) { |
| fling_booster_.Reset(); |
| EndCurrentFling(fling_start_event.TimeStamp()); |
| return false; |
| } |
| |
| gfx::Size root_widget_viewport_size = |
| event_sender_client_->GetRootWidgetViewportSize(); |
| // If the view is destroyed while FlingController is generating fling curve, |
| // |GetRootWidgetViewportSize()| will return empty size. Reset the |
| // state of fling_booster_ and return false. |
| if (root_widget_viewport_size.IsEmpty()) { |
| fling_booster_.Reset(); |
| EndCurrentFling(last_seen_scroll_update_); |
| return false; |
| } |
| |
| gfx::Vector2dF velocity_from_gfs( |
| fling_start_event.data.fling_start.velocity_x, |
| fling_start_event.data.fling_start.velocity_y); |
| |
| float max_velocity_from_gfs = |
| std::max(fabs(velocity_from_gfs.x()), fabs(velocity_from_gfs.y())); |
| float max_velocity = std::max(fabs(current_fling_parameters_.velocity.x()), |
| fabs(current_fling_parameters_.velocity.y())); |
| |
| // Scale the default bound multiplier to compute the maximum scroll distance a |
| // fling can travel based on physics based fling curve. |
| float boost_multiplier = max_velocity / max_velocity_from_gfs; |
| |
| fling_curve_ = std::unique_ptr<blink::WebGestureCurve>( |
| ui::WebGestureCurveImpl::CreateFromDefaultPlatformCurve( |
| current_fling_parameters_.source_device, |
| current_fling_parameters_.velocity, |
| gfx::Vector2dF() /*initial_offset*/, false /*on_main_thread*/, |
| GetContentClient()->browser()->ShouldUseMobileFlingCurve(), |
| current_fling_parameters_.global_point, boost_multiplier, |
| root_widget_viewport_size)); |
| return true; |
| } |
| |
| gfx::Vector2dF FlingController::CurrentFlingVelocity() const { |
| return current_fling_parameters_.velocity; |
| } |
| |
| TouchpadTapSuppressionController* |
| FlingController::GetTouchpadTapSuppressionController() { |
| return &touchpad_tap_suppression_controller_; |
| } |
| |
| void FlingController::OnWheelEventAck( |
| const MouseWheelEventWithLatencyInfo& event, |
| blink::mojom::InputEventResultSource ack_source, |
| blink::mojom::InputEventResultState ack_result) { |
| last_wheel_event_consumed_ = |
| (ack_result == blink::mojom::InputEventResultState::kConsumed); |
| } |
| |
| } // namespace content |