1 /* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.view; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.annotation.TestApi; 22 import android.content.pm.PackageManager; 23 import android.graphics.Rect; 24 import android.util.ArrayMap; 25 import android.util.ArraySet; 26 27 import java.util.ArrayList; 28 import java.util.Arrays; 29 import java.util.Collections; 30 import java.util.Comparator; 31 import java.util.HashMap; 32 import java.util.List; 33 34 /** 35 * The algorithm used for finding the next focusable view in a given direction 36 * from a view that currently has focus. 37 */ 38 public class FocusFinder { 39 40 private static final ThreadLocal<FocusFinder> tlFocusFinder = 41 new ThreadLocal<FocusFinder>() { 42 @Override 43 protected FocusFinder initialValue() { 44 return new FocusFinder(); 45 } 46 }; 47 48 /** 49 * Get the focus finder for this thread. 50 */ getInstance()51 public static FocusFinder getInstance() { 52 return tlFocusFinder.get(); 53 } 54 55 final Rect mFocusedRect = new Rect(); 56 final Rect mOtherRect = new Rect(); 57 final Rect mBestCandidateRect = new Rect(); 58 private final UserSpecifiedFocusComparator mUserSpecifiedFocusComparator = 59 new UserSpecifiedFocusComparator((r, v) -> isValidId(v.getNextFocusForwardId()) 60 ? v.findUserSetNextFocus(r, View.FOCUS_FORWARD) : null); 61 private final UserSpecifiedFocusComparator mUserSpecifiedClusterComparator = 62 new UserSpecifiedFocusComparator((r, v) -> isValidId(v.getNextClusterForwardId()) 63 ? v.findUserSetNextKeyboardNavigationCluster(r, View.FOCUS_FORWARD) : null); 64 private final FocusSorter mFocusSorter = new FocusSorter(); 65 66 private final ArrayList<View> mTempList = new ArrayList<View>(); 67 68 // enforce thread local access FocusFinder()69 private FocusFinder() {} 70 71 /** 72 * Find the next view to take focus in root's descendants, starting from the view 73 * that currently is focused. 74 * @param root Contains focused. Cannot be null. 75 * @param focused Has focus now. 76 * @param direction Direction to look. 77 * @return The next focusable view, or null if none exists. 78 */ findNextFocus(ViewGroup root, View focused, int direction)79 public final View findNextFocus(ViewGroup root, View focused, int direction) { 80 return findNextFocus(root, focused, null, direction); 81 } 82 83 /** 84 * Find the next view to take focus in root's descendants, searching from 85 * a particular rectangle in root's coordinates. 86 * @param root Contains focusedRect. Cannot be null. 87 * @param focusedRect The starting point of the search. 88 * @param direction Direction to look. 89 * @return The next focusable view, or null if none exists. 90 */ findNextFocusFromRect(ViewGroup root, Rect focusedRect, int direction)91 public View findNextFocusFromRect(ViewGroup root, Rect focusedRect, int direction) { 92 mFocusedRect.set(focusedRect); 93 return findNextFocus(root, null, mFocusedRect, direction); 94 } 95 findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction)96 private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) { 97 View next = null; 98 ViewGroup effectiveRoot = getEffectiveRoot(root, focused); 99 if (focused != null) { 100 next = findNextUserSpecifiedFocus(effectiveRoot, focused, direction); 101 } 102 if (next != null) { 103 return next; 104 } 105 ArrayList<View> focusables = mTempList; 106 try { 107 focusables.clear(); 108 effectiveRoot.addFocusables(focusables, direction); 109 if (!focusables.isEmpty()) { 110 next = findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables); 111 } 112 } finally { 113 focusables.clear(); 114 } 115 return next; 116 } 117 118 /** 119 * Returns the "effective" root of a view. The "effective" root is the closest ancestor 120 * within-which focus should cycle. 121 * <p> 122 * For example: normal focus navigation would stay within a ViewGroup marked as 123 * touchscreenBlocksFocus and keyboardNavigationCluster until a cluster-jump out. 124 * @return the "effective" root of {@param focused} 125 */ getEffectiveRoot(ViewGroup root, View focused)126 private ViewGroup getEffectiveRoot(ViewGroup root, View focused) { 127 if (focused == null || focused == root) { 128 return root; 129 } 130 ViewGroup effective = null; 131 ViewParent nextParent = focused.getParent(); 132 do { 133 if (nextParent == root) { 134 return effective != null ? effective : root; 135 } 136 ViewGroup vg = (ViewGroup) nextParent; 137 if (vg.getTouchscreenBlocksFocus() 138 && focused.getContext().getPackageManager().hasSystemFeature( 139 PackageManager.FEATURE_TOUCHSCREEN) 140 && vg.isKeyboardNavigationCluster()) { 141 // Don't stop and return here because the cluster could be nested and we only 142 // care about the top-most one. 143 effective = vg; 144 } 145 nextParent = nextParent.getParent(); 146 } while (nextParent instanceof ViewGroup); 147 return root; 148 } 149 150 /** 151 * Find the root of the next keyboard navigation cluster after the current one. 152 * @param root The view tree to look inside. Cannot be null 153 * @param currentCluster The starting point of the search. Null means the default cluster 154 * @param direction Direction to look 155 * @return The next cluster, or null if none exists 156 */ findNextKeyboardNavigationCluster( @onNull View root, @Nullable View currentCluster, @View.FocusDirection int direction)157 public View findNextKeyboardNavigationCluster( 158 @NonNull View root, 159 @Nullable View currentCluster, 160 @View.FocusDirection int direction) { 161 View next = null; 162 if (currentCluster != null) { 163 next = findNextUserSpecifiedKeyboardNavigationCluster(root, currentCluster, direction); 164 if (next != null) { 165 return next; 166 } 167 } 168 169 final ArrayList<View> clusters = mTempList; 170 try { 171 clusters.clear(); 172 root.addKeyboardNavigationClusters(clusters, direction); 173 if (!clusters.isEmpty()) { 174 next = findNextKeyboardNavigationCluster( 175 root, currentCluster, clusters, direction); 176 } 177 } finally { 178 clusters.clear(); 179 } 180 return next; 181 } 182 findNextUserSpecifiedKeyboardNavigationCluster(View root, View currentCluster, int direction)183 private View findNextUserSpecifiedKeyboardNavigationCluster(View root, View currentCluster, 184 int direction) { 185 View userSetNextCluster = 186 currentCluster.findUserSetNextKeyboardNavigationCluster(root, direction); 187 if (userSetNextCluster != null && userSetNextCluster.hasFocusable()) { 188 return userSetNextCluster; 189 } 190 return null; 191 } 192 findNextUserSpecifiedFocus(ViewGroup root, View focused, int direction)193 private View findNextUserSpecifiedFocus(ViewGroup root, View focused, int direction) { 194 // check for user specified next focus 195 View userSetNextFocus = focused.findUserSetNextFocus(root, direction); 196 while (userSetNextFocus != null) { 197 if (userSetNextFocus.isFocusable() 198 && userSetNextFocus.getVisibility() == View.VISIBLE 199 && (!userSetNextFocus.isInTouchMode() 200 || userSetNextFocus.isFocusableInTouchMode())) { 201 return userSetNextFocus; 202 } 203 userSetNextFocus = userSetNextFocus.findUserSetNextFocus(root, direction); 204 } 205 return null; 206 } 207 findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction, ArrayList<View> focusables)208 private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, 209 int direction, ArrayList<View> focusables) { 210 if (focused != null) { 211 if (focusedRect == null) { 212 focusedRect = mFocusedRect; 213 } 214 // fill in interesting rect from focused 215 focused.getFocusedRect(focusedRect); 216 root.offsetDescendantRectToMyCoords(focused, focusedRect); 217 } else { 218 if (focusedRect == null) { 219 focusedRect = mFocusedRect; 220 // make up a rect at top left or bottom right of root 221 switch (direction) { 222 case View.FOCUS_RIGHT: 223 case View.FOCUS_DOWN: 224 setFocusTopLeft(root, focusedRect); 225 break; 226 case View.FOCUS_FORWARD: 227 if (root.isLayoutRtl()) { 228 setFocusBottomRight(root, focusedRect); 229 } else { 230 setFocusTopLeft(root, focusedRect); 231 } 232 break; 233 234 case View.FOCUS_LEFT: 235 case View.FOCUS_UP: 236 setFocusBottomRight(root, focusedRect); 237 break; 238 case View.FOCUS_BACKWARD: 239 if (root.isLayoutRtl()) { 240 setFocusTopLeft(root, focusedRect); 241 } else { 242 setFocusBottomRight(root, focusedRect); 243 break; 244 } 245 } 246 } 247 } 248 249 switch (direction) { 250 case View.FOCUS_FORWARD: 251 case View.FOCUS_BACKWARD: 252 return findNextFocusInRelativeDirection(focusables, root, focused, focusedRect, 253 direction); 254 case View.FOCUS_UP: 255 case View.FOCUS_DOWN: 256 case View.FOCUS_LEFT: 257 case View.FOCUS_RIGHT: 258 return findNextFocusInAbsoluteDirection(focusables, root, focused, 259 focusedRect, direction); 260 default: 261 throw new IllegalArgumentException("Unknown direction: " + direction); 262 } 263 } 264 findNextKeyboardNavigationCluster( View root, View currentCluster, List<View> clusters, @View.FocusDirection int direction)265 private View findNextKeyboardNavigationCluster( 266 View root, 267 View currentCluster, 268 List<View> clusters, 269 @View.FocusDirection int direction) { 270 try { 271 // Note: This sort is stable. 272 mUserSpecifiedClusterComparator.setFocusables(clusters, root); 273 Collections.sort(clusters, mUserSpecifiedClusterComparator); 274 } finally { 275 mUserSpecifiedClusterComparator.recycle(); 276 } 277 final int count = clusters.size(); 278 279 switch (direction) { 280 case View.FOCUS_FORWARD: 281 case View.FOCUS_DOWN: 282 case View.FOCUS_RIGHT: 283 return getNextKeyboardNavigationCluster(root, currentCluster, clusters, count); 284 case View.FOCUS_BACKWARD: 285 case View.FOCUS_UP: 286 case View.FOCUS_LEFT: 287 return getPreviousKeyboardNavigationCluster(root, currentCluster, clusters, count); 288 default: 289 throw new IllegalArgumentException("Unknown direction: " + direction); 290 } 291 } 292 findNextFocusInRelativeDirection(ArrayList<View> focusables, ViewGroup root, View focused, Rect focusedRect, int direction)293 private View findNextFocusInRelativeDirection(ArrayList<View> focusables, ViewGroup root, 294 View focused, Rect focusedRect, int direction) { 295 try { 296 // Note: This sort is stable. 297 mUserSpecifiedFocusComparator.setFocusables(focusables, root); 298 Collections.sort(focusables, mUserSpecifiedFocusComparator); 299 } finally { 300 mUserSpecifiedFocusComparator.recycle(); 301 } 302 303 final int count = focusables.size(); 304 switch (direction) { 305 case View.FOCUS_FORWARD: 306 return getNextFocusable(focused, focusables, count); 307 case View.FOCUS_BACKWARD: 308 return getPreviousFocusable(focused, focusables, count); 309 } 310 return focusables.get(count - 1); 311 } 312 setFocusBottomRight(ViewGroup root, Rect focusedRect)313 private void setFocusBottomRight(ViewGroup root, Rect focusedRect) { 314 final int rootBottom = root.getScrollY() + root.getHeight(); 315 final int rootRight = root.getScrollX() + root.getWidth(); 316 focusedRect.set(rootRight, rootBottom, rootRight, rootBottom); 317 } 318 setFocusTopLeft(ViewGroup root, Rect focusedRect)319 private void setFocusTopLeft(ViewGroup root, Rect focusedRect) { 320 final int rootTop = root.getScrollY(); 321 final int rootLeft = root.getScrollX(); 322 focusedRect.set(rootLeft, rootTop, rootLeft, rootTop); 323 } 324 findNextFocusInAbsoluteDirection(ArrayList<View> focusables, ViewGroup root, View focused, Rect focusedRect, int direction)325 View findNextFocusInAbsoluteDirection(ArrayList<View> focusables, ViewGroup root, View focused, 326 Rect focusedRect, int direction) { 327 // initialize the best candidate to something impossible 328 // (so the first plausible view will become the best choice) 329 mBestCandidateRect.set(focusedRect); 330 switch(direction) { 331 case View.FOCUS_LEFT: 332 mBestCandidateRect.offset(focusedRect.width() + 1, 0); 333 break; 334 case View.FOCUS_RIGHT: 335 mBestCandidateRect.offset(-(focusedRect.width() + 1), 0); 336 break; 337 case View.FOCUS_UP: 338 mBestCandidateRect.offset(0, focusedRect.height() + 1); 339 break; 340 case View.FOCUS_DOWN: 341 mBestCandidateRect.offset(0, -(focusedRect.height() + 1)); 342 } 343 344 View closest = null; 345 346 int numFocusables = focusables.size(); 347 for (int i = 0; i < numFocusables; i++) { 348 View focusable = focusables.get(i); 349 350 // only interested in other non-root views 351 if (focusable == focused || focusable == root) continue; 352 353 // get focus bounds of other view in same coordinate system 354 focusable.getFocusedRect(mOtherRect); 355 root.offsetDescendantRectToMyCoords(focusable, mOtherRect); 356 357 if (isBetterCandidate(direction, focusedRect, mOtherRect, mBestCandidateRect)) { 358 mBestCandidateRect.set(mOtherRect); 359 closest = focusable; 360 } 361 } 362 return closest; 363 } 364 getNextFocusable(View focused, ArrayList<View> focusables, int count)365 private static View getNextFocusable(View focused, ArrayList<View> focusables, int count) { 366 if (focused != null) { 367 int position = focusables.lastIndexOf(focused); 368 if (position >= 0 && position + 1 < count) { 369 return focusables.get(position + 1); 370 } 371 } 372 if (!focusables.isEmpty()) { 373 return focusables.get(0); 374 } 375 return null; 376 } 377 getPreviousFocusable(View focused, ArrayList<View> focusables, int count)378 private static View getPreviousFocusable(View focused, ArrayList<View> focusables, int count) { 379 if (focused != null) { 380 int position = focusables.indexOf(focused); 381 if (position > 0) { 382 return focusables.get(position - 1); 383 } 384 } 385 if (!focusables.isEmpty()) { 386 return focusables.get(count - 1); 387 } 388 return null; 389 } 390 getNextKeyboardNavigationCluster( View root, View currentCluster, List<View> clusters, int count)391 private static View getNextKeyboardNavigationCluster( 392 View root, 393 View currentCluster, 394 List<View> clusters, 395 int count) { 396 if (currentCluster == null) { 397 // The current cluster is the default one. 398 // The next cluster after the default one is the first one. 399 // Note that the caller guarantees that 'clusters' is not empty. 400 return clusters.get(0); 401 } 402 403 final int position = clusters.lastIndexOf(currentCluster); 404 if (position >= 0 && position + 1 < count) { 405 // Return the next non-default cluster if we can find it. 406 return clusters.get(position + 1); 407 } 408 409 // The current cluster is the last one. The next one is the default one, i.e. the 410 // root. 411 return root; 412 } 413 getPreviousKeyboardNavigationCluster( View root, View currentCluster, List<View> clusters, int count)414 private static View getPreviousKeyboardNavigationCluster( 415 View root, 416 View currentCluster, 417 List<View> clusters, 418 int count) { 419 if (currentCluster == null) { 420 // The current cluster is the default one. 421 // The previous cluster before the default one is the last one. 422 // Note that the caller guarantees that 'clusters' is not empty. 423 return clusters.get(count - 1); 424 } 425 426 final int position = clusters.indexOf(currentCluster); 427 if (position > 0) { 428 // Return the previous non-default cluster if we can find it. 429 return clusters.get(position - 1); 430 } 431 432 // The current cluster is the first one. The previous one is the default one, i.e. 433 // the root. 434 return root; 435 } 436 437 /** 438 * Is rect1 a better candidate than rect2 for a focus search in a particular 439 * direction from a source rect? This is the core routine that determines 440 * the order of focus searching. 441 * @param direction the direction (up, down, left, right) 442 * @param source The source we are searching from 443 * @param rect1 The candidate rectangle 444 * @param rect2 The current best candidate. 445 * @return Whether the candidate is the new best. 446 */ isBetterCandidate(int direction, Rect source, Rect rect1, Rect rect2)447 boolean isBetterCandidate(int direction, Rect source, Rect rect1, Rect rect2) { 448 449 // to be a better candidate, need to at least be a candidate in the first 450 // place :) 451 if (!isCandidate(source, rect1, direction)) { 452 return false; 453 } 454 455 // we know that rect1 is a candidate.. if rect2 is not a candidate, 456 // rect1 is better 457 if (!isCandidate(source, rect2, direction)) { 458 return true; 459 } 460 461 // if rect1 is better by beam, it wins 462 if (beamBeats(direction, source, rect1, rect2)) { 463 return true; 464 } 465 466 // if rect2 is better, then rect1 cant' be :) 467 if (beamBeats(direction, source, rect2, rect1)) { 468 return false; 469 } 470 471 // otherwise, do fudge-tastic comparison of the major and minor axis 472 return (getWeightedDistanceFor( 473 majorAxisDistance(direction, source, rect1), 474 minorAxisDistance(direction, source, rect1)) 475 < getWeightedDistanceFor( 476 majorAxisDistance(direction, source, rect2), 477 minorAxisDistance(direction, source, rect2))); 478 } 479 480 /** 481 * One rectangle may be another candidate than another by virtue of being 482 * exclusively in the beam of the source rect. 483 * @return Whether rect1 is a better candidate than rect2 by virtue of it being in src's 484 * beam 485 */ beamBeats(int direction, Rect source, Rect rect1, Rect rect2)486 boolean beamBeats(int direction, Rect source, Rect rect1, Rect rect2) { 487 final boolean rect1InSrcBeam = beamsOverlap(direction, source, rect1); 488 final boolean rect2InSrcBeam = beamsOverlap(direction, source, rect2); 489 490 // if rect1 isn't exclusively in the src beam, it doesn't win 491 if (rect2InSrcBeam || !rect1InSrcBeam) { 492 return false; 493 } 494 495 // we know rect1 is in the beam, and rect2 is not 496 497 // if rect1 is to the direction of, and rect2 is not, rect1 wins. 498 // for example, for direction left, if rect1 is to the left of the source 499 // and rect2 is below, then we always prefer the in beam rect1, since rect2 500 // could be reached by going down. 501 if (!isToDirectionOf(direction, source, rect2)) { 502 return true; 503 } 504 505 // for horizontal directions, being exclusively in beam always wins 506 if ((direction == View.FOCUS_LEFT || direction == View.FOCUS_RIGHT)) { 507 return true; 508 } 509 510 // for vertical directions, beams only beat up to a point: 511 // now, as long as rect2 isn't completely closer, rect1 wins 512 // e.g for direction down, completely closer means for rect2's top 513 // edge to be closer to the source's top edge than rect1's bottom edge. 514 return (majorAxisDistance(direction, source, rect1) 515 < majorAxisDistanceToFarEdge(direction, source, rect2)); 516 } 517 518 /** 519 * Fudge-factor opportunity: how to calculate distance given major and minor 520 * axis distances. Warning: this fudge factor is finely tuned, be sure to 521 * run all focus tests if you dare tweak it. 522 */ getWeightedDistanceFor(int majorAxisDistance, int minorAxisDistance)523 int getWeightedDistanceFor(int majorAxisDistance, int minorAxisDistance) { 524 return 13 * majorAxisDistance * majorAxisDistance 525 + minorAxisDistance * minorAxisDistance; 526 } 527 528 /** 529 * Is destRect a candidate for the next focus given the direction? This 530 * checks whether the dest is at least partially to the direction of (e.g left of) 531 * from source. 532 * 533 * Includes an edge case for an empty rect (which is used in some cases when 534 * searching from a point on the screen). 535 */ isCandidate(Rect srcRect, Rect destRect, int direction)536 boolean isCandidate(Rect srcRect, Rect destRect, int direction) { 537 switch (direction) { 538 case View.FOCUS_LEFT: 539 return (srcRect.right > destRect.right || srcRect.left >= destRect.right) 540 && srcRect.left > destRect.left; 541 case View.FOCUS_RIGHT: 542 return (srcRect.left < destRect.left || srcRect.right <= destRect.left) 543 && srcRect.right < destRect.right; 544 case View.FOCUS_UP: 545 return (srcRect.bottom > destRect.bottom || srcRect.top >= destRect.bottom) 546 && srcRect.top > destRect.top; 547 case View.FOCUS_DOWN: 548 return (srcRect.top < destRect.top || srcRect.bottom <= destRect.top) 549 && srcRect.bottom < destRect.bottom; 550 } 551 throw new IllegalArgumentException("direction must be one of " 552 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); 553 } 554 555 556 /** 557 * Do the "beams" w.r.t the given direction's axis of rect1 and rect2 overlap? 558 * @param direction the direction (up, down, left, right) 559 * @param rect1 The first rectangle 560 * @param rect2 The second rectangle 561 * @return whether the beams overlap 562 */ beamsOverlap(int direction, Rect rect1, Rect rect2)563 boolean beamsOverlap(int direction, Rect rect1, Rect rect2) { 564 switch (direction) { 565 case View.FOCUS_LEFT: 566 case View.FOCUS_RIGHT: 567 return (rect2.bottom >= rect1.top) && (rect2.top <= rect1.bottom); 568 case View.FOCUS_UP: 569 case View.FOCUS_DOWN: 570 return (rect2.right >= rect1.left) && (rect2.left <= rect1.right); 571 } 572 throw new IllegalArgumentException("direction must be one of " 573 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); 574 } 575 576 /** 577 * e.g for left, is 'to left of' 578 */ isToDirectionOf(int direction, Rect src, Rect dest)579 boolean isToDirectionOf(int direction, Rect src, Rect dest) { 580 switch (direction) { 581 case View.FOCUS_LEFT: 582 return src.left >= dest.right; 583 case View.FOCUS_RIGHT: 584 return src.right <= dest.left; 585 case View.FOCUS_UP: 586 return src.top >= dest.bottom; 587 case View.FOCUS_DOWN: 588 return src.bottom <= dest.top; 589 } 590 throw new IllegalArgumentException("direction must be one of " 591 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); 592 } 593 594 /** 595 * @return The distance from the edge furthest in the given direction 596 * of source to the edge nearest in the given direction of dest. If the 597 * dest is not in the direction from source, return 0. 598 */ majorAxisDistance(int direction, Rect source, Rect dest)599 static int majorAxisDistance(int direction, Rect source, Rect dest) { 600 return Math.max(0, majorAxisDistanceRaw(direction, source, dest)); 601 } 602 majorAxisDistanceRaw(int direction, Rect source, Rect dest)603 static int majorAxisDistanceRaw(int direction, Rect source, Rect dest) { 604 switch (direction) { 605 case View.FOCUS_LEFT: 606 return source.left - dest.right; 607 case View.FOCUS_RIGHT: 608 return dest.left - source.right; 609 case View.FOCUS_UP: 610 return source.top - dest.bottom; 611 case View.FOCUS_DOWN: 612 return dest.top - source.bottom; 613 } 614 throw new IllegalArgumentException("direction must be one of " 615 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); 616 } 617 618 /** 619 * @return The distance along the major axis w.r.t the direction from the 620 * edge of source to the far edge of dest. If the 621 * dest is not in the direction from source, return 1 (to break ties with 622 * {@link #majorAxisDistance}). 623 */ majorAxisDistanceToFarEdge(int direction, Rect source, Rect dest)624 static int majorAxisDistanceToFarEdge(int direction, Rect source, Rect dest) { 625 return Math.max(1, majorAxisDistanceToFarEdgeRaw(direction, source, dest)); 626 } 627 majorAxisDistanceToFarEdgeRaw(int direction, Rect source, Rect dest)628 static int majorAxisDistanceToFarEdgeRaw(int direction, Rect source, Rect dest) { 629 switch (direction) { 630 case View.FOCUS_LEFT: 631 return source.left - dest.left; 632 case View.FOCUS_RIGHT: 633 return dest.right - source.right; 634 case View.FOCUS_UP: 635 return source.top - dest.top; 636 case View.FOCUS_DOWN: 637 return dest.bottom - source.bottom; 638 } 639 throw new IllegalArgumentException("direction must be one of " 640 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); 641 } 642 643 /** 644 * Find the distance on the minor axis w.r.t the direction to the nearest 645 * edge of the destination rectangle. 646 * @param direction the direction (up, down, left, right) 647 * @param source The source rect. 648 * @param dest The destination rect. 649 * @return The distance. 650 */ minorAxisDistance(int direction, Rect source, Rect dest)651 static int minorAxisDistance(int direction, Rect source, Rect dest) { 652 switch (direction) { 653 case View.FOCUS_LEFT: 654 case View.FOCUS_RIGHT: 655 // the distance between the center verticals 656 return Math.abs( 657 ((source.top + source.height() / 2) - 658 ((dest.top + dest.height() / 2)))); 659 case View.FOCUS_UP: 660 case View.FOCUS_DOWN: 661 // the distance between the center horizontals 662 return Math.abs( 663 ((source.left + source.width() / 2) - 664 ((dest.left + dest.width() / 2)))); 665 } 666 throw new IllegalArgumentException("direction must be one of " 667 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); 668 } 669 670 /** 671 * Find the nearest touchable view to the specified view. 672 * 673 * @param root The root of the tree in which to search 674 * @param x X coordinate from which to start the search 675 * @param y Y coordinate from which to start the search 676 * @param direction Direction to look 677 * @param deltas Offset from the <x, y> to the edge of the nearest view. Note that this array 678 * may already be populated with values. 679 * @return The nearest touchable view, or null if none exists. 680 */ findNearestTouchable(ViewGroup root, int x, int y, int direction, int[] deltas)681 public View findNearestTouchable(ViewGroup root, int x, int y, int direction, int[] deltas) { 682 ArrayList<View> touchables = root.getTouchables(); 683 int minDistance = Integer.MAX_VALUE; 684 View closest = null; 685 686 int numTouchables = touchables.size(); 687 688 int edgeSlop = ViewConfiguration.get(root.mContext).getScaledEdgeSlop(); 689 690 Rect closestBounds = new Rect(); 691 Rect touchableBounds = mOtherRect; 692 693 for (int i = 0; i < numTouchables; i++) { 694 View touchable = touchables.get(i); 695 696 // get visible bounds of other view in same coordinate system 697 touchable.getDrawingRect(touchableBounds); 698 699 root.offsetRectBetweenParentAndChild(touchable, touchableBounds, true, true); 700 701 if (!isTouchCandidate(x, y, touchableBounds, direction)) { 702 continue; 703 } 704 705 int distance = Integer.MAX_VALUE; 706 707 switch (direction) { 708 case View.FOCUS_LEFT: 709 distance = x - touchableBounds.right + 1; 710 break; 711 case View.FOCUS_RIGHT: 712 distance = touchableBounds.left; 713 break; 714 case View.FOCUS_UP: 715 distance = y - touchableBounds.bottom + 1; 716 break; 717 case View.FOCUS_DOWN: 718 distance = touchableBounds.top; 719 break; 720 } 721 722 if (distance < edgeSlop) { 723 // Give preference to innermost views 724 if (closest == null || 725 closestBounds.contains(touchableBounds) || 726 (!touchableBounds.contains(closestBounds) && distance < minDistance)) { 727 minDistance = distance; 728 closest = touchable; 729 closestBounds.set(touchableBounds); 730 switch (direction) { 731 case View.FOCUS_LEFT: 732 deltas[0] = -distance; 733 break; 734 case View.FOCUS_RIGHT: 735 deltas[0] = distance; 736 break; 737 case View.FOCUS_UP: 738 deltas[1] = -distance; 739 break; 740 case View.FOCUS_DOWN: 741 deltas[1] = distance; 742 break; 743 } 744 } 745 } 746 } 747 return closest; 748 } 749 750 751 /** 752 * Is destRect a candidate for the next touch given the direction? 753 */ isTouchCandidate(int x, int y, Rect destRect, int direction)754 private boolean isTouchCandidate(int x, int y, Rect destRect, int direction) { 755 switch (direction) { 756 case View.FOCUS_LEFT: 757 return destRect.left <= x && destRect.top <= y && y <= destRect.bottom; 758 case View.FOCUS_RIGHT: 759 return destRect.left >= x && destRect.top <= y && y <= destRect.bottom; 760 case View.FOCUS_UP: 761 return destRect.top <= y && destRect.left <= x && x <= destRect.right; 762 case View.FOCUS_DOWN: 763 return destRect.top >= y && destRect.left <= x && x <= destRect.right; 764 } 765 throw new IllegalArgumentException("direction must be one of " 766 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); 767 } 768 isValidId(final int id)769 private static final boolean isValidId(final int id) { 770 return id != 0 && id != View.NO_ID; 771 } 772 773 static final class FocusSorter { 774 private ArrayList<Rect> mRectPool = new ArrayList<>(); 775 private int mLastPoolRect; 776 private int mRtlMult; 777 private HashMap<View, Rect> mRectByView = null; 778 779 private Comparator<View> mTopsComparator = (first, second) -> { 780 if (first == second) { 781 return 0; 782 } 783 784 Rect firstRect = mRectByView.get(first); 785 Rect secondRect = mRectByView.get(second); 786 787 int result = firstRect.top - secondRect.top; 788 if (result == 0) { 789 return firstRect.bottom - secondRect.bottom; 790 } 791 return result; 792 }; 793 794 private Comparator<View> mSidesComparator = (first, second) -> { 795 if (first == second) { 796 return 0; 797 } 798 799 Rect firstRect = mRectByView.get(first); 800 Rect secondRect = mRectByView.get(second); 801 802 int result = firstRect.left - secondRect.left; 803 if (result == 0) { 804 return firstRect.right - secondRect.right; 805 } 806 return mRtlMult * result; 807 }; 808 sort(View[] views, int start, int end, ViewGroup root, boolean isRtl)809 public void sort(View[] views, int start, int end, ViewGroup root, boolean isRtl) { 810 int count = end - start; 811 if (count < 2) { 812 return; 813 } 814 if (mRectByView == null) { 815 mRectByView = new HashMap<>(); 816 } 817 mRtlMult = isRtl ? -1 : 1; 818 for (int i = mRectPool.size(); i < count; ++i) { 819 mRectPool.add(new Rect()); 820 } 821 for (int i = start; i < end; ++i) { 822 Rect next = mRectPool.get(mLastPoolRect++); 823 views[i].getDrawingRect(next); 824 root.offsetDescendantRectToMyCoords(views[i], next); 825 mRectByView.put(views[i], next); 826 } 827 828 // Sort top-to-bottom 829 Arrays.sort(views, start, count, mTopsComparator); 830 // Sweep top-to-bottom to identify rows 831 int sweepBottom = mRectByView.get(views[start]).bottom; 832 int rowStart = start; 833 int sweepIdx = start + 1; 834 for (; sweepIdx < end; ++sweepIdx) { 835 Rect currRect = mRectByView.get(views[sweepIdx]); 836 if (currRect.top >= sweepBottom) { 837 // Next view is on a new row, sort the row we've just finished left-to-right. 838 if ((sweepIdx - rowStart) > 1) { 839 Arrays.sort(views, rowStart, sweepIdx, mSidesComparator); 840 } 841 sweepBottom = currRect.bottom; 842 rowStart = sweepIdx; 843 } else { 844 // Next view vertically overlaps, we need to extend our "row height" 845 sweepBottom = Math.max(sweepBottom, currRect.bottom); 846 } 847 } 848 // Sort whatever's left (final row) left-to-right 849 if ((sweepIdx - rowStart) > 1) { 850 Arrays.sort(views, rowStart, sweepIdx, mSidesComparator); 851 } 852 853 mLastPoolRect = 0; 854 mRectByView.clear(); 855 } 856 } 857 858 /** 859 * Public for testing. 860 * 861 * @hide 862 */ 863 @TestApi sort(View[] views, int start, int end, ViewGroup root, boolean isRtl)864 public static void sort(View[] views, int start, int end, ViewGroup root, boolean isRtl) { 865 getInstance().mFocusSorter.sort(views, start, end, root, isRtl); 866 } 867 868 /** 869 * Sorts views according to any explicitly-specified focus-chains. If there are no explicitly 870 * specified focus chains (eg. no nextFocusForward attributes defined), this should be a no-op. 871 */ 872 private static final class UserSpecifiedFocusComparator implements Comparator<View> { 873 private final ArrayMap<View, View> mNextFoci = new ArrayMap<>(); 874 private final ArraySet<View> mIsConnectedTo = new ArraySet<>(); 875 private final ArrayMap<View, View> mHeadsOfChains = new ArrayMap<View, View>(); 876 private final ArrayMap<View, Integer> mOriginalOrdinal = new ArrayMap<>(); 877 private final NextFocusGetter mNextFocusGetter; 878 private View mRoot; 879 880 public interface NextFocusGetter { get(View root, View view)881 View get(View root, View view); 882 } 883 UserSpecifiedFocusComparator(NextFocusGetter nextFocusGetter)884 UserSpecifiedFocusComparator(NextFocusGetter nextFocusGetter) { 885 mNextFocusGetter = nextFocusGetter; 886 } 887 recycle()888 public void recycle() { 889 mRoot = null; 890 mHeadsOfChains.clear(); 891 mIsConnectedTo.clear(); 892 mOriginalOrdinal.clear(); 893 mNextFoci.clear(); 894 } 895 setFocusables(List<View> focusables, View root)896 public void setFocusables(List<View> focusables, View root) { 897 mRoot = root; 898 for (int i = 0; i < focusables.size(); ++i) { 899 mOriginalOrdinal.put(focusables.get(i), i); 900 } 901 902 for (int i = focusables.size() - 1; i >= 0; i--) { 903 final View view = focusables.get(i); 904 final View next = mNextFocusGetter.get(mRoot, view); 905 if (next != null && mOriginalOrdinal.containsKey(next)) { 906 mNextFoci.put(view, next); 907 mIsConnectedTo.add(next); 908 } 909 } 910 911 for (int i = focusables.size() - 1; i >= 0; i--) { 912 final View view = focusables.get(i); 913 final View next = mNextFoci.get(view); 914 if (next != null && !mIsConnectedTo.contains(view)) { 915 setHeadOfChain(view); 916 } 917 } 918 } 919 setHeadOfChain(View head)920 private void setHeadOfChain(View head) { 921 for (View view = head; view != null; view = mNextFoci.get(view)) { 922 final View otherHead = mHeadsOfChains.get(view); 923 if (otherHead != null) { 924 if (otherHead == head) { 925 return; // This view has already had its head set properly 926 } 927 // A hydra -- multi-headed focus chain (e.g. A->C and B->C) 928 // Use the one we've already chosen instead and reset this chain. 929 view = head; 930 head = otherHead; 931 } 932 mHeadsOfChains.put(view, head); 933 } 934 } 935 compare(View first, View second)936 public int compare(View first, View second) { 937 if (first == second) { 938 return 0; 939 } 940 // Order between views within a chain is immaterial -- next/previous is 941 // within a chain is handled elsewhere. 942 View firstHead = mHeadsOfChains.get(first); 943 View secondHead = mHeadsOfChains.get(second); 944 if (firstHead == secondHead && firstHead != null) { 945 if (first == firstHead) { 946 return -1; // first is the head, it should be first 947 } else if (second == firstHead) { 948 return 1; // second is the head, it should be first 949 } else if (mNextFoci.get(first) != null) { 950 return -1; // first is not the end of the chain 951 } else { 952 return 1; // first is end of chain 953 } 954 } 955 boolean involvesChain = false; 956 if (firstHead != null) { 957 first = firstHead; 958 involvesChain = true; 959 } 960 if (secondHead != null) { 961 second = secondHead; 962 involvesChain = true; 963 } 964 965 if (involvesChain) { 966 // keep original order between chains 967 return mOriginalOrdinal.get(first) < mOriginalOrdinal.get(second) ? -1 : 1; 968 } else { 969 return 0; 970 } 971 } 972 } 973 } 974