touch_handle.cc 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. // Copyright 2014 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.
  4. #include "ui/touch_selection/touch_handle.h"
  5. #include <algorithm>
  6. #include <cmath>
  7. #include "base/check_op.h"
  8. #include "base/cxx17_backports.h"
  9. #include "base/metrics/histogram_macros.h"
  10. #include "base/notreached.h"
  11. namespace ui {
  12. namespace {
  13. // Maximum duration of a fade sequence.
  14. constexpr auto kFadeDuration = base::Milliseconds(200);
  15. // Maximum amount of travel for a fade sequence. This avoids handle "ghosting"
  16. // when the handle is moving rapidly while the fade is active.
  17. constexpr double kFadeDistanceSquared = 20.0f * 20.0f;
  18. // Avoid using an empty touch rect, as it may fail the intersection test event
  19. // if it lies within the other rect's bounds.
  20. constexpr float kMinTouchMajorForHitTesting = 1.0f;
  21. // The maximum touch size to use when computing whether a touch point is
  22. // targetting a touch handle. This is necessary for devices that misreport
  23. // touch radii, preventing inappropriately largely touch sizes from completely
  24. // breaking handle dragging behavior.
  25. constexpr float kMaxTouchMajorForHitTesting = 36.0f;
  26. // Note that the intersection region is boundary *exclusive*.
  27. bool RectIntersectsCircle(const gfx::RectF& rect,
  28. const gfx::PointF& circle_center,
  29. float circle_radius) {
  30. DCHECK_GT(circle_radius, 0.f);
  31. // An intersection occurs if the closest point between the rect and the
  32. // circle's center is less than the circle's radius.
  33. gfx::PointF closest_point_in_rect(circle_center);
  34. closest_point_in_rect.SetToMax(rect.origin());
  35. closest_point_in_rect.SetToMin(rect.bottom_right());
  36. gfx::Vector2dF distance = circle_center - closest_point_in_rect;
  37. return distance.LengthSquared() < (circle_radius * circle_radius);
  38. }
  39. } // namespace
  40. // TODO(AviD): Remove this once logging(DCHECK) supports enum class.
  41. static std::ostream& operator<<(std::ostream& os,
  42. const TouchHandleOrientation& orientation) {
  43. switch (orientation) {
  44. case TouchHandleOrientation::LEFT:
  45. return os << "LEFT";
  46. case TouchHandleOrientation::RIGHT:
  47. return os << "RIGHT";
  48. case TouchHandleOrientation::CENTER:
  49. return os << "CENTER";
  50. case TouchHandleOrientation::UNDEFINED:
  51. return os << "UNDEFINED";
  52. default:
  53. return os << "INVALID: " << static_cast<int>(orientation);
  54. }
  55. }
  56. // Responsible for rendering a selection or insertion handle for text editing.
  57. TouchHandle::TouchHandle(TouchHandleClient* client,
  58. TouchHandleOrientation orientation,
  59. const gfx::RectF& viewport_rect)
  60. : drawable_(client->CreateDrawable()),
  61. client_(client),
  62. viewport_rect_(viewport_rect),
  63. orientation_(orientation),
  64. deferred_orientation_(TouchHandleOrientation::UNDEFINED),
  65. alpha_(0.f),
  66. animate_deferred_fade_(false),
  67. enabled_(true),
  68. is_visible_(false),
  69. is_dragging_(false),
  70. is_drag_within_tap_region_(false),
  71. is_handle_layout_update_required_(false),
  72. mirror_vertical_(false),
  73. mirror_horizontal_(false) {
  74. DCHECK_NE(orientation, TouchHandleOrientation::UNDEFINED);
  75. drawable_->SetEnabled(enabled_);
  76. drawable_->SetOrientation(orientation_, false, false);
  77. drawable_->SetOrigin(focus_bottom_);
  78. drawable_->SetAlpha(alpha_);
  79. handle_horizontal_padding_ = drawable_->GetDrawableHorizontalPaddingRatio();
  80. }
  81. TouchHandle::~TouchHandle() {
  82. }
  83. void TouchHandle::SetEnabled(bool enabled) {
  84. if (enabled_ == enabled)
  85. return;
  86. if (!enabled) {
  87. SetVisible(false, ANIMATION_NONE);
  88. EndDrag();
  89. EndFade();
  90. }
  91. enabled_ = enabled;
  92. drawable_->SetEnabled(enabled);
  93. }
  94. void TouchHandle::SetVisible(bool visible, AnimationStyle animation_style) {
  95. DCHECK(enabled_);
  96. if (is_visible_ == visible)
  97. return;
  98. is_visible_ = visible;
  99. // Handle repositioning may have been deferred while previously invisible.
  100. if (visible)
  101. SetUpdateLayoutRequired();
  102. bool animate = animation_style != ANIMATION_NONE;
  103. if (is_dragging_) {
  104. animate_deferred_fade_ = animate;
  105. return;
  106. }
  107. if (animate)
  108. BeginFade();
  109. else
  110. EndFade();
  111. }
  112. void TouchHandle::SetFocus(const gfx::PointF& top, const gfx::PointF& bottom) {
  113. DCHECK(enabled_);
  114. if (focus_top_ == top && focus_bottom_ == bottom)
  115. return;
  116. focus_top_ = top;
  117. focus_bottom_ = bottom;
  118. SetUpdateLayoutRequired();
  119. }
  120. void TouchHandle::SetViewportRect(const gfx::RectF& viewport_rect) {
  121. DCHECK(enabled_);
  122. if (viewport_rect_ == viewport_rect)
  123. return;
  124. viewport_rect_ = viewport_rect;
  125. SetUpdateLayoutRequired();
  126. }
  127. void TouchHandle::SetOrientation(TouchHandleOrientation orientation) {
  128. DCHECK(enabled_);
  129. DCHECK_NE(orientation, TouchHandleOrientation::UNDEFINED);
  130. if (is_dragging_) {
  131. deferred_orientation_ = orientation;
  132. return;
  133. }
  134. DCHECK_EQ(deferred_orientation_, TouchHandleOrientation::UNDEFINED);
  135. if (orientation_ == orientation)
  136. return;
  137. orientation_ = orientation;
  138. SetUpdateLayoutRequired();
  139. }
  140. bool TouchHandle::WillHandleTouchEvent(const MotionEvent& event) {
  141. if (!enabled_)
  142. return false;
  143. if (!is_dragging_ && event.GetAction() != MotionEvent::Action::DOWN)
  144. return false;
  145. switch (event.GetAction()) {
  146. case MotionEvent::Action::DOWN: {
  147. if (!is_visible_)
  148. return false;
  149. const gfx::PointF touch_point(event.GetX(), event.GetY());
  150. const float touch_radius =
  151. base::clamp(event.GetTouchMajor(), kMinTouchMajorForHitTesting,
  152. kMaxTouchMajorForHitTesting) *
  153. 0.5f;
  154. const gfx::RectF drawable_bounds = drawable_->GetVisibleBounds();
  155. // Only use the touch radius for targetting if the touch is at or below
  156. // the drawable area. This makes it easier to interact with the line of
  157. // text above the drawable.
  158. if (touch_point.y() < drawable_bounds.y() ||
  159. !RectIntersectsCircle(drawable_bounds, touch_point, touch_radius)) {
  160. EndDrag();
  161. return false;
  162. }
  163. touch_down_position_ = touch_point;
  164. touch_drag_offset_ = focus_bottom_ - touch_down_position_;
  165. touch_down_time_ = event.GetEventTime();
  166. BeginDrag();
  167. } break;
  168. case MotionEvent::Action::MOVE: {
  169. gfx::PointF touch_move_position(event.GetX(), event.GetY());
  170. is_drag_within_tap_region_ &=
  171. client_->IsWithinTapSlop(touch_down_position_ - touch_move_position);
  172. // Note that we signal drag update even if we're inside the tap region,
  173. // as there are cases where characters are narrower than the slop length.
  174. client_->OnDragUpdate(*this, touch_move_position + touch_drag_offset_);
  175. } break;
  176. case MotionEvent::Action::UP: {
  177. if (is_drag_within_tap_region_ &&
  178. (event.GetEventTime() - touch_down_time_) <
  179. client_->GetMaxTapDuration()) {
  180. client_->OnHandleTapped(*this);
  181. }
  182. EndDrag();
  183. } break;
  184. case MotionEvent::Action::CANCEL:
  185. EndDrag();
  186. break;
  187. default:
  188. break;
  189. };
  190. return true;
  191. }
  192. bool TouchHandle::IsActive() const {
  193. return is_dragging_;
  194. }
  195. bool TouchHandle::Animate(base::TimeTicks frame_time) {
  196. if (fade_end_time_ == base::TimeTicks())
  197. return false;
  198. DCHECK(enabled_);
  199. const float time_u = 1.0f - (fade_end_time_ - frame_time) / kFadeDuration;
  200. const float position_u =
  201. (focus_bottom_ - fade_start_position_).LengthSquared() /
  202. kFadeDistanceSquared;
  203. const float u = std::max(time_u, position_u);
  204. SetAlpha(is_visible_ ? u : 1.0f - u);
  205. if (u >= 1)
  206. EndFade();
  207. return u < 1;
  208. }
  209. gfx::RectF TouchHandle::GetVisibleBounds() const {
  210. if (!is_visible_ || !enabled_)
  211. return gfx::RectF();
  212. return drawable_->GetVisibleBounds();
  213. }
  214. void TouchHandle::UpdateHandleLayout() {
  215. // Suppress repositioning a handle while invisible or fading out to prevent it
  216. // from "ghosting" outside the visible bounds. The position will be pushed to
  217. // the drawable when the handle regains visibility (see |SetVisible()|).
  218. if (!is_visible_ || !is_handle_layout_update_required_)
  219. return;
  220. is_handle_layout_update_required_ = false;
  221. // Update mirror values only when dragging has stopped to prevent unwanted
  222. // inversion while dragging of handles.
  223. if (!is_dragging_) {
  224. gfx::RectF handle_bounds = drawable_->GetVisibleBounds();
  225. bool mirror_horizontal = false;
  226. bool mirror_vertical = false;
  227. const float handle_width =
  228. handle_bounds.width() * (1.0 - handle_horizontal_padding_);
  229. const float handle_height = handle_bounds.height();
  230. const float bottom_y_unmirrored =
  231. focus_bottom_.y() + handle_height + viewport_rect_.y();
  232. const float top_y_mirrored =
  233. focus_top_.y() - handle_height + viewport_rect_.y();
  234. const float bottom_y_clipped =
  235. std::max(bottom_y_unmirrored - viewport_rect_.bottom(), 0.f);
  236. const float top_y_clipped =
  237. std::max(viewport_rect_.y() - top_y_mirrored, 0.f);
  238. mirror_vertical = top_y_clipped < bottom_y_clipped;
  239. const float best_y_clipped =
  240. mirror_vertical ? top_y_clipped : bottom_y_clipped;
  241. UMA_HISTOGRAM_PERCENTAGE(
  242. "Event.TouchSelectionHandle.BottomHandleClippingPercentage",
  243. static_cast<int>((bottom_y_clipped / handle_height) * 100));
  244. UMA_HISTOGRAM_PERCENTAGE(
  245. "Event.TouchSelectionHandle.BestVerticalClippingPercentage",
  246. static_cast<int>((best_y_clipped / handle_height) * 100));
  247. UMA_HISTOGRAM_BOOLEAN(
  248. "Event.TouchSelectionHandle.ShouldFlipHandleVertically",
  249. mirror_vertical);
  250. UMA_HISTOGRAM_PERCENTAGE(
  251. "Event.TouchSelectionHandle.FlippingImprovementPercentage",
  252. static_cast<int>(((bottom_y_clipped - best_y_clipped) / handle_height) *
  253. 100));
  254. if (orientation_ == TouchHandleOrientation::LEFT) {
  255. const float left_x_clipped = std::max(
  256. viewport_rect_.x() - (focus_bottom_.x() - handle_width), 0.f);
  257. UMA_HISTOGRAM_PERCENTAGE(
  258. "Event.TouchSelectionHandle.LeftHandleClippingPercentage",
  259. static_cast<int>((left_x_clipped / handle_height) * 100));
  260. if (left_x_clipped > 0)
  261. mirror_horizontal = true;
  262. } else if (orientation_ == TouchHandleOrientation::RIGHT) {
  263. const float right_x_clipped = std::max(
  264. (focus_bottom_.x() + handle_width) - viewport_rect_.right(), 0.f);
  265. UMA_HISTOGRAM_PERCENTAGE(
  266. "Event.TouchSelectionHandle.RightHandleClippingPercentage",
  267. static_cast<int>((right_x_clipped / handle_height) * 100));
  268. if (right_x_clipped > 0)
  269. mirror_horizontal = true;
  270. }
  271. if (client_->IsAdaptiveHandleOrientationEnabled()) {
  272. mirror_horizontal_ = mirror_horizontal;
  273. mirror_vertical_ = mirror_vertical;
  274. }
  275. }
  276. drawable_->SetOrientation(orientation_, mirror_vertical_, mirror_horizontal_);
  277. drawable_->SetOrigin(ComputeHandleOrigin());
  278. }
  279. void TouchHandle::SetTransparent() {
  280. SetAlpha(0.f);
  281. }
  282. gfx::PointF TouchHandle::ComputeHandleOrigin() const {
  283. gfx::PointF focus = mirror_vertical_ ? focus_top_ : focus_bottom_;
  284. gfx::RectF drawable_bounds = drawable_->GetVisibleBounds();
  285. float drawable_width = drawable_->GetVisibleBounds().width();
  286. // Calculate the focal offsets from origin for the handle drawable
  287. // based on the orientation.
  288. int focal_offset_x = 0;
  289. int focal_offset_y = mirror_vertical_ ? drawable_bounds.height() : 0;
  290. switch (orientation_) {
  291. case ui::TouchHandleOrientation::LEFT:
  292. focal_offset_x =
  293. mirror_horizontal_
  294. ? drawable_width * handle_horizontal_padding_
  295. : drawable_width * (1.0f - handle_horizontal_padding_);
  296. break;
  297. case ui::TouchHandleOrientation::RIGHT:
  298. focal_offset_x =
  299. mirror_horizontal_
  300. ? drawable_width * (1.0f - handle_horizontal_padding_)
  301. : drawable_width * handle_horizontal_padding_;
  302. break;
  303. case ui::TouchHandleOrientation::CENTER:
  304. focal_offset_x = drawable_width * 0.5f;
  305. break;
  306. case ui::TouchHandleOrientation::UNDEFINED:
  307. NOTREACHED() << "Invalid touch handle orientation.";
  308. break;
  309. };
  310. return focus - gfx::Vector2dF(focal_offset_x, focal_offset_y);
  311. }
  312. void TouchHandle::BeginDrag() {
  313. DCHECK(enabled_);
  314. if (is_dragging_)
  315. return;
  316. EndFade();
  317. is_dragging_ = true;
  318. is_drag_within_tap_region_ = true;
  319. client_->OnDragBegin(*this, focus_bottom());
  320. }
  321. void TouchHandle::EndDrag() {
  322. DCHECK(enabled_);
  323. if (!is_dragging_)
  324. return;
  325. is_dragging_ = false;
  326. is_drag_within_tap_region_ = false;
  327. client_->OnDragEnd(*this);
  328. if (deferred_orientation_ != TouchHandleOrientation::UNDEFINED) {
  329. TouchHandleOrientation deferred_orientation = deferred_orientation_;
  330. deferred_orientation_ = TouchHandleOrientation::UNDEFINED;
  331. SetOrientation(deferred_orientation);
  332. // Handle layout may be deferred while the handle is dragged.
  333. SetUpdateLayoutRequired();
  334. UpdateHandleLayout();
  335. }
  336. if (animate_deferred_fade_) {
  337. BeginFade();
  338. } else {
  339. // As drawable visibility assignment is deferred while dragging, push the
  340. // change by forcing fade completion.
  341. EndFade();
  342. }
  343. }
  344. void TouchHandle::BeginFade() {
  345. DCHECK(enabled_);
  346. DCHECK(!is_dragging_);
  347. animate_deferred_fade_ = false;
  348. const float target_alpha = is_visible_ ? 1.f : 0.f;
  349. if (target_alpha == alpha_) {
  350. EndFade();
  351. return;
  352. }
  353. fade_end_time_ =
  354. base::TimeTicks::Now() + kFadeDuration * std::abs(target_alpha - alpha_);
  355. fade_start_position_ = focus_bottom_;
  356. client_->SetNeedsAnimate();
  357. }
  358. void TouchHandle::EndFade() {
  359. DCHECK(enabled_);
  360. animate_deferred_fade_ = false;
  361. fade_end_time_ = base::TimeTicks();
  362. SetAlpha(is_visible_ ? 1.f : 0.f);
  363. }
  364. void TouchHandle::SetAlpha(float alpha) {
  365. alpha = base::clamp(alpha, 0.0f, 1.0f);
  366. if (alpha_ == alpha)
  367. return;
  368. alpha_ = alpha;
  369. drawable_->SetAlpha(alpha);
  370. }
  371. void TouchHandle::SetUpdateLayoutRequired() {
  372. // TODO(AviD): Make the layout call explicit to the caller by adding this in
  373. // TouchHandleClient.
  374. is_handle_layout_update_required_ = true;
  375. }
  376. } // namespace ui