| // Copyright 2018 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 "chrome/browser/vr/gesture_detector.h" |
| |
| #include "base/numerics/math_constants.h" |
| #include "chrome/browser/vr/input_event.h" |
| #include "chrome/browser/vr/platform_controller.h" |
| |
| namespace vr { |
| |
| namespace { |
| |
| constexpr float kDisplacementScaleFactor = 129.0f; |
| |
| constexpr int kMaxNumOfExtrapolations = 2; |
| |
| // Minimum time distance needed to call two timestamps |
| // not equal. |
| constexpr float kDelta = 1.0e-7f; |
| |
| constexpr float kCutoffHz = 10.0f; |
| constexpr float kRC = 1.0f / (2.0f * base::kPiFloat * kCutoffHz); |
| |
| // A slop represents a small rectangular region around the first touch point of |
| // a gesture. |
| // If the user does not move outside of the slop, no gesture is detected. |
| // Gestures start to be detected when the user moves outside of the slop. |
| // Vertical distance from the border to the center of slop. |
| constexpr float kSlopVertical = 0.165f; |
| |
| // Horizontal distance from the border to the center of slop. |
| constexpr float kSlopHorizontal = 0.15f; |
| |
| // Exceeding pressing the appbutton for longer than this threshold will result |
| // in a long press. |
| constexpr base::TimeDelta kLongPressThreshold = |
| base::TimeDelta::FromMilliseconds(900); |
| |
| struct TouchPoint { |
| gfx::Vector2dF position; |
| base::TimeTicks timestamp; |
| }; |
| |
| } // namespace |
| |
| GestureDetector::GestureDetector() { |
| Reset(); |
| } |
| GestureDetector::~GestureDetector() = default; |
| |
| InputEventList GestureDetector::DetectGestures( |
| const PlatformController& controller, |
| base::TimeTicks current_timestamp) { |
| touch_position_changed_ = UpdateCurrentTouchPoint(controller); |
| TouchPoint touch_point{.position = controller.GetPositionInTrackpad(), |
| .timestamp = controller.GetLastTouchTimestamp()}; |
| ExtrapolateTouchPoint(&touch_point, current_timestamp); |
| if (touch_position_changed_) |
| UpdateOverallVelocity(touch_point); |
| is_select_button_pressed_ = |
| controller.IsButtonDown(PlatformController::kButtonSelect); |
| last_touching_state_ = is_touching_trackpad_; |
| is_touching_trackpad_ = controller.IsTouchingTrackpad(); |
| |
| InputEventList gesture_list; |
| DetectMenuButtonGestures(&gesture_list, controller, current_timestamp); |
| auto gesture = GetGestureFromTouchInfo(touch_point); |
| |
| if (!gesture) |
| return gesture_list; |
| |
| if (gesture->type() == InputEvent::kScrollEnd) |
| Reset(); |
| |
| if (gesture->type() != InputEvent::kTypeUndefined) |
| gesture_list.push_back(std::move(gesture)); |
| |
| return gesture_list; |
| } |
| |
| void GestureDetector::DetectMenuButtonGestures( |
| InputEventList* event_list, |
| const PlatformController& controller, |
| base::TimeTicks current_timestamp) { |
| std::unique_ptr<InputEvent> event; |
| if (controller.ButtonDownHappened(PlatformController::kButtonMenu)) { |
| menu_button_down_timestamp_ = current_timestamp; |
| menu_button_long_pressed_ = false; |
| } |
| if (controller.ButtonUpHappened(PlatformController::kButtonMenu)) { |
| event = std::make_unique<InputEvent>( |
| menu_button_long_pressed_ ? InputEvent::kMenuButtonLongPressEnd |
| : InputEvent::kMenuButtonClicked); |
| } |
| if (!menu_button_long_pressed_ && |
| controller.IsButtonDown(PlatformController::kButtonMenu) && |
| current_timestamp - menu_button_down_timestamp_ > kLongPressThreshold) { |
| menu_button_long_pressed_ = true; |
| event = std::make_unique<InputEvent>(InputEvent::kMenuButtonLongPressStart); |
| } |
| if (event) { |
| event->set_time_stamp(current_timestamp); |
| event_list->push_back(std::move(event)); |
| } |
| } |
| |
| std::unique_ptr<InputEvent> GestureDetector::GetGestureFromTouchInfo( |
| const TouchPoint& touch_point) { |
| std::unique_ptr<InputEvent> gesture; |
| |
| switch (state_->label) { |
| // User has not put finger on touch pad. |
| case WAITING: |
| gesture = HandleWaitingState(touch_point); |
| break; |
| // User has not started a gesture (by moving out of slop). |
| case TOUCHING: |
| gesture = HandleDetectingState(touch_point); |
| break; |
| // User is scrolling on touchpad |
| case SCROLLING: |
| gesture = HandleScrollingState(touch_point); |
| break; |
| // The user has finished scrolling, but we'll hallucinate a few points |
| // before really finishing. |
| case POST_SCROLL: |
| gesture = HandlePostScrollingState(touch_point); |
| break; |
| default: |
| NOTREACHED(); |
| break; |
| } |
| |
| if (gesture) |
| gesture->set_time_stamp(touch_point.timestamp); |
| |
| return gesture; |
| } |
| |
| std::unique_ptr<InputEvent> GestureDetector::HandleWaitingState( |
| const TouchPoint& touch_point) { |
| // User puts finger on touch pad (or when the touch down for current gesture |
| // is missed, initiate gesture from current touch point). |
| if (is_touching_trackpad_) { |
| // update initial touchpoint |
| state_->initial_touch_point = touch_point; |
| // update current touchpoint |
| state_->cur_touch_point = touch_point; |
| state_->label = TOUCHING; |
| |
| return std::make_unique<InputEvent>(InputEvent::kFlingCancel); |
| } |
| return nullptr; |
| } |
| |
| std::unique_ptr<InputEvent> GestureDetector::HandleDetectingState( |
| const TouchPoint& touch_point) { |
| // User lifts up finger from touch pad. |
| if (!is_touching_trackpad_) { |
| Reset(); |
| return nullptr; |
| } |
| |
| // Touch position is changed, the touch point moves outside of slop, |
| // and the Controller's button is not down. |
| if (touch_position_changed_ && is_touching_trackpad_ && |
| !InSlop(touch_point.position) && !is_select_button_pressed_) { |
| state_->label = SCROLLING; |
| auto gesture = std::make_unique<InputEvent>(InputEvent::kScrollBegin); |
| UpdateGestureParameters(touch_point); |
| UpdateGestureWithScrollDelta(gesture.get()); |
| return gesture; |
| } |
| return nullptr; |
| } |
| |
| std::unique_ptr<InputEvent> GestureDetector::HandleScrollingState( |
| const TouchPoint& touch_point) { |
| if (is_select_button_pressed_) { |
| UpdateGestureParameters(touch_point); |
| return std::make_unique<InputEvent>(InputEvent::kScrollEnd); |
| } |
| if (!is_touching_trackpad_) |
| state_->label = POST_SCROLL; |
| if (touch_position_changed_) { |
| auto gesture = std::make_unique<InputEvent>(InputEvent::kScrollUpdate); |
| UpdateGestureParameters(touch_point); |
| UpdateGestureWithScrollDelta(gesture.get()); |
| return gesture; |
| } |
| return nullptr; |
| } |
| |
| std::unique_ptr<InputEvent> GestureDetector::HandlePostScrollingState( |
| const TouchPoint& touch_point) { |
| if (extrapolated_touch_ == 0 || is_select_button_pressed_) { |
| UpdateGestureParameters(touch_point); |
| return std::make_unique<InputEvent>(InputEvent::kScrollEnd); |
| } else { |
| auto gesture = std::make_unique<InputEvent>(InputEvent::kScrollUpdate); |
| UpdateGestureParameters(touch_point); |
| UpdateGestureWithScrollDelta(gesture.get()); |
| return gesture; |
| } |
| } |
| |
| void GestureDetector::UpdateGestureWithScrollDelta(InputEvent* gesture) { |
| gesture->scroll_data.delta_x = |
| state_->displacement.x() * kDisplacementScaleFactor; |
| gesture->scroll_data.delta_y = |
| state_->displacement.y() * kDisplacementScaleFactor; |
| } |
| |
| bool GestureDetector::UpdateCurrentTouchPoint( |
| const PlatformController& controller) { |
| if (controller.IsTouchingTrackpad() || last_touching_state_) { |
| // Update the touch point when the touch position has changed. |
| if (state_->cur_touch_point.position != |
| controller.GetPositionInTrackpad()) { |
| state_->prev_touch_point = state_->cur_touch_point; |
| state_->cur_touch_point = { |
| .position = controller.GetPositionInTrackpad(), |
| .timestamp = controller.GetLastTouchTimestamp()}; |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| void GestureDetector::ExtrapolateTouchPoint(TouchPoint* touch_point, |
| base::TimeTicks current_timestamp) { |
| const bool effectively_scrolling = |
| state_->label == SCROLLING || state_->label == POST_SCROLL; |
| if (effectively_scrolling && extrapolated_touch_ < kMaxNumOfExtrapolations && |
| (touch_point->timestamp == last_touch_timestamp_ || |
| touch_point->position == state_->prev_touch_point.position)) { |
| extrapolated_touch_++; |
| touch_position_changed_ = true; |
| float duration = (current_timestamp - last_timestamp_).InSecondsF(); |
| touch_point->position.set_x(state_->cur_touch_point.position.x() + |
| state_->overall_velocity.x() * duration); |
| touch_point->position.set_y(state_->cur_touch_point.position.y() + |
| state_->overall_velocity.y() * duration); |
| } else { |
| if (extrapolated_touch_ == kMaxNumOfExtrapolations) { |
| state_->overall_velocity = {0, 0}; |
| } |
| extrapolated_touch_ = 0; |
| } |
| last_touch_timestamp_ = touch_point->timestamp; |
| last_timestamp_ = current_timestamp; |
| } |
| |
| void GestureDetector::UpdateOverallVelocity(const TouchPoint& touch_point) { |
| float duration = |
| (touch_point.timestamp - state_->prev_touch_point.timestamp).InSecondsF(); |
| // If the timestamp does not change, do not update velocity. |
| if (duration < kDelta) |
| return; |
| |
| const gfx::Vector2dF& displacement = |
| touch_point.position - state_->prev_touch_point.position; |
| |
| const gfx::Vector2dF& velocity = ScaleVector2d(displacement, (1 / duration)); |
| |
| float weight = duration / (kRC + duration); |
| |
| state_->overall_velocity = |
| ScaleVector2d(state_->overall_velocity, (1 - weight)) + |
| ScaleVector2d(velocity, weight); |
| } |
| |
| void GestureDetector::UpdateGestureParameters(const TouchPoint& touch_point) { |
| state_->displacement = |
| touch_point.position - state_->prev_touch_point.position; |
| } |
| |
| bool GestureDetector::InSlop(const gfx::PointF touch_position) const { |
| return (std::abs(touch_position.x() - |
| state_->initial_touch_point.position.x()) < |
| kSlopHorizontal) && |
| (std::abs(touch_position.y() - |
| state_->initial_touch_point.position.y()) < kSlopVertical); |
| } |
| |
| void GestureDetector::Reset() { |
| state_ = std::make_unique<GestureDetectorState>(); |
| } |
| |
| } // namespace vr |