| // 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 <algorithm> |
| #include <cmath> |
| |
| #include "base/check_op.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/notreached.h" |
| #include "base/numerics/ranges.h" |
| |
| 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 |
| |
| // TODO(AviD): Remove this once logging(DCHECK) supports enum class. |
| static std::ostream& operator<<(std::ostream& os, |
| const TouchHandleOrientation& orientation) { |
| switch (orientation) { |
| case TouchHandleOrientation::LEFT: |
| return os << "LEFT"; |
| case TouchHandleOrientation::RIGHT: |
| return os << "RIGHT"; |
| case TouchHandleOrientation::CENTER: |
| return os << "CENTER"; |
| case TouchHandleOrientation::UNDEFINED: |
| return os << "UNDEFINED"; |
| default: |
| return os << "INVALID: " << static_cast<int>(orientation); |
| } |
| } |
| |
| // Responsible for rendering a selection or insertion handle for text editing. |
| TouchHandle::TouchHandle(TouchHandleClient* client, |
| TouchHandleOrientation orientation, |
| const gfx::RectF& viewport_rect) |
| : drawable_(client->CreateDrawable()), |
| client_(client), |
| viewport_rect_(viewport_rect), |
| orientation_(orientation), |
| deferred_orientation_(TouchHandleOrientation::UNDEFINED), |
| alpha_(0.f), |
| animate_deferred_fade_(false), |
| enabled_(true), |
| is_visible_(false), |
| is_dragging_(false), |
| is_drag_within_tap_region_(false), |
| is_handle_layout_update_required_(false), |
| mirror_vertical_(false), |
| mirror_horizontal_(false) { |
| DCHECK_NE(orientation, TouchHandleOrientation::UNDEFINED); |
| drawable_->SetEnabled(enabled_); |
| drawable_->SetOrientation(orientation_, false, false); |
| drawable_->SetOrigin(focus_bottom_); |
| drawable_->SetAlpha(alpha_); |
| handle_horizontal_padding_ = drawable_->GetDrawableHorizontalPaddingRatio(); |
| } |
| |
| TouchHandle::~TouchHandle() { |
| } |
| |
| void TouchHandle::SetEnabled(bool enabled) { |
| if (enabled_ == enabled) |
| return; |
| if (!enabled) { |
| SetVisible(false, ANIMATION_NONE); |
| 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) |
| SetUpdateLayoutRequired(); |
| |
| bool animate = animation_style != ANIMATION_NONE; |
| if (is_dragging_) { |
| animate_deferred_fade_ = animate; |
| return; |
| } |
| |
| if (animate) |
| BeginFade(); |
| else |
| EndFade(); |
| } |
| |
| void TouchHandle::SetFocus(const gfx::PointF& top, const gfx::PointF& bottom) { |
| DCHECK(enabled_); |
| if (focus_top_ == top && focus_bottom_ == bottom) |
| return; |
| |
| focus_top_ = top; |
| focus_bottom_ = bottom; |
| SetUpdateLayoutRequired(); |
| } |
| |
| void TouchHandle::SetViewportRect(const gfx::RectF& viewport_rect) { |
| DCHECK(enabled_); |
| if (viewport_rect_ == viewport_rect) |
| return; |
| |
| viewport_rect_ = viewport_rect; |
| SetUpdateLayoutRequired(); |
| } |
| |
| void TouchHandle::SetOrientation(TouchHandleOrientation orientation) { |
| DCHECK(enabled_); |
| DCHECK_NE(orientation, TouchHandleOrientation::UNDEFINED); |
| if (is_dragging_) { |
| deferred_orientation_ = orientation; |
| return; |
| } |
| DCHECK_EQ(deferred_orientation_, TouchHandleOrientation::UNDEFINED); |
| if (orientation_ == orientation) |
| return; |
| |
| orientation_ = orientation; |
| SetUpdateLayoutRequired(); |
| } |
| |
| 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 = |
| base::ClampToRange(event.GetTouchMajor(), kMinTouchMajorForHitTesting, |
| kMaxTouchMajorForHitTesting) * |
| 0.5f; |
| const gfx::RectF drawable_bounds = drawable_->GetVisibleBounds(); |
| // Only use the touch radius for targetting if the touch is at or below |
| // the drawable area. This makes it easier to interact with the line of |
| // text above the drawable. |
| if (touch_point.y() < drawable_bounds.y() || |
| !RectIntersectsCircle(drawable_bounds, touch_point, touch_radius)) { |
| EndDrag(); |
| return false; |
| } |
| touch_down_position_ = touch_point; |
| touch_drag_offset_ = focus_bottom_ - touch_down_position_; |
| touch_down_time_ = event.GetEventTime(); |
| BeginDrag(); |
| } break; |
| |
| case MotionEvent::Action::MOVE: { |
| gfx::PointF touch_move_position(event.GetX(), event.GetY()); |
| is_drag_within_tap_region_ &= |
| client_->IsWithinTapSlop(touch_down_position_ - touch_move_position); |
| |
| // 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_->OnDragUpdate(*this, touch_move_position + touch_drag_offset_); |
| } break; |
| |
| case MotionEvent::Action::UP: { |
| if (is_drag_within_tap_region_ && |
| (event.GetEventTime() - touch_down_time_) < |
| client_->GetMaxTapDuration()) { |
| client_->OnHandleTapped(*this); |
| } |
| |
| EndDrag(); |
| } break; |
| |
| case MotionEvent::Action::CANCEL: |
| EndDrag(); |
| break; |
| |
| default: |
| break; |
| }; |
| return true; |
| } |
| |
| bool TouchHandle::IsActive() const { |
| return is_dragging_; |
| } |
| |
| 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 = (focus_bottom_ - 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; |
| } |
| |
| gfx::RectF TouchHandle::GetVisibleBounds() const { |
| if (!is_visible_ || !enabled_) |
| return gfx::RectF(); |
| |
| return drawable_->GetVisibleBounds(); |
| } |
| |
| void TouchHandle::UpdateHandleLayout() { |
| // 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_ || !is_handle_layout_update_required_) |
| return; |
| |
| is_handle_layout_update_required_ = false; |
| |
| // Update mirror values only when dragging has stopped to prevent unwanted |
| // inversion while dragging of handles. |
| if (!is_dragging_) { |
| gfx::RectF handle_bounds = drawable_->GetVisibleBounds(); |
| bool mirror_horizontal = false; |
| bool mirror_vertical = false; |
| |
| const float handle_width = |
| handle_bounds.width() * (1.0 - handle_horizontal_padding_); |
| const float handle_height = handle_bounds.height(); |
| |
| const float bottom_y_unmirrored = |
| focus_bottom_.y() + handle_height + viewport_rect_.y(); |
| const float top_y_mirrored = |
| focus_top_.y() - handle_height + viewport_rect_.y(); |
| |
| const float bottom_y_clipped = |
| std::max(bottom_y_unmirrored - viewport_rect_.bottom(), 0.f); |
| const float top_y_clipped = |
| std::max(viewport_rect_.y() - top_y_mirrored, 0.f); |
| |
| mirror_vertical = top_y_clipped < bottom_y_clipped; |
| |
| const float best_y_clipped = |
| mirror_vertical ? top_y_clipped : bottom_y_clipped; |
| |
| UMA_HISTOGRAM_PERCENTAGE( |
| "Event.TouchSelectionHandle.BottomHandleClippingPercentage", |
| static_cast<int>((bottom_y_clipped / handle_height) * 100)); |
| UMA_HISTOGRAM_PERCENTAGE( |
| "Event.TouchSelectionHandle.BestVerticalClippingPercentage", |
| static_cast<int>((best_y_clipped / handle_height) * 100)); |
| UMA_HISTOGRAM_BOOLEAN( |
| "Event.TouchSelectionHandle.ShouldFlipHandleVertically", |
| mirror_vertical); |
| UMA_HISTOGRAM_PERCENTAGE( |
| "Event.TouchSelectionHandle.FlippingImprovementPercentage", |
| static_cast<int>(((bottom_y_clipped - best_y_clipped) / handle_height) * |
| 100)); |
| |
| if (orientation_ == TouchHandleOrientation::LEFT) { |
| const float left_x_clipped = std::max( |
| viewport_rect_.x() - (focus_bottom_.x() - handle_width), 0.f); |
| UMA_HISTOGRAM_PERCENTAGE( |
| "Event.TouchSelectionHandle.LeftHandleClippingPercentage", |
| static_cast<int>((left_x_clipped / handle_height) * 100)); |
| if (left_x_clipped > 0) |
| mirror_horizontal = true; |
| } else if (orientation_ == TouchHandleOrientation::RIGHT) { |
| const float right_x_clipped = std::max( |
| (focus_bottom_.x() + handle_width) - viewport_rect_.right(), 0.f); |
| UMA_HISTOGRAM_PERCENTAGE( |
| "Event.TouchSelectionHandle.RightHandleClippingPercentage", |
| static_cast<int>((right_x_clipped / handle_height) * 100)); |
| if (right_x_clipped > 0) |
| mirror_horizontal = true; |
| } |
| |
| if (client_->IsAdaptiveHandleOrientationEnabled()) { |
| mirror_horizontal_ = mirror_horizontal; |
| mirror_vertical_ = mirror_vertical; |
| } |
| } |
| |
| drawable_->SetOrientation(orientation_, mirror_vertical_, mirror_horizontal_); |
| drawable_->SetOrigin(ComputeHandleOrigin()); |
| } |
| |
| void TouchHandle::SetTransparent() { |
| SetAlpha(0.f); |
| } |
| |
| gfx::PointF TouchHandle::ComputeHandleOrigin() const { |
| gfx::PointF focus = mirror_vertical_ ? focus_top_ : focus_bottom_; |
| gfx::RectF drawable_bounds = drawable_->GetVisibleBounds(); |
| float drawable_width = drawable_->GetVisibleBounds().width(); |
| |
| // Calculate the focal offsets from origin for the handle drawable |
| // based on the orientation. |
| int focal_offset_x = 0; |
| int focal_offset_y = mirror_vertical_ ? drawable_bounds.height() : 0; |
| switch (orientation_) { |
| case ui::TouchHandleOrientation::LEFT: |
| focal_offset_x = |
| mirror_horizontal_ |
| ? drawable_width * handle_horizontal_padding_ |
| : drawable_width * (1.0f - handle_horizontal_padding_); |
| break; |
| case ui::TouchHandleOrientation::RIGHT: |
| focal_offset_x = |
| mirror_horizontal_ |
| ? drawable_width * (1.0f - handle_horizontal_padding_) |
| : drawable_width * handle_horizontal_padding_; |
| break; |
| case ui::TouchHandleOrientation::CENTER: |
| focal_offset_x = drawable_width * 0.5f; |
| break; |
| case ui::TouchHandleOrientation::UNDEFINED: |
| NOTREACHED() << "Invalid touch handle orientation."; |
| break; |
| }; |
| |
| return focus - gfx::Vector2dF(focal_offset_x, focal_offset_y); |
| } |
| |
| void TouchHandle::BeginDrag() { |
| DCHECK(enabled_); |
| if (is_dragging_) |
| return; |
| EndFade(); |
| is_dragging_ = true; |
| is_drag_within_tap_region_ = true; |
| client_->OnDragBegin(*this, focus_bottom()); |
| } |
| |
| void TouchHandle::EndDrag() { |
| DCHECK(enabled_); |
| if (!is_dragging_) |
| return; |
| |
| is_dragging_ = false; |
| is_drag_within_tap_region_ = false; |
| client_->OnDragEnd(*this); |
| |
| if (deferred_orientation_ != TouchHandleOrientation::UNDEFINED) { |
| TouchHandleOrientation deferred_orientation = deferred_orientation_; |
| deferred_orientation_ = TouchHandleOrientation::UNDEFINED; |
| SetOrientation(deferred_orientation); |
| // Handle layout may be deferred while the handle is dragged. |
| SetUpdateLayoutRequired(); |
| UpdateHandleLayout(); |
| } |
| |
| 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_ = focus_bottom_; |
| 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 = base::ClampToRange(alpha, 0.0f, 1.0f); |
| if (alpha_ == alpha) |
| return; |
| alpha_ = alpha; |
| drawable_->SetAlpha(alpha); |
| } |
| |
| void TouchHandle::SetUpdateLayoutRequired() { |
| // TODO(AviD): Make the layout call explicit to the caller by adding this in |
| // TouchHandleClient. |
| is_handle_layout_update_required_ = true; |
| } |
| |
| } // namespace ui |