Skip to content

Commit

Permalink
Support longpress drag selection
Browse files Browse the repository at this point in the history
Allow users to extend a longpress-triggered selection by dragging their
finger continuously after the longpress. Enabled using the
"--enable-longpress-drag-selection" flag on Android.

BUG=466749

Review URL: https://codereview.chromium.org/1087893003

Cr-Commit-Position: refs/heads/master@{#332439}
  • Loading branch information
jdduke authored and Commit bot committed Jun 2, 2015
1 parent d6d92d4 commit 3f2a3ab
Show file tree
Hide file tree
Showing 17 changed files with 1,026 additions and 140 deletions.
44 changes: 29 additions & 15 deletions content/browser/renderer_host/render_widget_host_view_android.cc
Original file line number Diff line number Diff line change
Expand Up @@ -268,14 +268,16 @@ scoped_ptr<ui::TouchSelectionController> CreateSelectionController(
ContentViewCore* content_view_core) {
DCHECK(client);
DCHECK(content_view_core);
int tap_timeout_ms = gfx::ViewConfiguration::GetTapTimeoutInMs();
int touch_slop_pixels = gfx::ViewConfiguration::GetTouchSlopInPixels();
bool show_on_tap_for_empty_editable = false;
return make_scoped_ptr(new ui::TouchSelectionController(
client,
base::TimeDelta::FromMilliseconds(tap_timeout_ms),
touch_slop_pixels / content_view_core->GetDpiScale(),
show_on_tap_for_empty_editable));
ui::TouchSelectionController::Config config;
config.tap_timeout = base::TimeDelta::FromMilliseconds(
gfx::ViewConfiguration::GetTapTimeoutInMs());
config.tap_slop = gfx::ViewConfiguration::GetTouchSlopInPixels() /
content_view_core->GetDpiScale();
config.show_on_tap_for_empty_editable = false;
config.enable_longpress_drag_selection =
base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kEnableLongpressDragSelection);
return make_scoped_ptr(new ui::TouchSelectionController(client, config));
}

scoped_ptr<OverscrollControllerAndroid> CreateOverscrollController(
Expand Down Expand Up @@ -1584,13 +1586,25 @@ InputEventAckState RenderWidgetHostViewAndroid::FilterInputEvent(
blink::WebInputEvent::isGestureEventType(input_event.type)) {
const blink::WebGestureEvent& gesture_event =
static_cast<const blink::WebGestureEvent&>(input_event);
gfx::PointF gesture_location(gesture_event.x, gesture_event.y);
if (input_event.type == blink::WebInputEvent::GestureLongPress) {
if (selection_controller_->WillHandleLongPressEvent(gesture_location))
return INPUT_EVENT_ACK_STATE_CONSUMED;
} else if (input_event.type == blink::WebInputEvent::GestureTap) {
if (selection_controller_->WillHandleTapEvent(gesture_location))
return INPUT_EVENT_ACK_STATE_CONSUMED;
switch (gesture_event.type) {
case blink::WebInputEvent::GestureLongPress:
if (selection_controller_->WillHandleLongPressEvent(
base::TimeTicks() +
base::TimeDelta::FromSecondsD(input_event.timeStampSeconds),
gfx::PointF(gesture_event.x, gesture_event.y))) {
return INPUT_EVENT_ACK_STATE_CONSUMED;
}
break;

case blink::WebInputEvent::GestureTap:
if (selection_controller_->WillHandleTapEvent(
gfx::PointF(gesture_event.x, gesture_event.y))) {
return INPUT_EVENT_ACK_STATE_CONSUMED;
}
break;

default:
break;
}
}

Expand Down
3 changes: 3 additions & 0 deletions content/public/common/content_switches.cc
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,9 @@ const char kDisableWebRTC[] = "disable-webrtc";
const char kEnableAndroidCompositorAnimationTimelines[] =
"enable-android-compositor-animation-timelines";

// Enable drag manipulation of longpress-triggered text selections.
const char kEnableLongpressDragSelection[] = "enable-longpress-drag-selection";

// The telephony region (ISO country code) to use in phone number detection.
const char kNetworkCountryIso[] = "network-country-iso";

Expand Down
1 change: 1 addition & 0 deletions content/public/common/content_switches.h
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ CONTENT_EXPORT extern const char kDisablePullToRefreshEffect[];
CONTENT_EXPORT extern const char kDisableScreenOrientationLock[];
CONTENT_EXPORT extern const char kDisableWebRTC[];
CONTENT_EXPORT extern const char kEnableAndroidCompositorAnimationTimelines[];
CONTENT_EXPORT extern const char kEnableLongpressDragSelection[];
CONTENT_EXPORT extern const char kHideScrollbars[];
extern const char kNetworkCountryIso[];
CONTENT_EXPORT extern const char kRemoteDebuggingSocketName[];
Expand Down
26 changes: 18 additions & 8 deletions ui/events/test/motion_event_test_utils.cc
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ MockMotionEvent::MockMotionEvent(const MockMotionEvent& other)
MockMotionEvent::~MockMotionEvent() {
}

void MockMotionEvent::PressPoint(float x, float y) {
MockMotionEvent& MockMotionEvent::PressPoint(float x, float y) {
UpdatePointersAndID();
PushPointer(x, y);
if (GetPointerCount() > 1) {
Expand All @@ -101,9 +101,10 @@ void MockMotionEvent::PressPoint(float x, float y) {
} else {
set_action(ACTION_DOWN);
}
return *this;
}

void MockMotionEvent::MovePoint(size_t index, float x, float y) {
MockMotionEvent& MockMotionEvent::MovePoint(size_t index, float x, float y) {
UpdatePointersAndID();
DCHECK_LT(index, GetPointerCount());
PointerProperties& p = pointer(index);
Expand All @@ -114,9 +115,10 @@ void MockMotionEvent::MovePoint(size_t index, float x, float y) {
p.raw_x += dx;
p.raw_y += dy;
set_action(ACTION_MOVE);
return *this;
}

void MockMotionEvent::ReleasePoint() {
MockMotionEvent& MockMotionEvent::ReleasePoint() {
UpdatePointersAndID();
DCHECK_GT(GetPointerCount(), 0U);
if (GetPointerCount() > 1) {
Expand All @@ -125,29 +127,36 @@ void MockMotionEvent::ReleasePoint() {
} else {
set_action(ACTION_UP);
}
return *this;
}

void MockMotionEvent::CancelPoint() {
MockMotionEvent& MockMotionEvent::CancelPoint() {
UpdatePointersAndID();
DCHECK_GT(GetPointerCount(), 0U);
set_action(ACTION_CANCEL);
return *this;
}

void MockMotionEvent::SetTouchMajor(float new_touch_major) {
MockMotionEvent& MockMotionEvent::SetTouchMajor(float new_touch_major) {
for (size_t i = 0; i < GetPointerCount(); ++i)
pointer(i).touch_major = new_touch_major;
return *this;
}

void MockMotionEvent::SetRawOffset(float raw_offset_x, float raw_offset_y) {
MockMotionEvent& MockMotionEvent::SetRawOffset(float raw_offset_x,
float raw_offset_y) {
for (size_t i = 0; i < GetPointerCount(); ++i) {
pointer(i).raw_x = pointer(i).x + raw_offset_x;
pointer(i).raw_y = pointer(i).y + raw_offset_y;
}
return *this;
}

void MockMotionEvent::SetToolType(size_t pointer_index, ToolType tool_type) {
MockMotionEvent& MockMotionEvent::SetToolType(size_t pointer_index,
ToolType tool_type) {
DCHECK_LT(pointer_index, GetPointerCount());
pointer(pointer_index).tool_type = tool_type;
return *this;
}

void MockMotionEvent::PushPointer(float x, float y) {
Expand All @@ -169,9 +178,10 @@ void MockMotionEvent::UpdatePointersAndID() {
}
}

void MockMotionEvent::SetPrimaryPointerId(int id) {
MockMotionEvent& MockMotionEvent::SetPrimaryPointerId(int id) {
DCHECK_GT(GetPointerCount(), 0U);
pointer(0).id = id;
return *this;
}

std::string ToString(const MotionEvent& event) {
Expand Down
16 changes: 8 additions & 8 deletions ui/events/test/motion_event_test_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,14 @@ struct MockMotionEvent : public MotionEventGeneric {
~MockMotionEvent() override;

// Utility methods.
void PressPoint(float x, float y);
void MovePoint(size_t index, float x, float y);
void ReleasePoint();
void CancelPoint();
void SetTouchMajor(float new_touch_major);
void SetRawOffset(float raw_offset_x, float raw_offset_y);
void SetToolType(size_t index, ToolType tool_type);
void SetPrimaryPointerId(int id);
MockMotionEvent& PressPoint(float x, float y);
MockMotionEvent& MovePoint(size_t index, float x, float y);
MockMotionEvent& ReleasePoint();
MockMotionEvent& CancelPoint();
MockMotionEvent& SetTouchMajor(float new_touch_major);
MockMotionEvent& SetRawOffset(float raw_offset_x, float raw_offset_y);
MockMotionEvent& SetToolType(size_t index, ToolType tool_type);
MockMotionEvent& SetPrimaryPointerId(int id);

private:
void PushPointer(float x, float y);
Expand Down
4 changes: 4 additions & 0 deletions ui/touch_selection/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@ component("touch_selection") {
output_name = "ui_touch_selection"

sources = [
"longpress_drag_selector.cc",
"longpress_drag_selector.h",
"selection_event_type.h",
"touch_handle.cc",
"touch_handle.h",
"touch_handle_orientation.h",
"touch_selection_controller.cc",
"touch_selection_controller.h",
"touch_selection_draggable.h",
"ui_touch_selection_export.h",
]

Expand Down Expand Up @@ -52,6 +55,7 @@ component("touch_selection") {

test("ui_touch_selection_unittests") {
sources = [
"longpress_drag_selector_unittest.cc",
"touch_handle_unittest.cc",
"touch_selection_controller_unittest.cc",
]
Expand Down
141 changes: 141 additions & 0 deletions ui/touch_selection/longpress_drag_selector.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// Copyright 2015 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/touch_selection/longpress_drag_selector.h"

#include "base/auto_reset.h"
#include "ui/events/gesture_detection/motion_event.h"

namespace ui {

LongPressDragSelector::LongPressDragSelector(
LongPressDragSelectorClient* client)
: client_(client),
state_(INACTIVE),
has_longpress_drag_start_anchor_(false) {
}

LongPressDragSelector::~LongPressDragSelector() {
}

bool LongPressDragSelector::WillHandleTouchEvent(const MotionEvent& event) {
switch (event.GetAction()) {
case MotionEvent::ACTION_DOWN:
touch_down_position_.SetPoint(event.GetX(), event.GetY());
touch_down_time_ = event.GetEventTime();
has_longpress_drag_start_anchor_ = false;
SetState(LONGPRESS_PENDING);
return false;

case MotionEvent::ACTION_UP:
case MotionEvent::ACTION_CANCEL:
SetState(INACTIVE);
return false;

case MotionEvent::ACTION_MOVE:
break;

default:
return false;
}

if (state_ != DRAG_PENDING && state_ != DRAGGING)
return false;

gfx::PointF position(event.GetX(), event.GetY());
if (state_ == DRAGGING) {
gfx::PointF drag_position = position + longpress_drag_selection_offset_;
client_->OnDragUpdate(*this, drag_position);
return true;
}

// We can't use |touch_down_position_| as the offset anchor, as
// showing the selection UI may have shifted the motion coordinates.
if (!has_longpress_drag_start_anchor_) {
has_longpress_drag_start_anchor_ = true;
longpress_drag_start_anchor_ = position;
return true;
}

// Allow an additional slop affordance after the longpress occurs.
gfx::Vector2dF delta = position - longpress_drag_start_anchor_;
if (client_->IsWithinTapSlop(delta))
return true;

gfx::PointF selection_start = client_->GetSelectionStart();
gfx::PointF selection_end = client_->GetSelectionEnd();
bool extend_selection_start = false;
if (std::abs(delta.y()) > std::abs(delta.x())) {
// If initial motion is up/down, extend the start/end selection bound.
extend_selection_start = delta.y() < 0;
} else {
// Otherwise extend the selection bound toward which we're moving.
// Note that, for mixed RTL text, or for multiline selections triggered
// by longpress, this may not pick the most suitable drag target
gfx::Vector2dF start_delta = selection_start - position;

// The vectors must be normalized to make dot product comparison meaningful.
if (!start_delta.IsZero())
start_delta.Scale(1.f / start_delta.Length());
gfx::Vector2dF end_delta = selection_end - position;
if (!end_delta.IsZero())
end_delta.Scale(1.f / start_delta.Length());

// The larger the dot product the more similar the direction.
extend_selection_start =
gfx::DotProduct(start_delta, delta) > gfx::DotProduct(end_delta, delta);
}

gfx::PointF extent = extend_selection_start ? selection_start : selection_end;
longpress_drag_selection_offset_ = extent - position;
client_->OnDragBegin(*this, extent);
SetState(DRAGGING);
return true;
}

bool LongPressDragSelector::IsActive() const {
return state_ != INACTIVE && state_ != LONGPRESS_PENDING;
}

void LongPressDragSelector::OnLongPressEvent(base::TimeTicks event_time,
const gfx::PointF& position) {
// We have no guarantees that the current gesture stream is aligned with the
// observed touch stream. We only know that the gesture sequence is downstream
// from the touch sequence. Using a time/distance heuristic helps ensure that
// the observed longpress corresponds to the active touch sequence.
if (state_ == LONGPRESS_PENDING &&
// Ensure the down event occurs *before* the longpress event. Use a
// small time epsilon to account for floating point time conversion.
(touch_down_time_ < event_time + base::TimeDelta::FromMicroseconds(10)) &&
client_->IsWithinTapSlop(touch_down_position_ - position)) {
SetState(SELECTION_PENDING);
}
}

void LongPressDragSelector::OnSelectionActivated() {
if (state_ == SELECTION_PENDING)
SetState(DRAG_PENDING);
}

void LongPressDragSelector::OnSelectionDeactivated() {
SetState(INACTIVE);
}

void LongPressDragSelector::SetState(SelectionState state) {
if (state_ == state)
return;

const bool was_dragging = state_ == DRAGGING;
const bool was_active = IsActive();
state_ = state;

// TODO(jdduke): Add UMA for tracking relative longpress drag frequency.
if (was_dragging)
client_->OnDragEnd(*this);

if (was_active != IsActive())
client_->OnLongPressDragActiveStateChanged();
}

} // namespace ui
Loading

0 comments on commit 3f2a3ab

Please sign in to comment.