| // 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/gesture_detection/gesture_provider.h" |
| |
| #include <stddef.h> |
| |
| #include <cmath> |
| |
| #include "base/auto_reset.h" |
| #include "base/macros.h" |
| #include "base/trace_event/trace_event.h" |
| #include "ui/events/event_constants.h" |
| #include "ui/events/gesture_detection/gesture_configuration.h" |
| #include "ui/events/gesture_detection/gesture_event_data.h" |
| #include "ui/events/gesture_detection/gesture_listeners.h" |
| #include "ui/events/gesture_detection/motion_event.h" |
| #include "ui/events/gesture_detection/motion_event_generic.h" |
| #include "ui/events/gesture_detection/scale_gesture_listeners.h" |
| #include "ui/gfx/geometry/point_f.h" |
| #include "ui/gfx/geometry/vector2d_f.h" |
| |
| namespace ui { |
| namespace { |
| |
| // Double-tap drag zoom sensitivity (speed). |
| const float kDoubleTapDragZoomSpeed = 0.005f; |
| |
| const char* GetMotionEventActionName(MotionEvent::Action action) { |
| switch (action) { |
| case MotionEvent::Action::NONE: |
| return "Action::NONE"; |
| case MotionEvent::Action::POINTER_DOWN: |
| return "Action::POINTER_DOWN"; |
| case MotionEvent::Action::POINTER_UP: |
| return "Action::POINTER_UP"; |
| case MotionEvent::Action::DOWN: |
| return "Action::DOWN"; |
| case MotionEvent::Action::UP: |
| return "Action::UP"; |
| case MotionEvent::Action::CANCEL: |
| return "Action::CANCEL"; |
| case MotionEvent::Action::MOVE: |
| return "Action::MOVE"; |
| case MotionEvent::Action::HOVER_ENTER: |
| return "Action::HOVER_ENTER"; |
| case MotionEvent::Action::HOVER_EXIT: |
| return "Action::HOVER_EXIT"; |
| case MotionEvent::Action::HOVER_MOVE: |
| return "Action::HOVER_MOVE"; |
| case MotionEvent::Action::BUTTON_PRESS: |
| return "Action::BUTTON_PRESS"; |
| case MotionEvent::Action::BUTTON_RELEASE: |
| return "Action::BUTTON_RELEASE"; |
| } |
| return ""; |
| } |
| |
| gfx::RectF ClampBoundingBox(const gfx::RectF& bounds, |
| float min_length, |
| float max_length) { |
| float width = bounds.width(); |
| float height = bounds.height(); |
| if (min_length) { |
| width = std::max(min_length, width); |
| height = std::max(min_length, height); |
| } |
| if (max_length) { |
| width = std::min(max_length, width); |
| height = std::min(max_length, height); |
| } |
| const gfx::PointF center = bounds.CenterPoint(); |
| return gfx::RectF( |
| center.x() - width / 2.f, center.y() - height / 2.f, width, height); |
| } |
| |
| } // namespace |
| |
| // GestureProviderClient: |
| |
| bool GestureProviderClient::RequiresDoubleTapGestureEvents() const { |
| return false; |
| } |
| |
| // GestureProvider:::Config |
| |
| GestureProvider::Config::Config() |
| : display(display::kInvalidDisplayId, gfx::Rect(1, 1)), |
| double_tap_support_for_platform_enabled(true), |
| gesture_begin_end_types_enabled(false), |
| min_gesture_bounds_length(0), |
| max_gesture_bounds_length(0) {} |
| |
| GestureProvider::Config::Config(const Config& other) = default; |
| |
| GestureProvider::Config::~Config() { |
| } |
| |
| // GestureProvider::GestureListener |
| |
| class GestureProvider::GestureListenerImpl : public ScaleGestureListener, |
| public GestureListener, |
| public DoubleTapListener { |
| public: |
| GestureListenerImpl(const GestureProvider::Config& config, |
| GestureProviderClient* client) |
| : config_(config), |
| client_(client), |
| gesture_detector_(config.gesture_detector_config, this, this), |
| scale_gesture_detector_(config.scale_gesture_detector_config, this), |
| snap_scroll_controller_(config.gesture_detector_config.touch_slop, |
| gfx::SizeF(config.display.size())), |
| ignore_multitouch_zoom_events_(false), |
| ignore_single_tap_(false), |
| pinch_event_sent_(false), |
| scroll_event_sent_(false), |
| max_diameter_before_show_press_(0), |
| show_press_event_sent_(false) {} |
| |
| void OnTouchEvent(const MotionEvent& event) { |
| const bool in_scale_gesture = IsScaleGestureDetectionInProgress(); |
| snap_scroll_controller_.SetSnapScrollMode(event, in_scale_gesture); |
| if (in_scale_gesture) |
| SetIgnoreSingleTap(true); |
| |
| const MotionEvent::Action action = event.GetAction(); |
| if (action == MotionEvent::Action::DOWN) { |
| current_down_action_event_time_ = event.GetEventTime(); |
| current_longpress_time_ = base::TimeTicks(); |
| ignore_single_tap_ = false; |
| scroll_event_sent_ = false; |
| pinch_event_sent_ = false; |
| show_press_event_sent_ = false; |
| gesture_detector_.set_longpress_enabled(true); |
| tap_down_point_ = gfx::PointF(event.GetX(), event.GetY()); |
| max_diameter_before_show_press_ = event.GetTouchMajor(); |
| } |
| gesture_detector_.OnTouchEvent(event, |
| client_->RequiresDoubleTapGestureEvents()); |
| scale_gesture_detector_.OnTouchEvent(event); |
| |
| if (action == MotionEvent::Action::UP || |
| action == MotionEvent::Action::CANCEL) { |
| // Note: This call will have no effect if a fling was just generated, as |
| // |Fling()| will have already signalled an end to touch-scrolling. |
| if (scroll_event_sent_) |
| Send(CreateGesture(ET_GESTURE_SCROLL_END, event)); |
| |
| // If this was the last pointer that was canceled or lifted reset the |
| // |current_down_action_event_time_| to indicate no sequence is going on. |
| if (action != MotionEvent::Action::CANCEL || |
| !GestureConfiguration::GetInstance() |
| ->single_pointer_cancel_enabled() || |
| event.GetPointerCount() == 1) |
| current_down_action_event_time_ = base::TimeTicks(); |
| } else if (action == MotionEvent::Action::MOVE) { |
| if (!show_press_event_sent_ && !scroll_event_sent_) { |
| max_diameter_before_show_press_ = |
| std::max(max_diameter_before_show_press_, event.GetTouchMajor()); |
| } |
| } |
| } |
| |
| void Send(GestureEventData gesture) { |
| DCHECK(!gesture.time.is_null()); |
| // The only valid events that should be sent without an active touch |
| // sequence are SHOW_PRESS, TAP and TAP_CANCEL, potentially triggered by |
| // the double-tap delay timing out or being cancelled. |
| DCHECK(!current_down_action_event_time_.is_null() || |
| gesture.type() == ET_GESTURE_TAP || |
| gesture.type() == ET_GESTURE_SHOW_PRESS || |
| gesture.type() == ET_GESTURE_TAP_CANCEL || |
| gesture.type() == ET_GESTURE_BEGIN || |
| gesture.type() == ET_GESTURE_END); |
| |
| if (gesture.primary_tool_type == MotionEvent::ToolType::UNKNOWN || |
| gesture.primary_tool_type == MotionEvent::ToolType::FINGER) { |
| gesture.details.set_bounding_box( |
| ClampBoundingBox(gesture.details.bounding_box_f(), |
| config_.min_gesture_bounds_length, |
| config_.max_gesture_bounds_length)); |
| } |
| |
| switch (gesture.type()) { |
| case ET_GESTURE_LONG_PRESS: |
| DCHECK(!IsScaleGestureDetectionInProgress()); |
| current_longpress_time_ = gesture.time; |
| break; |
| case ET_GESTURE_LONG_TAP: |
| current_longpress_time_ = base::TimeTicks(); |
| break; |
| case ET_GESTURE_SCROLL_BEGIN: |
| DCHECK(!scroll_event_sent_); |
| scroll_event_sent_ = true; |
| break; |
| case ET_GESTURE_SCROLL_END: |
| DCHECK(scroll_event_sent_); |
| if (pinch_event_sent_) |
| Send(GestureEventData(ET_GESTURE_PINCH_END, gesture)); |
| scroll_event_sent_ = false; |
| break; |
| case ET_SCROLL_FLING_START: |
| DCHECK(scroll_event_sent_); |
| scroll_event_sent_ = false; |
| break; |
| case ET_GESTURE_PINCH_BEGIN: |
| DCHECK(!pinch_event_sent_); |
| if (!scroll_event_sent_ && |
| !scale_gesture_detector_.InAnchoredScaleMode()) { |
| Send(GestureEventData(ET_GESTURE_SCROLL_BEGIN, gesture)); |
| } |
| pinch_event_sent_ = true; |
| break; |
| case ET_GESTURE_PINCH_END: |
| DCHECK(pinch_event_sent_); |
| pinch_event_sent_ = false; |
| break; |
| case ET_GESTURE_SHOW_PRESS: |
| // It's possible that a double-tap drag zoom (from ScaleGestureDetector) |
| // will start before the press gesture fires (from GestureDetector), in |
| // which case the press should simply be dropped. |
| if (pinch_event_sent_ || scroll_event_sent_) |
| return; |
| break; |
| default: |
| break; |
| }; |
| |
| client_->OnGestureEvent(gesture); |
| GestureTouchUMAHistogram::RecordGestureEvent(gesture); |
| } |
| |
| // ScaleGestureListener implementation. |
| bool OnScaleBegin(const ScaleGestureDetector& detector, |
| const MotionEvent& e) override { |
| if (ignore_multitouch_zoom_events_ && !detector.InAnchoredScaleMode()) |
| return false; |
| return true; |
| } |
| |
| void OnScaleEnd(const ScaleGestureDetector& detector, |
| const MotionEvent& e) override { |
| if (!pinch_event_sent_) |
| return; |
| Send(CreateGesture(ET_GESTURE_PINCH_END, e)); |
| } |
| |
| bool OnScale(const ScaleGestureDetector& detector, |
| const MotionEvent& e) override { |
| if (ignore_multitouch_zoom_events_ && !detector.InAnchoredScaleMode()) |
| return false; |
| bool first_scale = false; |
| if (!pinch_event_sent_) { |
| first_scale = true; |
| Send(CreateGesture(ET_GESTURE_PINCH_BEGIN, |
| e.GetPointerId(), |
| e.GetToolType(), |
| detector.GetEventTime(), |
| detector.GetFocusX(), |
| detector.GetFocusY(), |
| detector.GetFocusX() + e.GetRawOffsetX(), |
| detector.GetFocusY() + e.GetRawOffsetY(), |
| e.GetPointerCount(), |
| GetBoundingBox(e, ET_GESTURE_PINCH_BEGIN), |
| e.GetFlags())); |
| } |
| |
| if (std::abs(detector.GetCurrentSpan() - detector.GetPreviousSpan()) < |
| config_.scale_gesture_detector_config.min_pinch_update_span_delta) { |
| return false; |
| } |
| |
| float scale = detector.GetScaleFactor(); |
| if (scale == 1) |
| return true; |
| |
| if (detector.InAnchoredScaleMode()) { |
| // Relative changes in the double-tap scale factor computed by |detector| |
| // diminish as the touch moves away from the original double-tap focus. |
| // For historical reasons, Chrome has instead adopted a scale factor |
| // computation that is invariant to the focal distance, where |
| // the scale delta remains constant if the touch velocity is constant. |
| // Note: Because we calculate the scale here manually based on the |
| // y-span, but the scale factor accounts for slop in the first previous |
| // span, we manaully reproduce the behavior here for previous span y. |
| float prev_y = first_scale |
| ? config_.gesture_detector_config.touch_slop * 2 |
| : detector.GetPreviousSpanY(); |
| float dy = (detector.GetCurrentSpanY() - prev_y) * 0.5f; |
| scale = std::pow(scale > 1 ? 1.0f + kDoubleTapDragZoomSpeed |
| : 1.0f - kDoubleTapDragZoomSpeed, |
| std::abs(dy)); |
| } |
| GestureEventDetails pinch_details(ET_GESTURE_PINCH_UPDATE); |
| pinch_details.set_device_type(GestureDeviceType::DEVICE_TOUCHSCREEN); |
| pinch_details.set_scale(scale); |
| Send(CreateGesture(pinch_details, |
| e.GetPointerId(), |
| e.GetToolType(), |
| detector.GetEventTime(), |
| detector.GetFocusX(), |
| detector.GetFocusY(), |
| detector.GetFocusX() + e.GetRawOffsetX(), |
| detector.GetFocusY() + e.GetRawOffsetY(), |
| e.GetPointerCount(), |
| GetBoundingBox(e, pinch_details.type()), |
| e.GetFlags())); |
| return true; |
| } |
| |
| // GestureListener implementation. |
| bool OnDown(const MotionEvent& e) override { |
| GestureEventDetails tap_details(ET_GESTURE_TAP_DOWN); |
| tap_details.set_device_type(GestureDeviceType::DEVICE_TOUCHSCREEN); |
| Send(CreateGesture(tap_details, e)); |
| |
| // Return true to indicate that we want to handle touch. |
| return true; |
| } |
| |
| bool OnScroll(const MotionEvent& e1, |
| const MotionEvent& e2, |
| const MotionEvent& secondary_pointer_down, |
| float raw_distance_x, |
| float raw_distance_y) override { |
| float distance_x = raw_distance_x; |
| float distance_y = raw_distance_y; |
| if (!scroll_event_sent_ && e2.GetPointerCount() < 3) { |
| // Remove the touch slop region from the first scroll event to avoid a |
| // jump. Touch slop isn't used for scroll gestures with greater than 2 |
| // pointers down, in those cases we don't subtract the slop. |
| gfx::Vector2dF delta = |
| ComputeFirstScrollDelta(e1, e2, secondary_pointer_down); |
| distance_x = delta.x(); |
| distance_y = delta.y(); |
| } |
| |
| snap_scroll_controller_.UpdateSnapScrollMode(distance_x, distance_y); |
| if (snap_scroll_controller_.IsSnappingScrolls()) { |
| if (snap_scroll_controller_.IsSnapHorizontal()) |
| distance_y = 0; |
| else |
| distance_x = 0; |
| } |
| |
| if (!distance_x && !distance_y) |
| return true; |
| |
| if (!scroll_event_sent_) { |
| // Note that scroll start hints are in distance traveled, where |
| // scroll deltas are in the opposite direction. |
| GestureEventDetails scroll_details(ET_GESTURE_SCROLL_BEGIN, -distance_x, |
| -distance_y); |
| scroll_details.set_device_type(GestureDeviceType::DEVICE_TOUCHSCREEN); |
| |
| // Scroll focus point always starts with the first touch down point. |
| scroll_focus_point_.SetPoint(e1.GetX(), e1.GetY()); |
| |
| // Use the co-ordinates from the touch down, as these co-ordinates are |
| // used to determine which layer the scroll should affect. |
| Send(CreateGesture(scroll_details, e2.GetPointerId(), e2.GetToolType(), |
| e2.GetEventTime(), e1.GetX(), e1.GetY(), e1.GetRawX(), |
| e1.GetRawY(), e2.GetPointerCount(), |
| GetBoundingBox(e2, scroll_details.type()), |
| e2.GetFlags())); |
| DCHECK(scroll_event_sent_); |
| } |
| scroll_focus_point_.SetPoint(scroll_focus_point_.x() - raw_distance_x, |
| scroll_focus_point_.y() - raw_distance_y); |
| |
| GestureEventDetails scroll_details(ET_GESTURE_SCROLL_UPDATE, -distance_x, |
| -distance_y); |
| scroll_details.set_device_type(GestureDeviceType::DEVICE_TOUCHSCREEN); |
| const gfx::RectF bounding_box = GetBoundingBox(e2, scroll_details.type()); |
| const gfx::PointF raw_center = |
| scroll_focus_point_ + |
| gfx::Vector2dF(e2.GetRawOffsetX(), e2.GetRawOffsetY()); |
| Send(CreateGesture(scroll_details, e2.GetPointerId(), e2.GetToolType(), |
| e2.GetEventTime(), scroll_focus_point_.x(), |
| scroll_focus_point_.y(), raw_center.x(), raw_center.y(), |
| e2.GetPointerCount(), bounding_box, e2.GetFlags())); |
| |
| return true; |
| } |
| |
| bool OnFling(const MotionEvent& e1, |
| const MotionEvent& e2, |
| float velocity_x, |
| float velocity_y) override { |
| if (snap_scroll_controller_.IsSnappingScrolls()) { |
| if (snap_scroll_controller_.IsSnapHorizontal()) { |
| velocity_y = 0; |
| } else { |
| velocity_x = 0; |
| } |
| } |
| |
| if (!velocity_x && !velocity_y) |
| return true; |
| |
| DCHECK(scroll_event_sent_); |
| if (!scroll_event_sent_) { |
| // The native side needs a ET_GESTURE_SCROLL_BEGIN before |
| // ET_SCROLL_FLING_START to send the fling to the correct target. |
| // The distance traveled in one second is a reasonable scroll start hint. |
| GestureEventDetails scroll_details( |
| ET_GESTURE_SCROLL_BEGIN, velocity_x, velocity_y); |
| scroll_details.set_device_type(GestureDeviceType::DEVICE_TOUCHSCREEN); |
| Send(CreateGesture(scroll_details, e2)); |
| } |
| |
| GestureEventDetails fling_details( |
| ET_SCROLL_FLING_START, velocity_x, velocity_y); |
| fling_details.set_device_type(GestureDeviceType::DEVICE_TOUCHSCREEN); |
| Send(CreateGesture(fling_details, e2)); |
| return true; |
| } |
| |
| bool OnSwipe(const MotionEvent& e1, |
| const MotionEvent& e2, |
| float velocity_x, |
| float velocity_y) override { |
| GestureEventDetails swipe_details(ET_GESTURE_SWIPE, velocity_x, velocity_y); |
| swipe_details.set_device_type(GestureDeviceType::DEVICE_TOUCHSCREEN); |
| Send(CreateGesture(swipe_details, e2)); |
| return true; |
| } |
| |
| bool OnTwoFingerTap(const MotionEvent& e1, const MotionEvent& e2) override { |
| // The location of the two finger tap event should be the location of the |
| // primary pointer. |
| GestureEventDetails two_finger_tap_details( |
| ET_GESTURE_TWO_FINGER_TAP, e1.GetTouchMajor(), e1.GetTouchMajor()); |
| two_finger_tap_details.set_device_type( |
| GestureDeviceType::DEVICE_TOUCHSCREEN); |
| Send(CreateGesture(two_finger_tap_details, |
| e2.GetPointerId(), |
| e2.GetToolType(), |
| e2.GetEventTime(), |
| e1.GetX(), |
| e1.GetY(), |
| e1.GetRawX(), |
| e1.GetRawY(), |
| e2.GetPointerCount(), |
| GetBoundingBox(e2, two_finger_tap_details.type()), |
| e2.GetFlags())); |
| return true; |
| } |
| |
| void OnTapCancel(const MotionEvent& e) override { |
| Send(CreateGesture(ET_GESTURE_TAP_CANCEL, e)); |
| } |
| |
| void OnShowPress(const MotionEvent& e) override { |
| GestureEventDetails show_press_details(ET_GESTURE_SHOW_PRESS); |
| show_press_details.set_device_type(GestureDeviceType::DEVICE_TOUCHSCREEN); |
| show_press_event_sent_ = true; |
| Send(CreateGesture(show_press_details, e)); |
| } |
| |
| bool OnSingleTapUp(const MotionEvent& e, int tap_count) override { |
| // This is a hack to address the issue where user hovers |
| // over a link for longer than double_tap_timeout_, then |
| // OnSingleTapConfirmed() is not triggered. But we still |
| // want to trigger the tap event at UP. So we override |
| // OnSingleTapUp() in this case. This assumes singleTapUp |
| // gets always called before singleTapConfirmed. |
| if (!ignore_single_tap_) { |
| if (e.GetEventTime() - current_down_action_event_time_ > |
| config_.gesture_detector_config.double_tap_timeout) { |
| return OnSingleTapImpl(e, tap_count); |
| } else if (!IsDoubleTapEnabled()) { |
| // If double-tap has been disabled, there is no need to wait |
| // for the double-tap timeout. |
| return OnSingleTapImpl(e, tap_count); |
| } else { |
| // Notify Blink about this tapUp event anyway, when none of the above |
| // conditions applied. |
| Send(CreateTapGesture(ET_GESTURE_TAP_UNCONFIRMED, e, 1)); |
| } |
| } |
| |
| if (e.GetAction() == MotionEvent::Action::UP && |
| !current_longpress_time_.is_null() && |
| !IsScaleGestureDetectionInProgress()) { |
| GestureEventDetails long_tap_details(ET_GESTURE_LONG_TAP); |
| long_tap_details.set_device_type(GestureDeviceType::DEVICE_TOUCHSCREEN); |
| Send(CreateGesture(long_tap_details, e)); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| // DoubleTapListener implementation. |
| bool OnSingleTapConfirmed(const MotionEvent& e) override { |
| return OnSingleTapImpl(e, 1); |
| } |
| |
| bool OnDoubleTap(const MotionEvent& e) override { |
| return scale_gesture_detector_.OnDoubleTap(e); |
| } |
| |
| bool OnDoubleTapEvent(const MotionEvent& e) override { |
| switch (e.GetAction()) { |
| case MotionEvent::Action::DOWN: |
| gesture_detector_.set_longpress_enabled(false); |
| break; |
| |
| case MotionEvent::Action::UP: |
| if (!IsPinchInProgress() && !IsScrollInProgress()) { |
| Send(CreateTapGesture(ET_GESTURE_DOUBLE_TAP, e, 1)); |
| return true; |
| } |
| break; |
| |
| default: |
| break; |
| } |
| return false; |
| } |
| |
| void OnLongPress(const MotionEvent& e) override { |
| DCHECK(!IsDoubleTapInProgress()); |
| SetIgnoreSingleTap(true); |
| GestureEventDetails long_press_details(ET_GESTURE_LONG_PRESS); |
| long_press_details.set_device_type(GestureDeviceType::DEVICE_TOUCHSCREEN); |
| Send(CreateGesture(long_press_details, e)); |
| } |
| |
| GestureEventData CreateGesture(const GestureEventDetails& details, |
| int motion_event_id, |
| MotionEvent::ToolType primary_tool_type, |
| base::TimeTicks time, |
| float x, |
| float y, |
| float raw_x, |
| float raw_y, |
| size_t touch_point_count, |
| const gfx::RectF& bounding_box, |
| int flags) const { |
| return GestureEventData(details, |
| motion_event_id, |
| primary_tool_type, |
| time, |
| x, |
| y, |
| raw_x, |
| raw_y, |
| touch_point_count, |
| bounding_box, |
| flags, |
| 0U); |
| } |
| |
| GestureEventData CreateGesture(EventType type, |
| int motion_event_id, |
| MotionEvent::ToolType primary_tool_type, |
| base::TimeTicks time, |
| float x, |
| float y, |
| float raw_x, |
| float raw_y, |
| size_t touch_point_count, |
| const gfx::RectF& bounding_box, |
| int flags) const { |
| GestureEventDetails details(type); |
| details.set_device_type(GestureDeviceType::DEVICE_TOUCHSCREEN); |
| return GestureEventData(details, |
| motion_event_id, |
| primary_tool_type, |
| time, |
| x, |
| y, |
| raw_x, |
| raw_y, |
| touch_point_count, |
| bounding_box, |
| flags, |
| 0U); |
| } |
| |
| GestureEventData CreateGesture(const GestureEventDetails& details, |
| const MotionEvent& event) const { |
| return GestureEventData(details, event.GetPointerId(), event.GetToolType(), |
| event.GetEventTime(), event.GetX(), event.GetY(), |
| event.GetRawX(), event.GetRawY(), |
| event.GetPointerCount(), |
| GetBoundingBox(event, details.type()), |
| event.GetFlags(), event.GetUniqueEventId()); |
| } |
| |
| GestureEventData CreateGesture(EventType type, |
| const MotionEvent& event) const { |
| GestureEventDetails details(type); |
| details.set_device_type(GestureDeviceType::DEVICE_TOUCHSCREEN); |
| return CreateGesture(details, event); |
| } |
| |
| GestureEventData CreateTapGesture(EventType type, |
| const MotionEvent& event, |
| int tap_count) const { |
| DCHECK_GE(tap_count, 0); |
| GestureEventDetails details(type); |
| details.set_device_type(GestureDeviceType::DEVICE_TOUCHSCREEN); |
| details.set_tap_count(tap_count); |
| return CreateGesture(details, event); |
| } |
| |
| gfx::RectF GetBoundingBox(const MotionEvent& event, EventType type) const { |
| // Can't use gfx::RectF::Union, as it ignores touches with a radius of 0. |
| float left = std::numeric_limits<float>::max(); |
| float top = std::numeric_limits<float>::max(); |
| float right = -std::numeric_limits<float>::max(); |
| float bottom = -std::numeric_limits<float>::max(); |
| for (size_t i = 0; i < event.GetPointerCount(); ++i) { |
| float x, y, diameter; |
| // Only for the show press and tap events, the bounding box is calculated |
| // based on the touch start point and the maximum diameter before the |
| // show press event is sent. |
| if (type == ET_GESTURE_SHOW_PRESS || type == ET_GESTURE_TAP || |
| type == ET_GESTURE_TAP_UNCONFIRMED) { |
| DCHECK_EQ(0U, i); |
| diameter = max_diameter_before_show_press_; |
| x = tap_down_point_.x(); |
| y = tap_down_point_.y(); |
| } else { |
| diameter = event.GetTouchMajor(i); |
| x = event.GetX(i); |
| y = event.GetY(i); |
| } |
| x = x - diameter / 2; |
| y = y - diameter / 2; |
| left = std::min(left, x); |
| right = std::max(right, x + diameter); |
| top = std::min(top, y); |
| bottom = std::max(bottom, y + diameter); |
| } |
| return gfx::RectF(left, top, right - left, bottom - top); |
| } |
| |
| void SetDoubleTapEnabled(bool enabled) { |
| DCHECK(!IsDoubleTapInProgress()); |
| gesture_detector_.SetDoubleTapListener(enabled ? this : NULL); |
| } |
| |
| void SetMultiTouchZoomEnabled(bool enabled) { |
| // Note that returning false from |OnScaleBegin()| or |OnScale()| prevents |
| // the detector from emitting further scale updates for the current touch |
| // sequence. Thus, if multitouch events are enabled in the middle of a |
| // gesture, it will only take effect with the next gesture. |
| ignore_multitouch_zoom_events_ = !enabled; |
| } |
| |
| bool IsDoubleTapInProgress() const { |
| return gesture_detector_.is_double_tapping() || |
| (IsScaleGestureDetectionInProgress() && InAnchoredScaleMode()); |
| } |
| |
| bool IsScrollInProgress() const { return scroll_event_sent_; } |
| |
| bool IsPinchInProgress() const { return pinch_event_sent_; } |
| |
| private: |
| bool OnSingleTapImpl(const MotionEvent& e, int tap_count) { |
| // Long taps in the edges of the screen have their events delayed by |
| // ContentViewHolder for tab swipe operations. As a consequence of the delay |
| // this method might be called after receiving the up event. |
| // These corner cases should be ignored. |
| if (ignore_single_tap_) |
| return true; |
| |
| ignore_single_tap_ = true; |
| |
| Send(CreateTapGesture(ET_GESTURE_TAP, e, tap_count)); |
| return true; |
| } |
| |
| bool IsScaleGestureDetectionInProgress() const { |
| return scale_gesture_detector_.IsInProgress(); |
| } |
| |
| bool InAnchoredScaleMode() const { |
| return scale_gesture_detector_.InAnchoredScaleMode(); |
| } |
| |
| bool IsDoubleTapEnabled() const { |
| return gesture_detector_.has_doubletap_listener() && |
| client_->RequiresDoubleTapGestureEvents(); |
| } |
| |
| void SetIgnoreSingleTap(bool value) { ignore_single_tap_ = value; } |
| |
| gfx::Vector2dF SubtractSlopRegion(const float dx, const float dy) { |
| float distance = std::sqrt(dx * dx + dy * dy); |
| float epsilon = 1e-3f; |
| if (distance > epsilon) { |
| float ratio = |
| std::max(0.f, distance - config_.gesture_detector_config.touch_slop) / |
| distance; |
| gfx::Vector2dF delta(dx * ratio, dy * ratio); |
| return delta; |
| } |
| gfx::Vector2dF delta(dx, dy); |
| return delta; |
| } |
| |
| // When any of the currently down pointers exceeds its slop region |
| // for the first time, scroll delta is adjusted. |
| // The new deltas are calculated for each pointer individually, |
| // and the final scroll delta is the average over all delta values. |
| gfx::Vector2dF ComputeFirstScrollDelta( |
| const MotionEvent& ev1, |
| const MotionEvent& ev2, |
| const MotionEvent& secondary_pointer_down) { |
| // If there are more than two down pointers, tapping is not possible, |
| // so Slop region is not deducted. |
| DCHECK(ev2.GetPointerCount() < 3); |
| |
| gfx::Vector2dF delta(0, 0); |
| for (size_t i = 0; i < ev2.GetPointerCount(); i++) { |
| const int pointer_id = ev2.GetPointerId(i); |
| const MotionEvent* source_pointer_down_event = |
| gesture_detector_.GetSourcePointerDownEvent( |
| ev1, &secondary_pointer_down, pointer_id); |
| |
| if (!source_pointer_down_event) |
| continue; |
| int source_index = |
| source_pointer_down_event->FindPointerIndexOfId(pointer_id); |
| DCHECK_GE(source_index, 0); |
| if (source_index < 0) |
| continue; |
| float dx = source_pointer_down_event->GetX(source_index) - ev2.GetX(i); |
| float dy = source_pointer_down_event->GetY(source_index) - ev2.GetY(i); |
| delta += SubtractSlopRegion(dx, dy); |
| } |
| delta.Scale(1.0 / ev2.GetPointerCount()); |
| return delta; |
| } |
| |
| const GestureProvider::Config config_; |
| GestureProviderClient* const client_; |
| |
| GestureDetector gesture_detector_; |
| ScaleGestureDetector scale_gesture_detector_; |
| SnapScrollController snap_scroll_controller_; |
| |
| // Keeps track of the event time of the first down action in current touch |
| // sequence. |
| base::TimeTicks current_down_action_event_time_; |
| |
| // Keeps track of the current GESTURE_LONG_PRESS event. If a context menu is |
| // opened after a GESTURE_LONG_PRESS, this is used to insert a |
| // GESTURE_TAP_CANCEL for removing any ::active styling. |
| base::TimeTicks current_longpress_time_; |
| |
| // Completely silence multi-touch (pinch) scaling events. Used in WebView when |
| // zoom support is turned off. |
| bool ignore_multitouch_zoom_events_; |
| |
| // TODO(klobag): This is to avoid a bug in GestureDetector. With multi-touch, |
| // always_in_tap_region_ is not reset. So when the last finger is up, |
| // |OnSingleTapUp()| will be mistakenly fired. |
| bool ignore_single_tap_; |
| |
| // Tracks whether {PINCH|SCROLL}_BEGIN events have been forwarded for the |
| // current touch sequence. |
| bool pinch_event_sent_; |
| bool scroll_event_sent_; |
| |
| // Only track the maximum diameter before the show press event has been |
| // sent and a tap must still be possible for this touch sequence. |
| float max_diameter_before_show_press_; |
| |
| gfx::PointF tap_down_point_; |
| |
| // Tracks whether an ET_GESTURE_SHOW_PRESS event has been sent for this touch |
| // sequence. |
| bool show_press_event_sent_; |
| |
| // The scroll focus point is set to the first touch down point when scroll |
| // begins and is later updated based on the delta of touch points. |
| gfx::PointF scroll_focus_point_; |
| DISALLOW_COPY_AND_ASSIGN(GestureListenerImpl); |
| }; |
| |
| // GestureProvider |
| |
| GestureProvider::GestureProvider(const Config& config, |
| GestureProviderClient* client) |
| : double_tap_support_for_page_(true), |
| double_tap_support_for_platform_( |
| config.double_tap_support_for_platform_enabled), |
| gesture_begin_end_types_enabled_(config.gesture_begin_end_types_enabled) { |
| DCHECK(client); |
| DCHECK(!config.min_gesture_bounds_length || |
| !config.max_gesture_bounds_length || |
| config.min_gesture_bounds_length <= config.max_gesture_bounds_length); |
| TRACE_EVENT0("input", "GestureProvider::InitGestureDetectors"); |
| gesture_listener_ = std::make_unique<GestureListenerImpl>(config, client); |
| UpdateDoubleTapDetectionSupport(); |
| } |
| |
| GestureProvider::~GestureProvider() { |
| } |
| |
| bool GestureProvider::OnTouchEvent(const MotionEvent& event) { |
| TRACE_EVENT1("input", |
| "GestureProvider::OnTouchEvent", |
| "action", |
| GetMotionEventActionName(event.GetAction())); |
| DCHECK_NE(0u, event.GetPointerCount()); |
| |
| if (!CanHandle(event)) |
| return false; |
| |
| OnTouchEventHandlingBegin(event); |
| gesture_listener_->OnTouchEvent(event); |
| OnTouchEventHandlingEnd(event); |
| uma_histogram_.RecordTouchEvent(event); |
| return true; |
| } |
| |
| void GestureProvider::ResetDetection() { |
| MotionEventGeneric generic_cancel_event( |
| MotionEvent::Action::CANCEL, base::TimeTicks::Now(), PointerProperties()); |
| OnTouchEvent(generic_cancel_event); |
| } |
| |
| void GestureProvider::SetMultiTouchZoomSupportEnabled(bool enabled) { |
| gesture_listener_->SetMultiTouchZoomEnabled(enabled); |
| } |
| |
| void GestureProvider::SetDoubleTapSupportForPlatformEnabled(bool enabled) { |
| if (double_tap_support_for_platform_ == enabled) |
| return; |
| double_tap_support_for_platform_ = enabled; |
| UpdateDoubleTapDetectionSupport(); |
| } |
| |
| void GestureProvider::SetDoubleTapSupportForPageEnabled(bool enabled) { |
| if (double_tap_support_for_page_ == enabled) |
| return; |
| double_tap_support_for_page_ = enabled; |
| UpdateDoubleTapDetectionSupport(); |
| } |
| |
| bool GestureProvider::IsScrollInProgress() const { |
| return gesture_listener_->IsScrollInProgress(); |
| } |
| |
| bool GestureProvider::IsPinchInProgress() const { |
| return gesture_listener_->IsPinchInProgress(); |
| } |
| |
| bool GestureProvider::IsDoubleTapInProgress() const { |
| return gesture_listener_->IsDoubleTapInProgress(); |
| } |
| |
| bool GestureProvider::CanHandle(const MotionEvent& event) const { |
| // Aura requires one cancel event per touch point, whereas Android requires |
| // one cancel event per touch sequence. Thus we need to allow extra cancel |
| // events. |
| return current_down_event_ || |
| event.GetAction() == MotionEvent::Action::DOWN || |
| event.GetAction() == MotionEvent::Action::CANCEL; |
| } |
| |
| void GestureProvider::OnTouchEventHandlingBegin(const MotionEvent& event) { |
| switch (event.GetAction()) { |
| case MotionEvent::Action::DOWN: |
| current_down_event_ = event.Clone(); |
| if (gesture_begin_end_types_enabled_) |
| gesture_listener_->Send( |
| gesture_listener_->CreateGesture(ET_GESTURE_BEGIN, event)); |
| break; |
| case MotionEvent::Action::POINTER_DOWN: |
| if (gesture_begin_end_types_enabled_) { |
| const int action_index = event.GetActionIndex(); |
| gesture_listener_->Send(gesture_listener_->CreateGesture( |
| ET_GESTURE_BEGIN, |
| event.GetPointerId(), |
| event.GetToolType(), |
| event.GetEventTime(), |
| event.GetX(action_index), |
| event.GetY(action_index), |
| event.GetRawX(action_index), |
| event.GetRawY(action_index), |
| event.GetPointerCount(), |
| gesture_listener_->GetBoundingBox(event, ET_GESTURE_BEGIN), |
| event.GetFlags())); |
| } |
| break; |
| case MotionEvent::Action::POINTER_UP: |
| case MotionEvent::Action::UP: |
| case MotionEvent::Action::CANCEL: |
| case MotionEvent::Action::MOVE: |
| break; |
| case MotionEvent::Action::NONE: |
| case MotionEvent::Action::HOVER_ENTER: |
| case MotionEvent::Action::HOVER_EXIT: |
| case MotionEvent::Action::HOVER_MOVE: |
| case MotionEvent::Action::BUTTON_PRESS: |
| case MotionEvent::Action::BUTTON_RELEASE: |
| NOTREACHED(); |
| break; |
| } |
| } |
| |
| void GestureProvider::OnTouchEventHandlingEnd(const MotionEvent& event) { |
| switch (event.GetAction()) { |
| case MotionEvent::Action::UP: |
| case MotionEvent::Action::CANCEL: { |
| if (gesture_begin_end_types_enabled_) |
| gesture_listener_->Send( |
| gesture_listener_->CreateGesture(ET_GESTURE_END, event)); |
| |
| if (event.GetAction() != MotionEvent::Action::CANCEL || |
| !GestureConfiguration::GetInstance() |
| ->single_pointer_cancel_enabled() || |
| event.GetPointerCount() == 1) |
| current_down_event_.reset(); |
| |
| UpdateDoubleTapDetectionSupport(); |
| break; |
| } |
| case MotionEvent::Action::POINTER_UP: |
| if (gesture_begin_end_types_enabled_) |
| gesture_listener_->Send( |
| gesture_listener_->CreateGesture(ET_GESTURE_END, event)); |
| break; |
| case MotionEvent::Action::DOWN: |
| case MotionEvent::Action::POINTER_DOWN: |
| case MotionEvent::Action::MOVE: |
| break; |
| case MotionEvent::Action::NONE: |
| case MotionEvent::Action::HOVER_ENTER: |
| case MotionEvent::Action::HOVER_EXIT: |
| case MotionEvent::Action::HOVER_MOVE: |
| case MotionEvent::Action::BUTTON_PRESS: |
| case MotionEvent::Action::BUTTON_RELEASE: |
| NOTREACHED(); |
| break; |
| } |
| } |
| |
| void GestureProvider::UpdateDoubleTapDetectionSupport() { |
| // The GestureDetector requires that any provided DoubleTapListener remain |
| // attached to it for the duration of a touch sequence. Defer any potential |
| // null'ing of the listener until the sequence has ended. |
| if (current_down_event_) |
| return; |
| |
| const bool double_tap_enabled = |
| double_tap_support_for_page_ && double_tap_support_for_platform_; |
| gesture_listener_->SetDoubleTapEnabled(double_tap_enabled); |
| } |
| |
| } // namespace ui |