Chromium Code Reviews| Index: ui/events/gesture_detection/gesture_detector.cc |
| diff --git a/ui/events/gesture_detection/gesture_detector.cc b/ui/events/gesture_detection/gesture_detector.cc |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..79c1a92b539b2ca623da50f6ec236f55b95d1564 |
| --- /dev/null |
| +++ b/ui/events/gesture_detection/gesture_detector.cc |
| @@ -0,0 +1,394 @@ |
| +// 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_detector.h" |
| + |
| +#include "base/timer/timer.h" |
| +#include "ui/events/gesture_detection/motion_event.h" |
| + |
| +namespace ui { |
| +namespace { |
| + |
| +// Constants used by TimeoutGestureHandler. |
| +enum TimeoutEvent { |
| + SHOW_PRESS = 0, |
| + LONG_PRESS, |
| + TAP, |
| + TIMEOUT_EVENT_COUNT |
| +}; |
| + |
| +} // namespace |
| + |
| +GestureDetector::Config::Config() |
|
Sami
2014/02/20 14:59:05
Could you add a comment saying which device these
jdduke (slow)
2014/02/20 18:01:09
Done.
|
| + : longpress_timeout(base::TimeDelta::FromMilliseconds(500)), |
| + tap_timeout(base::TimeDelta::FromMilliseconds(180)), |
| + double_tap_timeout(base::TimeDelta::FromMilliseconds(300)), |
| + scaled_touch_slop(8), |
| + scaled_double_tap_slop(100), |
| + scaled_minimum_fling_velocity(50), |
| + scaled_maximum_fling_velocity(8000) {} |
| + |
| +GestureDetector::Config::~Config() {} |
| + |
| +bool GestureDetector::SimpleOnGestureListener::OnDown(const MotionEvent& e) { |
| + return false; |
| +} |
| + |
| +void GestureDetector::SimpleOnGestureListener::OnShowPress( |
| + const MotionEvent& e) {} |
| + |
| +bool GestureDetector::SimpleOnGestureListener::OnSingleTapUp( |
| + const MotionEvent& e) { |
| + return false; |
| +} |
| + |
| +bool GestureDetector::SimpleOnGestureListener::OnLongPress( |
| + const MotionEvent& e) { |
| + return false; |
| +} |
| + |
| +bool GestureDetector::SimpleOnGestureListener::OnScroll(const MotionEvent& e1, |
| + const MotionEvent& e2, |
| + float distance_x, |
| + float distance_y) { |
| + return false; |
| +} |
| + |
| +bool GestureDetector::SimpleOnGestureListener::OnFling(const MotionEvent& e1, |
| + const MotionEvent& e2, |
| + float velocity_x, |
| + float velocity_y) { |
| + return false; |
| +} |
| + |
| +bool GestureDetector::SimpleOnGestureListener::OnSingleTapConfirmed( |
| + const MotionEvent& e) { |
| + return false; |
| +} |
| + |
| +bool GestureDetector::SimpleOnGestureListener::OnDoubleTap( |
| + const MotionEvent& e) { |
| + return false; |
| +} |
| + |
| +bool GestureDetector::SimpleOnGestureListener::OnDoubleTapEvent( |
| + const MotionEvent& e) { |
| + return false; |
| +} |
| + |
| +class GestureDetector::TimeoutGestureHandler { |
| + public: |
| + typedef void (GestureDetector::*ReceiverMethod)(); |
|
Sami
2014/02/20 14:59:05
This could be private, right?
jdduke (slow)
2014/02/20 18:01:09
Done.
|
| + |
| + TimeoutGestureHandler(const Config& config, GestureDetector* gesture_detector) |
| + : gesture_detector_(gesture_detector) { |
| + timeout_callbacks_[SHOW_PRESS] = &GestureDetector::OnShowPressTimeout; |
| + timeout_delays_[SHOW_PRESS] = config.tap_timeout; |
| + |
| + timeout_callbacks_[LONG_PRESS] = &GestureDetector::OnLongPressTimeout; |
| + timeout_delays_[LONG_PRESS] = config.longpress_timeout + config.tap_timeout; |
| + |
| + timeout_callbacks_[TAP] = &GestureDetector::OnTapTimeout; |
| + timeout_delays_[TAP] = config.double_tap_timeout; |
| + } |
| + |
|
Sami
2014/02/20 14:59:05
Should we have a destructor that calls Stop() auto
jdduke (slow)
2014/02/20 18:01:09
I believe the |timeout_timer_[i]| destructor will
|
| + void StartTimeout(TimeoutEvent event) { |
| + timeout_timers_[event].Start(FROM_HERE, |
| + timeout_delays_[event], |
| + gesture_detector_, |
| + 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: |
| + GestureDetector* const gesture_detector_; |
| + base::OneShotTimer<GestureDetector> timeout_timers_[TIMEOUT_EVENT_COUNT]; |
| + ReceiverMethod timeout_callbacks_[TIMEOUT_EVENT_COUNT]; |
| + base::TimeDelta timeout_delays_[TIMEOUT_EVENT_COUNT]; |
| +}; |
| + |
| +GestureDetector::GestureDetector( |
| + const Config& config, |
| + OnGestureListener* listener, |
| + OnDoubleTapListener* optional_double_tap_listener) |
| + : timeout_handler_(new TimeoutGestureHandler(config, this)), |
| + listener_(listener), |
| + double_tap_listener_(optional_double_tap_listener), |
| + touch_slop_square_(0), |
| + double_tap_touch_slop_square_(0), |
| + double_tap_slop_square_(0), |
| + min_fling_velocity_(1), |
| + max_fling_velocity_(1), |
| + still_down_(false), |
| + defer_confirm_single_tap_(false), |
| + in_long_press_(false), |
| + always_in_tap_region_(false), |
| + always_in_bigger_tap_region_(false), |
| + is_double_tapping_(false), |
| + last_focus_x_(0), |
| + last_focus_y_(0), |
| + down_focus_x_(0), |
| + down_focus_y_(0), |
| + is_longpress_enabled_(true) { |
| + DCHECK(listener_); |
| + Init(config); |
| +} |
| + |
| +GestureDetector::~GestureDetector() {} |
| + |
| +bool GestureDetector::OnTouchEvent(const MotionEvent& ev) { |
| + 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 |
|
Sami
2014/02/20 14:59:05
nit: Comments should end with a period.
jdduke (slow)
2014/02/20 18:01:09
Done.
|
| + float sum_x = 0, sum_y = 0; |
| + const int count = 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_POINTER_DOWN: |
| + down_focus_x_ = last_focus_x_ = focus_x; |
| + down_focus_y_ = last_focus_y_ = focus_y; |
| + // Cancel long press and taps |
|
Sami
2014/02/20 14:59:05
Ditto.
jdduke (slow)
2014/02/20 18:01:09
Done.
|
| + CancelTaps(); |
| + 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 upIndex = ev.GetActionIndex(); |
|
Sami
2014/02/20 14:59:05
up_index
jdduke (slow)
2014/02/20 18:01:09
Done.
|
| + const int id1 = ev.GetPointerId(upIndex); |
| + const float x1 = velocity_tracker_.GetXVelocity(id1); |
| + const float y1 = velocity_tracker_.GetYVelocity(id1); |
| + for (int i = 0; i < count; i++) { |
| + if (i == upIndex) |
| + continue; |
| + |
| + const int id2 = ev.GetPointerId(i); |
| + const float x = x1 * velocity_tracker_.GetXVelocity(id2); |
| + const float y = y1 * velocity_tracker_.GetYVelocity(id2); |
| + |
| + const float dot = x + y; |
| + if (dot < 0) { |
| + velocity_tracker_.Clear(); |
| + break; |
| + } |
| + } |
| + } |
| + break; |
| + |
| + case MotionEvent::ACTION_DOWN: |
| + if (double_tap_listener_) { |
| + bool hadTapMessage = timeout_handler_->HasTimeout(TAP); |
|
Sami
2014/02/20 14:59:05
had_tap_message
jdduke (slow)
2014/02/20 18:01:09
Done.
|
| + if (hadTapMessage) |
| + timeout_handler_->StopTimeout(TAP); |
| + if (current_down_event_ && previous_up_event_ && hadTapMessage && |
| + IsConsideredDoubleTap( |
| + *current_down_event_, *previous_up_event_, ev)) { |
| + // This is a second tap |
|
Sami
2014/02/20 14:59:05
Add '.'
jdduke (slow)
2014/02/20 18:01:09
Done.
|
| + is_double_tapping_ = true; |
| + // Give a callback with the first tap of the double-tap |
|
Sami
2014/02/20 14:59:05
Ditto.
jdduke (slow)
2014/02/20 18:01:09
Done.
|
| + handled |= double_tap_listener_->OnDoubleTap(*current_down_event_); |
| + // Give a callback with down event of the double-tap |
|
Sami
2014/02/20 14:59:05
Ditto.
jdduke (slow)
2014/02/20 18:01:09
Done.
|
| + handled |= double_tap_listener_->OnDoubleTapEvent(ev); |
| + } else { |
| + // This is a first tap |
|
Sami
2014/02/20 14:59:05
Ditto.
jdduke (slow)
2014/02/20 18:01:09
Done.
|
| + timeout_handler_->StartTimeout(TAP); |
| + } |
| + } |
| + |
| + down_focus_x_ = last_focus_x_ = focus_x; |
| + down_focus_y_ = last_focus_y_ = focus_y; |
| + current_down_event_ = ev.Clone(); |
| + |
| + always_in_tap_region_ = true; |
| + always_in_bigger_tap_region_ = true; |
| + still_down_ = true; |
| + in_long_press_ = false; |
| + defer_confirm_single_tap_ = false; |
| + |
| + if (is_longpress_enabled_) |
| + timeout_handler_->StartTimeout(LONG_PRESS); |
| + timeout_handler_->StartTimeout(SHOW_PRESS); |
| + handled |= listener_->OnDown(ev); |
| + break; |
| + |
| + case MotionEvent::ACTION_MOVE: |
| + if (in_long_press_) |
| + break; |
| + |
| + { |
| + 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 |
|
Sami
2014/02/20 14:59:05
Add '.'
jdduke (slow)
2014/02/20 18:01:09
Done.
|
| + handled |= double_tap_listener_->OnDoubleTapEvent(ev); |
| + } else if (always_in_tap_region_) { |
| + const int delta_x = (int)(focus_x - down_focus_x_); |
|
Sami
2014/02/20 14:59:05
static_cast<int> instead of C-casts.
jdduke (slow)
2014/02/20 18:01:09
Done.
|
| + const int delta_y = (int)(focus_y - down_focus_y_); |
| + int distance = (delta_x * delta_x) + (delta_y * delta_y); |
| + if (distance > touch_slop_square_) { |
| + handled = listener_->OnScroll( |
| + *current_down_event_, ev, scroll_x, scroll_y); |
| + last_focus_x_ = focus_x; |
| + last_focus_y_ = focus_y; |
| + always_in_tap_region_ = false; |
| + timeout_handler_->Stop(); |
| + } |
| + if (distance > double_tap_touch_slop_square_) |
| + always_in_bigger_tap_region_ = false; |
| + } else if ((std::abs(scroll_x) >= 1) || (std::abs(scroll_y) >= 1)) { |
| + handled = |
| + listener_->OnScroll(*current_down_event_, ev, scroll_x, scroll_y); |
| + last_focus_x_ = focus_x; |
| + last_focus_y_ = focus_y; |
| + } |
| + } |
| + break; |
| + |
| + case MotionEvent::ACTION_UP: |
| + still_down_ = false; |
| + { |
| + if (is_double_tapping_) { |
| + // Finally, give the up event of the double-tap |
|
Sami
2014/02/20 14:59:05
Add '.'
jdduke (slow)
2014/02/20 18:01:09
Done.
|
| + handled |= double_tap_listener_->OnDoubleTapEvent(ev); |
| + } else if (in_long_press_) { |
| + timeout_handler_->StopTimeout(TAP); |
| + in_long_press_ = false; |
| + } else if (always_in_tap_region_) { |
| + handled = listener_->OnSingleTapUp(ev); |
| + if (defer_confirm_single_tap_ && double_tap_listener_ != NULL) { |
| + double_tap_listener_->OnSingleTapConfirmed(ev); |
| + } |
| + } else { |
| + |
| + // A fling must travel the minimum tap distance |
|
Sami
2014/02/20 14:59:05
Add '.'
jdduke (slow)
2014/02/20 18:01:09
Done.
|
| + 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); |
| + } |
| + } |
| + |
| + 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(LONG_PRESS); |
| + } |
| + break; |
| + |
| + case MotionEvent::ACTION_CANCEL: |
| + Cancel(); |
| + break; |
| + } |
| + |
| + return handled; |
| +} |
| + |
| +void GestureDetector::Init(Config config) { |
| + DCHECK(listener_); |
| + |
| + const int touch_slop = config.scaled_touch_slop; |
| + const int double_tap_touch_slop = touch_slop; |
| + const int double_tap_slop = config.scaled_double_tap_slop; |
| + min_fling_velocity_ = config.scaled_minimum_fling_velocity; |
| + max_fling_velocity_ = config.scaled_maximum_fling_velocity; |
| + 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; |
| +} |
| + |
| +void GestureDetector::OnShowPressTimeout() { |
| + listener_->OnShowPress(*current_down_event_); |
| +} |
| + |
| +void GestureDetector::OnLongPressTimeout() { |
| + timeout_handler_->StopTimeout(TAP); |
| + defer_confirm_single_tap_ = false; |
| + in_long_press_ = listener_->OnLongPress(*current_down_event_); |
| +} |
| + |
| +void GestureDetector::OnTapTimeout() { |
| + if (!double_tap_listener_) |
| + return; |
| + if (!still_down_) |
| + double_tap_listener_->OnSingleTapConfirmed(*current_down_event_); |
| + else |
| + defer_confirm_single_tap_ = true; |
| +} |
| + |
| +void GestureDetector::Cancel() { |
| + timeout_handler_->Stop(); |
| + velocity_tracker_.Clear(); |
| + is_double_tapping_ = false; |
| + still_down_ = false; |
| + always_in_tap_region_ = false; |
| + always_in_bigger_tap_region_ = false; |
| + defer_confirm_single_tap_ = false; |
| + in_long_press_ = false; |
| +} |
| + |
| +void GestureDetector::CancelTaps() { |
| + timeout_handler_->Stop(); |
| + is_double_tapping_ = false; |
| + always_in_tap_region_ = false; |
| + always_in_bigger_tap_region_ = false; |
| + defer_confirm_single_tap_ = false; |
| + in_long_press_ = false; |
| +} |
| + |
| +bool GestureDetector::IsConsideredDoubleTap(const MotionEvent& first_down, |
| + const MotionEvent& first_up, |
| + const MotionEvent& second_down) { |
| + if (!always_in_bigger_tap_region_) |
| + return false; |
| + |
| + if (second_down.GetEventTime() - first_up.GetEventTime() > |
| + double_tap_timeout_) |
| + return false; |
| + |
| + int delta_x = (int)first_down.GetX() - (int)second_down.GetX(); |
|
Sami
2014/02/20 14:59:05
Use static_cast<int> instead of C-style casts.
jdduke (slow)
2014/02/20 18:01:09
Done.
|
| + int delta_y = (int)first_down.GetY() - (int)second_down.GetY(); |
| + return (delta_x * delta_x + delta_y * delta_y < double_tap_slop_square_); |
| +} |
| + |
| +} // namespace ui |