1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "ui/touch_selection/longpress_drag_selector.h"
7 #include "base/auto_reset.h"
8 #include "ui/events/gesture_detection/motion_event.h"
13 gfx::Vector2dF
SafeNormalize(const gfx::Vector2dF
& v
) {
14 return v
.IsZero() ? v
: ScaleVector2d(v
, 1.f
/ v
.Length());
19 LongPressDragSelector::LongPressDragSelector(
20 LongPressDragSelectorClient
* client
)
23 has_longpress_drag_start_anchor_(false) {
26 LongPressDragSelector::~LongPressDragSelector() {
29 bool LongPressDragSelector::WillHandleTouchEvent(const MotionEvent
& event
) {
30 switch (event
.GetAction()) {
31 case MotionEvent::ACTION_DOWN
:
32 touch_down_position_
.SetPoint(event
.GetX(), event
.GetY());
33 touch_down_time_
= event
.GetEventTime();
34 has_longpress_drag_start_anchor_
= false;
35 SetState(LONGPRESS_PENDING
);
38 case MotionEvent::ACTION_UP
:
39 case MotionEvent::ACTION_CANCEL
:
43 case MotionEvent::ACTION_MOVE
:
50 if (state_
!= DRAG_PENDING
&& state_
!= DRAGGING
)
53 gfx::PointF
position(event
.GetX(), event
.GetY());
54 if (state_
== DRAGGING
) {
55 gfx::PointF drag_position
= position
+ longpress_drag_selection_offset_
;
56 client_
->OnDragUpdate(*this, drag_position
);
60 // We can't use |touch_down_position_| as the offset anchor, as
61 // showing the selection UI may have shifted the motion coordinates.
62 if (!has_longpress_drag_start_anchor_
) {
63 has_longpress_drag_start_anchor_
= true;
64 longpress_drag_start_anchor_
= position
;
68 // Allow an additional slop affordance after the longpress occurs.
69 gfx::Vector2dF delta
= position
- longpress_drag_start_anchor_
;
70 if (client_
->IsWithinTapSlop(delta
))
73 gfx::PointF selection_start
= client_
->GetSelectionStart();
74 gfx::PointF selection_end
= client_
->GetSelectionEnd();
75 bool extend_selection_start
= false;
76 if (std::abs(delta
.y()) > std::abs(delta
.x())) {
77 // If initial motion is up/down, extend the start/end selection bound.
78 extend_selection_start
= delta
.y() < 0;
80 // Otherwise extend the selection bound toward which we're moving, or
81 // the closest bound if motion is already away from both bounds.
82 // Note that, for mixed RTL text, or for multiline selections triggered
83 // by longpress, this may not pick the most suitable drag target
84 gfx::Vector2dF start_delta
= selection_start
- longpress_drag_start_anchor_
;
85 gfx::Vector2dF end_delta
= selection_end
- longpress_drag_start_anchor_
;
87 // The vectors must be normalized to make dot product comparison meaningful.
88 gfx::Vector2dF normalized_start_delta
= SafeNormalize(start_delta
);
89 gfx::Vector2dF normalized_end_delta
= SafeNormalize(end_delta
);
90 double start_dot_product
= gfx::DotProduct(normalized_start_delta
, delta
);
91 double end_dot_product
= gfx::DotProduct(normalized_end_delta
, delta
);
93 if (start_dot_product
>= 0 || end_dot_product
>= 0) {
94 // The greater the dot product the more similar the direction.
95 extend_selection_start
= start_dot_product
> end_dot_product
;
97 // If we're already moving away from both endpoints, pick the closest.
98 extend_selection_start
=
99 start_delta
.LengthSquared() < end_delta
.LengthSquared();
103 gfx::PointF extent
= extend_selection_start
? selection_start
: selection_end
;
104 longpress_drag_selection_offset_
= extent
- position
;
105 client_
->OnDragBegin(*this, extent
);
110 bool LongPressDragSelector::IsActive() const {
111 return state_
!= INACTIVE
&& state_
!= LONGPRESS_PENDING
;
114 void LongPressDragSelector::OnLongPressEvent(base::TimeTicks event_time
,
115 const gfx::PointF
& position
) {
116 // We have no guarantees that the current gesture stream is aligned with the
117 // observed touch stream. We only know that the gesture sequence is downstream
118 // from the touch sequence. Using a time/distance heuristic helps ensure that
119 // the observed longpress corresponds to the active touch sequence.
120 if (state_
== LONGPRESS_PENDING
&&
121 // Ensure the down event occurs *before* the longpress event. Use a
122 // small time epsilon to account for floating point time conversion.
123 (touch_down_time_
< event_time
+ base::TimeDelta::FromMicroseconds(10)) &&
124 client_
->IsWithinTapSlop(touch_down_position_
- position
)) {
125 SetState(SELECTION_PENDING
);
129 void LongPressDragSelector::OnSelectionActivated() {
130 if (state_
== SELECTION_PENDING
)
131 SetState(DRAG_PENDING
);
134 void LongPressDragSelector::OnSelectionDeactivated() {
138 void LongPressDragSelector::SetState(SelectionState state
) {
142 const bool was_dragging
= state_
== DRAGGING
;
143 const bool was_active
= IsActive();
146 // TODO(jdduke): Add UMA for tracking relative longpress drag frequency.
148 client_
->OnDragEnd(*this);
150 if (was_active
!= IsActive())
151 client_
->OnLongPressDragActiveStateChanged();