| // Copyright 2014 The Chromium Authors |
| // 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_detector.h" |
| |
| #include <stddef.h> |
| |
| #include <algorithm> |
| #include <array> |
| #include <cmath> |
| |
| #include "base/memory/raw_ptr.h" |
| #include "base/numerics/angle_conversions.h" |
| #include "base/timer/timer.h" |
| #include "build/build_config.h" |
| #include "ui/base/ui_base_features.h" |
| #include "ui/events/event_constants.h" |
| #include "ui/events/gesture_detection/gesture_listeners.h" |
| #include "ui/events/velocity_tracker/motion_event.h" |
| |
| namespace ui { |
| namespace { |
| |
| // Minimum distance a scroll must have traveled from the last scroll/focal point |
| // to trigger an |OnScroll| callback. |
| const float kScrollEpsilon = .1f; |
| |
| // Constants used by TimeoutGestureHandler. |
| enum TimeoutEvent { |
| SHOW_PRESS = 0, |
| SHORT_PRESS, |
| LONG_PRESS, |
| TAP, |
| TIMEOUT_EVENT_COUNT |
| }; |
| |
| } // namespace |
| |
| GestureDetector::Config::Config() = default; |
| GestureDetector::Config::Config(const Config& other) = default; |
| GestureDetector::Config::~Config() = default; |
| |
| class GestureDetector::TimeoutGestureHandler { |
| public: |
| TimeoutGestureHandler(const Config& config, GestureDetector* gesture_detector) |
| : gesture_detector_(gesture_detector) { |
| DCHECK(config.shortpress_timeout <= config.longpress_timeout); |
| |
| timeout_callbacks_[SHOW_PRESS] = &GestureDetector::OnShowPressTimeout; |
| timeout_delays_[SHOW_PRESS] = config.showpress_timeout; |
| |
| timeout_callbacks_[SHORT_PRESS] = &GestureDetector::OnShortPressTimeout; |
| timeout_delays_[SHORT_PRESS] = |
| config.shortpress_timeout + config.showpress_timeout; |
| |
| timeout_callbacks_[LONG_PRESS] = &GestureDetector::OnLongPressTimeout; |
| timeout_delays_[LONG_PRESS] = |
| config.longpress_timeout + config.showpress_timeout; |
| |
| timeout_callbacks_[TAP] = &GestureDetector::OnTapTimeout; |
| timeout_delays_[TAP] = config.double_tap_timeout; |
| |
| if (config.task_runner) { |
| timeout_timers_[SHOW_PRESS].SetTaskRunner(config.task_runner); |
| timeout_timers_[LONG_PRESS].SetTaskRunner(config.task_runner); |
| timeout_timers_[TAP].SetTaskRunner(config.task_runner); |
| } |
| } |
| |
| ~TimeoutGestureHandler() { |
| Stop(); |
| } |
| |
| void StartTimeout(TimeoutEvent event) { |
| timeout_timers_[event].Start(FROM_HERE, timeout_delays_[event], |
| gesture_detector_.get(), |
| timeout_callbacks_[event]); |
| } |
| |
| void StopTimeout(TimeoutEvent event) { timeout_timers_[event].Stop(); } |
| |
| void Stop() { |
| for (size_t i = SHOW_PRESS; i < TIMEOUT_EVENT_COUNT; ++i) |
| timeout_timers_[i].Stop(); |
| } |
| |
| bool HasTimeout(TimeoutEvent event) const { |
| return timeout_timers_[event].IsRunning(); |
| } |
| |
| private: |
| typedef void (GestureDetector::*ReceiverMethod)(); |
| |
| const raw_ptr<GestureDetector> gesture_detector_; |
| std::array<base::OneShotTimer, TIMEOUT_EVENT_COUNT> timeout_timers_; |
| std::array<ReceiverMethod, TIMEOUT_EVENT_COUNT> timeout_callbacks_; |
| std::array<base::TimeDelta, TIMEOUT_EVENT_COUNT> timeout_delays_; |
| }; |
| |
| GestureDetector::GestureDetector( |
| const Config& config, |
| GestureListener* listener, |
| DoubleTapListener* optional_double_tap_listener) |
| : timeout_handler_(new TimeoutGestureHandler(config, this)), |
| listener_(listener), |
| double_tap_listener_(optional_double_tap_listener), |
| velocity_tracker_(config.velocity_tracker_strategy) { |
| DCHECK(listener_); |
| Init(config); |
| } |
| |
| GestureDetector::~GestureDetector() = default; |
| |
| bool GestureDetector::OnTouchEvent(const MotionEvent& ev, |
| bool should_process_double_tap) { |
| const MotionEvent::Action action = ev.GetAction(); |
| |
| velocity_tracker_.AddMovement(ev); |
| |
| const bool pointer_up = action == MotionEvent::Action::POINTER_UP; |
| const int skip_index = pointer_up ? ev.GetActionIndex() : -1; |
| |
| // Determine focal point. |
| float sum_x = 0, sum_y = 0; |
| const int count = static_cast<int>(ev.GetPointerCount()); |
| for (int i = 0; i < count; i++) { |
| if (skip_index == i) |
| continue; |
| sum_x += ev.GetX(i); |
| sum_y += ev.GetY(i); |
| } |
| const int div = pointer_up ? count - 1 : count; |
| const float focus_x = sum_x / div; |
| const float focus_y = sum_y / div; |
| |
| bool handled = false; |
| |
| switch (action) { |
| 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(); |
| |
| case MotionEvent::Action::POINTER_DOWN: { |
| down_focus_x_ = last_focus_x_ = focus_x; |
| down_focus_y_ = last_focus_y_ = focus_y; |
| // Cancel long press and taps. |
| CancelTaps(); |
| maximum_pointer_count_ = std::max(maximum_pointer_count_, |
| static_cast<int>(ev.GetPointerCount())); |
| |
| // Even when two_finger_tap_allowed_for_gesture_ is false, |
| // second pointer down information must be stored to check |
| // the slop region in multi-finger scrolls. |
| if (ev.GetPointerCount() == 2) |
| secondary_pointer_down_event_ = ev.Clone(); |
| |
| if (!two_finger_tap_allowed_for_gesture_) |
| break; |
| |
| const int action_index = ev.GetActionIndex(); |
| const float dx = ev.GetX(action_index) - current_down_event_->GetX(); |
| const float dy = ev.GetY(action_index) - current_down_event_->GetY(); |
| |
| if (maximum_pointer_count_ > 2 || |
| dx * dx + dy * dy >= two_finger_tap_distance_square_) |
| two_finger_tap_allowed_for_gesture_ = false; |
| } break; |
| |
| case MotionEvent::Action::POINTER_UP: { |
| down_focus_x_ = last_focus_x_ = focus_x; |
| down_focus_y_ = last_focus_y_ = focus_y; |
| |
| // Check the dot product of current velocities. |
| // If the pointer that left was opposing another velocity vector, clear. |
| velocity_tracker_.ComputeCurrentVelocity(1000, max_fling_velocity_); |
| const int up_index = ev.GetActionIndex(); |
| const int id1 = ev.GetPointerId(up_index); |
| const float vx1 = velocity_tracker_.GetXVelocity(id1); |
| const float vy1 = velocity_tracker_.GetYVelocity(id1); |
| float vx_total = vx1; |
| float vy_total = vy1; |
| for (int i = 0; i < count; i++) { |
| if (i == up_index) |
| continue; |
| |
| const int id2 = ev.GetPointerId(i); |
| const float vx2 = velocity_tracker_.GetXVelocity(id2); |
| const float vy2 = velocity_tracker_.GetYVelocity(id2); |
| const float dot = vx1 * vx2 + vy1 * vy2; |
| if (dot < 0) { |
| vx_total = 0; |
| vy_total = 0; |
| velocity_tracker_.Clear(); |
| break; |
| } |
| vx_total += vx2; |
| vy_total += vy2; |
| } |
| |
| handled = HandleSwipeIfNeeded(ev, vx_total / count, vy_total / count); |
| |
| if (two_finger_tap_allowed_for_gesture_ && ev.GetPointerCount() == 2 && |
| secondary_pointer_down_event_ && |
| (ev.GetEventTime() - secondary_pointer_down_event_->GetEventTime() <= |
| two_finger_tap_timeout_)) { |
| handled = listener_->OnTwoFingerTap(*current_down_event_, ev); |
| } |
| two_finger_tap_allowed_for_gesture_ = false; |
| } break; |
| |
| case MotionEvent::Action::DOWN: { |
| bool is_repeated_tap = |
| current_down_event_ && previous_up_event_ && |
| IsRepeatedTap(*current_down_event_, *previous_up_event_, ev, |
| should_process_double_tap); |
| if (double_tap_listener_ && should_process_double_tap) { |
| is_down_candidate_for_repeated_single_tap_ = false; |
| bool had_tap_message = timeout_handler_->HasTimeout(TAP); |
| if (had_tap_message) |
| timeout_handler_->StopTimeout(TAP); |
| if (is_repeated_tap && had_tap_message) { |
| // This is a second tap. |
| is_double_tapping_ = true; |
| // Give a callback with the first tap of the double-tap. |
| handled |= double_tap_listener_->OnDoubleTap(*current_down_event_); |
| // Give a callback with down event of the double-tap. |
| handled |= double_tap_listener_->OnDoubleTapEvent(ev); |
| } else { |
| // This is a first tap. |
| DCHECK(double_tap_timeout_.is_positive()); |
| timeout_handler_->StartTimeout(TAP); |
| } |
| } else { |
| is_down_candidate_for_repeated_single_tap_ = is_repeated_tap; |
| } |
| |
| down_focus_x_ = last_focus_x_ = focus_x; |
| down_focus_y_ = last_focus_y_ = focus_y; |
| current_down_event_ = ev.Clone(); |
| |
| secondary_pointer_down_event_.reset(); |
| all_pointers_within_slop_regions_ = true; |
| always_in_bigger_tap_region_ = true; |
| still_down_ = true; |
| defer_confirm_single_tap_ = false; |
| two_finger_tap_allowed_for_gesture_ = two_finger_tap_enabled_; |
| maximum_pointer_count_ = 1; |
| |
| // Always start the SHOW_PRESS timer before the LONG_PRESS timer to |
| // ensure proper timeout ordering. |
| if (showpress_enabled_) |
| timeout_handler_->StartTimeout(SHOW_PRESS); |
| if (press_and_hold_enabled_) { |
| timeout_handler_->StartTimeout(SHORT_PRESS); |
| timeout_handler_->StartTimeout(LONG_PRESS); |
| } |
| |
| // Number of complete taps that have occurred in the current tap sequence. |
| int previous_tap_count = is_down_candidate_for_repeated_single_tap_ |
| ? (1 + current_single_tap_repeat_count_) % |
| single_tap_repeat_interval_ |
| : 0; |
| handled |= listener_->OnDown(ev, 1 + previous_tap_count); |
| } break; |
| |
| case MotionEvent::Action::MOVE: { |
| const float scroll_x = last_focus_x_ - focus_x; |
| const float scroll_y = last_focus_y_ - focus_y; |
| if (is_double_tapping_) { |
| // Give the move events of the double-tap. |
| DCHECK(double_tap_listener_); |
| handled |= double_tap_listener_->OnDoubleTapEvent(ev); |
| } else if (all_pointers_within_slop_regions_) { |
| if (!IsWithinSlopForTap(ev)) { |
| handled = listener_->OnScroll( |
| *current_down_event_, ev, |
| (maximum_pointer_count_ > 1 && secondary_pointer_down_event_) |
| ? *secondary_pointer_down_event_ |
| : ev, |
| scroll_x, scroll_y); |
| last_focus_x_ = focus_x; |
| last_focus_y_ = focus_y; |
| all_pointers_within_slop_regions_ = false; |
| timeout_handler_->Stop(); |
| } |
| |
| const float delta_x = focus_x - down_focus_x_; |
| const float delta_y = focus_y - down_focus_y_; |
| const float distance_square = delta_x * delta_x + delta_y * delta_y; |
| if (distance_square > double_tap_touch_slop_square_) |
| always_in_bigger_tap_region_ = false; |
| } else if (std::abs(scroll_x) > kScrollEpsilon || |
| std::abs(scroll_y) > kScrollEpsilon) { |
| handled = listener_->OnScroll( |
| *current_down_event_, ev, |
| (maximum_pointer_count_ > 1 && secondary_pointer_down_event_) |
| ? *secondary_pointer_down_event_ |
| : ev, |
| scroll_x, scroll_y); |
| last_focus_x_ = focus_x; |
| last_focus_y_ = focus_y; |
| } |
| |
| // Try to activate long press gesture early. |
| if (ev.GetPointerCount() == 1 && |
| timeout_handler_->HasTimeout(LONG_PRESS)) { |
| if (ev.GetToolType(0) == MotionEvent::ToolType::STYLUS && |
| stylus_button_accelerated_longpress_enabled_ && |
| (ev.GetFlags() & ui::EF_LEFT_MOUSE_BUTTON)) { |
| // This will generate a EventType::kGestureLongPress event with |
| // EF_LEFT_MOUSE_BUTTON. |
| ActivateShortPressGesture(ev); |
| ActivateLongPressGesture(ev); |
| } else if (ev.GetToolType(0) == MotionEvent::ToolType::FINGER && |
| deep_press_accelerated_longpress_enabled_ && |
| ev.GetClassification() == |
| MotionEvent::Classification::DEEP_PRESS) { |
| // This uses the current_down_event_ to generate the short/long press |
| // gesture which keeps the original coordinates in case the current |
| // move event has a different coordinate. |
| OnShortPressTimeout(); |
| OnLongPressTimeout(); |
| } |
| } |
| |
| if (!two_finger_tap_allowed_for_gesture_) |
| break; |
| |
| // Two-finger tap should be prevented if either pointer exceeds its |
| // (independent) slop region. |
| // If the event has had more than two pointers down at any time, |
| // two finger tap should be prevented. |
| if (maximum_pointer_count_ > 2 || !IsWithinSlopForTap(ev)) { |
| two_finger_tap_allowed_for_gesture_ = false; |
| } |
| } break; |
| |
| case MotionEvent::Action::UP: |
| still_down_ = false; |
| { |
| if (is_double_tapping_ && should_process_double_tap) { |
| // Finally, give the up event of the double-tap. |
| DCHECK(double_tap_listener_); |
| handled |= double_tap_listener_->OnDoubleTapEvent(ev); |
| } else if (all_pointers_within_slop_regions_ && |
| maximum_pointer_count_ == 1) { |
| if (is_down_candidate_for_repeated_single_tap_) { |
| current_single_tap_repeat_count_ = |
| (1 + current_single_tap_repeat_count_) % |
| single_tap_repeat_interval_; |
| } else { |
| current_single_tap_repeat_count_ = 0; |
| } |
| handled = listener_->OnSingleTapUp( |
| ev, 1 + current_single_tap_repeat_count_); |
| if (defer_confirm_single_tap_ && should_process_double_tap && |
| double_tap_listener_) { |
| double_tap_listener_->OnSingleTapConfirmed(ev); |
| } |
| } else if (!all_pointers_within_slop_regions_) { |
| // A fling must travel the minimum tap distance. |
| current_single_tap_repeat_count_ = 0; |
| const int pointer_id = ev.GetPointerId(0); |
| velocity_tracker_.ComputeCurrentVelocity(1000, max_fling_velocity_); |
| const float velocity_y = velocity_tracker_.GetYVelocity(pointer_id); |
| const float velocity_x = velocity_tracker_.GetXVelocity(pointer_id); |
| |
| if ((std::abs(velocity_y) > min_fling_velocity_) || |
| (std::abs(velocity_x) > min_fling_velocity_)) { |
| handled = listener_->OnFling(*current_down_event_, ev, velocity_x, |
| velocity_y); |
| } |
| |
| handled |= HandleSwipeIfNeeded(ev, velocity_x, velocity_y); |
| } |
| |
| previous_up_event_ = ev.Clone(); |
| |
| velocity_tracker_.Clear(); |
| is_double_tapping_ = false; |
| defer_confirm_single_tap_ = false; |
| timeout_handler_->StopTimeout(SHOW_PRESS); |
| timeout_handler_->StopTimeout(SHORT_PRESS); |
| timeout_handler_->StopTimeout(LONG_PRESS); |
| } |
| maximum_pointer_count_ = 0; |
| break; |
| |
| case MotionEvent::Action::CANCEL: |
| Cancel(); |
| break; |
| } |
| |
| return handled; |
| } |
| |
| void GestureDetector::SetDoubleTapListener( |
| DoubleTapListener* double_tap_listener) { |
| if (double_tap_listener == double_tap_listener_) |
| return; |
| |
| DCHECK(!is_double_tapping_); |
| |
| // Null'ing the double-tap listener should flush an active tap timeout. |
| if (!double_tap_listener) { |
| if (timeout_handler_->HasTimeout(TAP)) { |
| timeout_handler_->StopTimeout(TAP); |
| OnTapTimeout(); |
| } |
| } |
| |
| double_tap_listener_ = double_tap_listener; |
| } |
| |
| void GestureDetector::Init(const Config& config) { |
| DCHECK(listener_); |
| |
| // Using a small epsilon when comparing slop distances allows pixel |
| // perfect slop determination when using fractional DIP coordinates |
| // (assuming the slop region and DPI scale are reasonably |
| // proportioned). |
| const float kSlopEpsilon = .05f; |
| |
| const float stylus_slop = config.stylus_slop + kSlopEpsilon; |
| const float touch_slop = config.touch_slop + kSlopEpsilon; |
| const float double_tap_touch_slop = touch_slop; |
| const float double_tap_slop = config.double_tap_slop + kSlopEpsilon; |
| stylus_slop_square_ = stylus_slop * stylus_slop; |
| touch_slop_square_ = touch_slop * touch_slop; |
| double_tap_touch_slop_square_ = double_tap_touch_slop * double_tap_touch_slop; |
| double_tap_slop_square_ = double_tap_slop * double_tap_slop; |
| double_tap_timeout_ = config.double_tap_timeout; |
| double_tap_min_time_ = config.double_tap_min_time; |
| DCHECK(double_tap_min_time_ < double_tap_timeout_); |
| min_fling_velocity_ = config.minimum_fling_velocity; |
| max_fling_velocity_ = config.maximum_fling_velocity; |
| |
| swipe_enabled_ = config.swipe_enabled; |
| min_swipe_velocity_ = config.minimum_swipe_velocity; |
| DCHECK_GT(config.maximum_swipe_deviation_angle, 0); |
| DCHECK_LE(config.maximum_swipe_deviation_angle, 45); |
| const float maximum_swipe_deviation_angle = |
| std::clamp(config.maximum_swipe_deviation_angle, 0.001f, 45.0f); |
| min_swipe_direction_component_ratio_ = |
| 1.f / tan(base::DegToRad(maximum_swipe_deviation_angle)); |
| |
| two_finger_tap_enabled_ = config.two_finger_tap_enabled; |
| two_finger_tap_distance_square_ = config.two_finger_tap_max_separation * |
| config.two_finger_tap_max_separation; |
| two_finger_tap_timeout_ = config.two_finger_tap_timeout; |
| |
| DCHECK_GE(config.single_tap_repeat_interval, 1); |
| single_tap_repeat_interval_ = config.single_tap_repeat_interval; |
| stylus_button_accelerated_longpress_enabled_ = |
| config.stylus_button_accelerated_longpress_enabled; |
| deep_press_accelerated_longpress_enabled_ = |
| config.deep_press_accelerated_longpress_enabled; |
| } |
| |
| void GestureDetector::OnShowPressTimeout() { |
| listener_->OnShowPress(*current_down_event_); |
| } |
| |
| void GestureDetector::OnShortPressTimeout() { |
| ActivateShortPressGesture(*current_down_event_); |
| } |
| |
| void GestureDetector::OnLongPressTimeout() { |
| ActivateLongPressGesture(*current_down_event_); |
| } |
| |
| void GestureDetector::OnTapTimeout() { |
| if (!double_tap_listener_) |
| return; |
| if (!still_down_) { |
| CHECK(previous_up_event_); |
| double_tap_listener_->OnSingleTapConfirmed(*previous_up_event_); |
| } else { |
| defer_confirm_single_tap_ = true; |
| } |
| } |
| |
| void GestureDetector::ActivateShortPressGesture(const MotionEvent& ev) { |
| timeout_handler_->StopTimeout(SHORT_PRESS); |
| listener_->OnShortPress(ev); |
| } |
| |
| void GestureDetector::ActivateLongPressGesture(const MotionEvent& ev) { |
| timeout_handler_->Stop(); |
| defer_confirm_single_tap_ = false; |
| listener_->OnLongPress(ev); |
| } |
| |
| void GestureDetector::Cancel() { |
| // Stop waiting for a second tap and send a GESTURE_TAP_CANCEL to keep the |
| // gesture stream valid. |
| if (timeout_handler_->HasTimeout(TAP)) |
| listener_->OnTapCancel(*current_down_event_); |
| CancelTaps(); |
| velocity_tracker_.Clear(); |
| all_pointers_within_slop_regions_ = false; |
| still_down_ = false; |
| } |
| |
| void GestureDetector::CancelTaps() { |
| timeout_handler_->Stop(); |
| is_double_tapping_ = false; |
| always_in_bigger_tap_region_ = false; |
| defer_confirm_single_tap_ = false; |
| is_down_candidate_for_repeated_single_tap_ = false; |
| current_single_tap_repeat_count_ = 0; |
| } |
| |
| bool GestureDetector::IsRepeatedTap(const MotionEvent& first_down, |
| const MotionEvent& first_up, |
| const MotionEvent& second_down, |
| bool should_process_double_tap) const { |
| if (!always_in_bigger_tap_region_) |
| return false; |
| |
| const base::TimeDelta delta_time = |
| second_down.GetEventTime() - first_up.GetEventTime(); |
| if (delta_time > double_tap_timeout_) |
| return false; |
| |
| // Only use the min time when in double-tap detection mode. For repeated |
| // single taps the risk of accidental repeat detection (e.g., from fingernail |
| // interference) is minimal. |
| if (should_process_double_tap && double_tap_listener_ && |
| delta_time < double_tap_min_time_) { |
| return false; |
| } |
| |
| const float delta_x = first_down.GetX() - second_down.GetX(); |
| const float delta_y = first_down.GetY() - second_down.GetY(); |
| return (delta_x * delta_x + delta_y * delta_y < double_tap_slop_square_); |
| } |
| |
| bool GestureDetector::HandleSwipeIfNeeded(const MotionEvent& up, |
| float vx, |
| float vy) { |
| if (!swipe_enabled_ || (!vx && !vy)) |
| return false; |
| float vx_abs = std::abs(vx); |
| float vy_abs = std::abs(vy); |
| |
| if (vx_abs < min_swipe_velocity_) |
| vx_abs = vx = 0; |
| if (vy_abs < min_swipe_velocity_) |
| vy_abs = vy = 0; |
| |
| // Note that the ratio will be 0 if both velocites are below the min. |
| float ratio = vx_abs > vy_abs ? vx_abs / std::max(vy_abs, 0.001f) |
| : vy_abs / std::max(vx_abs, 0.001f); |
| |
| if (ratio < min_swipe_direction_component_ratio_) |
| return false; |
| |
| if (vx_abs > vy_abs) |
| vy = 0; |
| else |
| vx = 0; |
| return listener_->OnSwipe(*current_down_event_, up, vx, vy); |
| } |
| |
| bool GestureDetector::IsWithinSlopForTap(const MotionEvent& ev) { |
| // If there have been more than two down pointers in the touch sequence, |
| // tapping is not possible. Slop region check is not needed. |
| if (maximum_pointer_count_ > 2) |
| return false; |
| |
| for (size_t i = 0; i < ev.GetPointerCount(); i++) { |
| const int pointer_id = ev.GetPointerId(i); |
| const MotionEvent* source_pointer_down_event = GetSourcePointerDownEvent( |
| *current_down_event_.get(), secondary_pointer_down_event_.get(), |
| pointer_id); |
| |
| if (!source_pointer_down_event) |
| return false; |
| |
| int source_index = |
| source_pointer_down_event->FindPointerIndexOfId(pointer_id); |
| DCHECK_GE(source_index, 0); |
| if (source_index < 0) |
| return false; |
| |
| float dx = source_pointer_down_event->GetX(source_index) - ev.GetX(i); |
| float dy = source_pointer_down_event->GetY(source_index) - ev.GetY(i); |
| bool is_stylus_slop_effective = |
| base::FeatureList::IsEnabled(features::kStylusSpecificTapSlop) && |
| ev.GetToolType(i) == MotionEvent::ToolType::STYLUS; |
| float slop_square = |
| is_stylus_slop_effective ? stylus_slop_square_ : touch_slop_square_; |
| if (dx * dx + dy * dy > slop_square) { |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| const MotionEvent* GestureDetector::GetSourcePointerDownEvent( |
| const MotionEvent& current_down_event, |
| const MotionEvent* secondary_pointer_down_event, |
| const int pointer_id) { |
| if (current_down_event.GetPointerId(0) == pointer_id) |
| return ¤t_down_event; |
| |
| // Secondary pointer down event is sometimes missing (crbug.com/704426), the |
| // source pointer down event is not found in these cases. |
| // crbug.com/704426 is the only related bug report and we don't have any |
| // reliable repro of the bug. |
| if (!secondary_pointer_down_event) |
| return nullptr; |
| |
| for (size_t i = 0; i < secondary_pointer_down_event->GetPointerCount(); i++) { |
| if (secondary_pointer_down_event->GetPointerId(i) == pointer_id) |
| return secondary_pointer_down_event; |
| } |
| |
| return nullptr; |
| } |
| |
| } // namespace ui |