// 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/touch_selection/touch_handle.h" #include namespace ui { namespace { // Maximum duration of a fade sequence. const double kFadeDurationMs = 200; // Maximum amount of travel for a fade sequence. This avoids handle "ghosting" // when the handle is moving rapidly while the fade is active. const double kFadeDistanceSquared = 20.f * 20.f; // Avoid using an empty touch rect, as it may fail the intersection test event // if it lies within the other rect's bounds. const float kMinTouchMajorForHitTesting = 1.f; // The maximum touch size to use when computing whether a touch point is // targetting a touch handle. This is necessary for devices that misreport // touch radii, preventing inappropriately largely touch sizes from completely // breaking handle dragging behavior. const float kMaxTouchMajorForHitTesting = 36.f; // Note that the intersection region is boundary *exclusive*. bool RectIntersectsCircle(const gfx::RectF& rect, const gfx::PointF& circle_center, float circle_radius) { DCHECK_GT(circle_radius, 0.f); // An intersection occurs if the closest point between the rect and the // circle's center is less than the circle's radius. gfx::PointF closest_point_in_rect(circle_center); closest_point_in_rect.SetToMax(rect.origin()); closest_point_in_rect.SetToMin(rect.bottom_right()); gfx::Vector2dF distance = circle_center - closest_point_in_rect; return distance.LengthSquared() < (circle_radius * circle_radius); } } // namespace // Responsible for rendering a selection or insertion handle for text editing. TouchHandle::TouchHandle(TouchHandleClient* client, TouchHandleOrientation orientation) : drawable_(client->CreateDrawable()), client_(client), orientation_(orientation), deferred_orientation_(TOUCH_HANDLE_ORIENTATION_UNDEFINED), alpha_(0.f), animate_deferred_fade_(false), enabled_(true), is_visible_(false), is_dragging_(false), is_drag_within_tap_region_(false) { DCHECK_NE(orientation, TOUCH_HANDLE_ORIENTATION_UNDEFINED); drawable_->SetEnabled(enabled_); drawable_->SetOrientation(orientation_); drawable_->SetAlpha(alpha_); drawable_->SetFocus(position_); } TouchHandle::~TouchHandle() { } void TouchHandle::SetEnabled(bool enabled) { if (enabled_ == enabled) return; if (!enabled) { EndDrag(); EndFade(); } enabled_ = enabled; drawable_->SetEnabled(enabled); } void TouchHandle::SetVisible(bool visible, AnimationStyle animation_style) { DCHECK(enabled_); if (is_visible_ == visible) return; is_visible_ = visible; // Handle repositioning may have been deferred while previously invisible. if (visible) drawable_->SetFocus(position_); bool animate = animation_style != ANIMATION_NONE; if (is_dragging_) { animate_deferred_fade_ = animate; return; } if (animate) BeginFade(); else EndFade(); } void TouchHandle::SetPosition(const gfx::PointF& position) { DCHECK(enabled_); if (position_ == position) return; position_ = position; // Suppress repositioning a handle while invisible or fading out to prevent it // from "ghosting" outside the visible bounds. The position will be pushed to // the drawable when the handle regains visibility (see |SetVisible()|). if (is_visible_) drawable_->SetFocus(position_); } void TouchHandle::SetOrientation(TouchHandleOrientation orientation) { DCHECK(enabled_); DCHECK_NE(orientation, TOUCH_HANDLE_ORIENTATION_UNDEFINED); if (is_dragging_) { deferred_orientation_ = orientation; return; } DCHECK_EQ(deferred_orientation_, TOUCH_HANDLE_ORIENTATION_UNDEFINED); if (orientation_ == orientation) return; orientation_ = orientation; drawable_->SetOrientation(orientation); } bool TouchHandle::WillHandleTouchEvent(const MotionEvent& event) { if (!enabled_) return false; if (!is_dragging_ && event.GetAction() != MotionEvent::ACTION_DOWN) return false; switch (event.GetAction()) { case MotionEvent::ACTION_DOWN: { if (!is_visible_) return false; const gfx::PointF touch_point(event.GetX(), event.GetY()); const float touch_radius = std::max( kMinTouchMajorForHitTesting, std::min(kMaxTouchMajorForHitTesting, event.GetTouchMajor())) * 0.5f; if (!RectIntersectsCircle(drawable_->GetVisibleBounds(), touch_point, touch_radius)) { EndDrag(); return false; } touch_down_position_ = touch_point; touch_to_focus_offset_ = position_ - touch_down_position_; touch_down_time_ = event.GetEventTime(); BeginDrag(); } break; case MotionEvent::ACTION_MOVE: { gfx::PointF touch_move_position(event.GetX(), event.GetY()); if (is_drag_within_tap_region_) { const float tap_slop = client_->GetTapSlop(); is_drag_within_tap_region_ = (touch_move_position - touch_down_position_).LengthSquared() < tap_slop * tap_slop; } // Note that we signal drag update even if we're inside the tap region, // as there are cases where characters are narrower than the slop length. client_->OnHandleDragUpdate(*this, touch_move_position + touch_to_focus_offset_); } break; case MotionEvent::ACTION_UP: { if (is_drag_within_tap_region_ && (event.GetEventTime() - touch_down_time_) < client_->GetTapTimeout()) { client_->OnHandleTapped(*this); } EndDrag(); } break; case MotionEvent::ACTION_CANCEL: EndDrag(); break; default: break; }; return true; } bool TouchHandle::Animate(base::TimeTicks frame_time) { if (fade_end_time_ == base::TimeTicks()) return false; DCHECK(enabled_); float time_u = 1.f - (fade_end_time_ - frame_time).InMillisecondsF() / kFadeDurationMs; float position_u = (position_ - fade_start_position_).LengthSquared() / kFadeDistanceSquared; float u = std::max(time_u, position_u); SetAlpha(is_visible_ ? u : 1.f - u); if (u >= 1.f) { EndFade(); return false; } return true; } void TouchHandle::BeginDrag() { DCHECK(enabled_); if (is_dragging_) return; EndFade(); is_dragging_ = true; is_drag_within_tap_region_ = true; client_->OnHandleDragBegin(*this); } void TouchHandle::EndDrag() { DCHECK(enabled_); if (!is_dragging_) return; is_dragging_ = false; is_drag_within_tap_region_ = false; client_->OnHandleDragEnd(*this); if (deferred_orientation_ != TOUCH_HANDLE_ORIENTATION_UNDEFINED) { TouchHandleOrientation deferred_orientation = deferred_orientation_; deferred_orientation_ = TOUCH_HANDLE_ORIENTATION_UNDEFINED; SetOrientation(deferred_orientation); } if (animate_deferred_fade_) { BeginFade(); } else { // As drawable visibility assignment is deferred while dragging, push the // change by forcing fade completion. EndFade(); } } void TouchHandle::BeginFade() { DCHECK(enabled_); DCHECK(!is_dragging_); animate_deferred_fade_ = false; const float target_alpha = is_visible_ ? 1.f : 0.f; if (target_alpha == alpha_) { EndFade(); return; } fade_end_time_ = base::TimeTicks::Now() + base::TimeDelta::FromMillisecondsD( kFadeDurationMs * std::abs(target_alpha - alpha_)); fade_start_position_ = position_; client_->SetNeedsAnimate(); } void TouchHandle::EndFade() { DCHECK(enabled_); animate_deferred_fade_ = false; fade_end_time_ = base::TimeTicks(); SetAlpha(is_visible_ ? 1.f : 0.f); } void TouchHandle::SetAlpha(float alpha) { alpha = std::max(0.f, std::min(1.f, alpha)); if (alpha_ == alpha) return; alpha_ = alpha; drawable_->SetAlpha(alpha); } } // namespace ui