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 while (nextParent instanceof ViewGroup) { 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 } 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 View cycleCheck = userSetNextFocus; 197 boolean cycleStep = true; // we want the first toggle to yield false 198 while (userSetNextFocus != null) { 199 if (userSetNextFocus.isFocusable() 200 && userSetNextFocus.getVisibility() == View.VISIBLE 201 && (!userSetNextFocus.isInTouchMode() 202 || userSetNextFocus.isFocusableInTouchMode())) { 203 return userSetNextFocus; 204 } 205 userSetNextFocus = userSetNextFocus.findUserSetNextFocus(root, direction); 206 if (cycleStep = !cycleStep) { 207 cycleCheck = cycleCheck.findUserSetNextFocus(root, direction); 208 if (cycleCheck == userSetNextFocus) { 209 // found a cycle, user-specified focus forms a loop and none of the views 210 // are currently focusable. 211 break; 212 } 213 } 214 } 215 return null; 216 } 217 findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction, ArrayList<View> focusables)218 private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, 219 int direction, ArrayList<View> focusables) { 220 if (focused != null) { 221 if (focusedRect == null) { 222 focusedRect = mFocusedRect; 223 } 224 // fill in interesting rect from focused 225 focused.getFocusedRect(focusedRect); 226 root.offsetDescendantRectToMyCoords(focused, focusedRect); 227 } else { 228 if (focusedRect == null) { 229 focusedRect = mFocusedRect; 230 // make up a rect at top left or bottom right of root 231 switch (direction) { 232 case View.FOCUS_RIGHT: 233 case View.FOCUS_DOWN: 234 setFocusTopLeft(root, focusedRect); 235 break; 236 case View.FOCUS_FORWARD: 237 if (root.isLayoutRtl()) { 238 setFocusBottomRight(root, focusedRect); 239 } else { 240 setFocusTopLeft(root, focusedRect); 241 } 242 break; 243 244 case View.FOCUS_LEFT: 245 case View.FOCUS_UP: 246 setFocusBottomRight(root, focusedRect); 247 break; 248 case View.FOCUS_BACKWARD: 249 if (root.isLayoutRtl()) { 250 setFocusTopLeft(root, focusedRect); 251 } else { 252 setFocusBottomRight(root, focusedRect); 253 break; 254 } 255 } 256 } 257 } 258 259 switch (direction) { 260 case View.FOCUS_FORWARD: 261 case View.FOCUS_BACKWARD: 262 return findNextFocusInRelativeDirection(focusables, root, focused, focusedRect, 263 direction); 264 case View.FOCUS_UP: 265 case View.FOCUS_DOWN: 266 case View.FOCUS_LEFT: 267 case View.FOCUS_RIGHT: 268 return findNextFocusInAbsoluteDirection(focusables, root, focused, 269 focusedRect, direction); 270 default: 271 throw new IllegalArgumentException("Unknown direction: " + direction); 272 } 273 } 274 findNextKeyboardNavigationCluster( View root, View currentCluster, List<View> clusters, @View.FocusDirection int direction)275 private View findNextKeyboardNavigationCluster( 276 View root, 277 View currentCluster, 278 List<View> clusters, 279 @View.FocusDirection int direction) { 280 try { 281 // Note: This sort is stable. 282 mUserSpecifiedClusterComparator.setFocusables(clusters, root); 283 Collections.sort(clusters, mUserSpecifiedClusterComparator); 284 } finally { 285 mUserSpecifiedClusterComparator.recycle(); 286 } 287 final int count = clusters.size(); 288 289 switch (direction) { 290 case View.FOCUS_FORWARD: 291 case View.FOCUS_DOWN: 292 case View.FOCUS_RIGHT: 293 return getNextKeyboardNavigationCluster(root, currentCluster, clusters, count); 294 case View.FOCUS_BACKWARD: 295 case View.FOCUS_UP: 296 case View.FOCUS_LEFT: 297 return getPreviousKeyboardNavigationCluster(root, currentCluster, clusters, count); 298 default: 299 throw new IllegalArgumentException("Unknown direction: " + direction); 300 } 301 } 302 findNextFocusInRelativeDirection(ArrayList<View> focusables, ViewGroup root, View focused, Rect focusedRect, int direction)303 private View findNextFocusInRelativeDirection(ArrayList<View> focusables, ViewGroup root, 304 View focused, Rect focusedRect, int direction) { 305 try { 306 // Note: This sort is stable. 307 mUserSpecifiedFocusComparator.setFocusables(focusables, root); 308 Collections.sort(focusables, mUserSpecifiedFocusComparator); 309 } finally { 310 mUserSpecifiedFocusComparator.recycle(); 311 } 312 313 final int count = focusables.size(); 314 if (count < 2) { 315 return null; 316 } 317 View next = null; 318 final boolean[] looped = new boolean[1]; 319 switch (direction) { 320 case View.FOCUS_FORWARD: 321 next = getNextFocusable(focused, focusables, count, looped); 322 break; 323 case View.FOCUS_BACKWARD: 324 next = getPreviousFocusable(focused, focusables, count, looped); 325 break; 326 } 327 if (root != null && root.mAttachInfo != null && root == root.getRootView()) { 328 root.mAttachInfo.mNextFocusLooped = looped[0]; 329 } 330 return next != null ? next : focusables.get(count - 1); 331 } 332 setFocusBottomRight(ViewGroup root, Rect focusedRect)333 private void setFocusBottomRight(ViewGroup root, Rect focusedRect) { 334 final int rootBottom = root.getScrollY() + root.getHeight(); 335 final int rootRight = root.getScrollX() + root.getWidth(); 336 focusedRect.set(rootRight, rootBottom, rootRight, rootBottom); 337 } 338 setFocusTopLeft(ViewGroup root, Rect focusedRect)339 private void setFocusTopLeft(ViewGroup root, Rect focusedRect) { 340 final int rootTop = root.getScrollY(); 341 final int rootLeft = root.getScrollX(); 342 focusedRect.set(rootLeft, rootTop, rootLeft, rootTop); 343 } 344 findNextFocusInAbsoluteDirection(ArrayList<View> focusables, ViewGroup root, View focused, Rect focusedRect, int direction)345 View findNextFocusInAbsoluteDirection(ArrayList<View> focusables, ViewGroup root, View focused, 346 Rect focusedRect, int direction) { 347 // initialize the best candidate to something impossible 348 // (so the first plausible view will become the best choice) 349 mBestCandidateRect.set(focusedRect); 350 switch(direction) { 351 case View.FOCUS_LEFT: 352 mBestCandidateRect.offset(focusedRect.width() + 1, 0); 353 break; 354 case View.FOCUS_RIGHT: 355 mBestCandidateRect.offset(-(focusedRect.width() + 1), 0); 356 break; 357 case View.FOCUS_UP: 358 mBestCandidateRect.offset(0, focusedRect.height() + 1); 359 break; 360 case View.FOCUS_DOWN: 361 mBestCandidateRect.offset(0, -(focusedRect.height() + 1)); 362 } 363 364 View closest = null; 365 366 int numFocusables = focusables.size(); 367 for (int i = 0; i < numFocusables; i++) { 368 View focusable = focusables.get(i); 369 370 // only interested in other non-root views 371 if (focusable == focused || focusable == root) continue; 372 373 // get focus bounds of other view in same coordinate system 374 focusable.getFocusedRect(mOtherRect); 375 root.offsetDescendantRectToMyCoords(focusable, mOtherRect); 376 377 if (isBetterCandidate(direction, focusedRect, mOtherRect, mBestCandidateRect)) { 378 mBestCandidateRect.set(mOtherRect); 379 closest = focusable; 380 } 381 } 382 return closest; 383 } 384 getNextFocusable(View focused, ArrayList<View> focusables, int count, boolean[] outLooped)385 private static View getNextFocusable(View focused, ArrayList<View> focusables, int count, 386 boolean[] outLooped) { 387 if (count < 2) { 388 return null; 389 } 390 if (focused != null) { 391 int position = focusables.lastIndexOf(focused); 392 if (position >= 0 && position + 1 < count) { 393 return focusables.get(position + 1); 394 } 395 } 396 outLooped[0] = true; 397 return focusables.get(0); 398 } 399 getPreviousFocusable(View focused, ArrayList<View> focusables, int count, boolean[] outLooped)400 private static View getPreviousFocusable(View focused, ArrayList<View> focusables, int count, 401 boolean[] outLooped) { 402 if (count < 2) { 403 return null; 404 } 405 if (focused != null) { 406 int position = focusables.indexOf(focused); 407 if (position > 0) { 408 return focusables.get(position - 1); 409 } 410 } 411 outLooped[0] = true; 412 return focusables.get(count - 1); 413 } 414 getNextKeyboardNavigationCluster( View root, View currentCluster, List<View> clusters, int count)415 private static View getNextKeyboardNavigationCluster( 416 View root, 417 View currentCluster, 418 List<View> clusters, 419 int count) { 420 if (currentCluster == null) { 421 // The current cluster is the default one. 422 // The next cluster after the default one is the first one. 423 // Note that the caller guarantees that 'clusters' is not empty. 424 return clusters.get(0); 425 } 426 427 final int position = clusters.lastIndexOf(currentCluster); 428 if (position >= 0 && position + 1 < count) { 429 // Return the next non-default cluster if we can find it. 430 return clusters.get(position + 1); 431 } 432 433 // The current cluster is the last one. The next one is the default one, i.e. the 434 // root. 435 return root; 436 } 437 getPreviousKeyboardNavigationCluster( View root, View currentCluster, List<View> clusters, int count)438 private static View getPreviousKeyboardNavigationCluster( 439 View root, 440 View currentCluster, 441 List<View> clusters, 442 int count) { 443 if (currentCluster == null) { 444 // The current cluster is the default one. 445 // The previous cluster before the default one is the last one. 446 // Note that the caller guarantees that 'clusters' is not empty. 447 return clusters.get(count - 1); 448 } 449 450 final int position = clusters.indexOf(currentCluster); 451 if (position > 0) { 452 // Return the previous non-default cluster if we can find it. 453 return clusters.get(position - 1); 454 } 455 456 // The current cluster is the first one. The previous one is the default one, i.e. 457 // the root. 458 return root; 459 } 460 461 /** 462 * Is rect1 a better candidate than rect2 for a focus search in a particular 463 * direction from a source rect? This is the core routine that determines 464 * the order of focus searching. 465 * @param direction the direction (up, down, left, right) 466 * @param source The source we are searching from 467 * @param rect1 The candidate rectangle 468 * @param rect2 The current best candidate. 469 * @return Whether the candidate is the new best. 470 */ isBetterCandidate(int direction, Rect source, Rect rect1, Rect rect2)471 boolean isBetterCandidate(int direction, Rect source, Rect rect1, Rect rect2) { 472 473 // to be a better candidate, need to at least be a candidate in the first 474 // place :) 475 if (!isCandidate(source, rect1, direction)) { 476 return false; 477 } 478 479 // we know that rect1 is a candidate.. if rect2 is not a candidate, 480 // rect1 is better 481 if (!isCandidate(source, rect2, direction)) { 482 return true; 483 } 484 485 // if rect1 is better by beam, it wins 486 if (beamBeats(direction, source, rect1, rect2)) { 487 return true; 488 } 489 490 // if rect2 is better, then rect1 cant' be :) 491 if (beamBeats(direction, source, rect2, rect1)) { 492 return false; 493 } 494 495 // otherwise, do fudge-tastic comparison of the major and minor axis 496 return (getWeightedDistanceFor( 497 majorAxisDistance(direction, source, rect1), 498 minorAxisDistance(direction, source, rect1)) 499 < getWeightedDistanceFor( 500 majorAxisDistance(direction, source, rect2), 501 minorAxisDistance(direction, source, rect2))); 502 } 503 504 /** 505 * One rectangle may be another candidate than another by virtue of being 506 * exclusively in the beam of the source rect. 507 * @return Whether rect1 is a better candidate than rect2 by virtue of it being in src's 508 * beam 509 */ beamBeats(int direction, Rect source, Rect rect1, Rect rect2)510 boolean beamBeats(int direction, Rect source, Rect rect1, Rect rect2) { 511 final boolean rect1InSrcBeam = beamsOverlap(direction, source, rect1); 512 final boolean rect2InSrcBeam = beamsOverlap(direction, source, rect2); 513 514 // if rect1 isn't exclusively in the src beam, it doesn't win 515 if (rect2InSrcBeam || !rect1InSrcBeam) { 516 return false; 517 } 518 519 // we know rect1 is in the beam, and rect2 is not 520 521 // if rect1 is to the direction of, and rect2 is not, rect1 wins. 522 // for example, for direction left, if rect1 is to the left of the source 523 // and rect2 is below, then we always prefer the in beam rect1, since rect2 524 // could be reached by going down. 525 if (!isToDirectionOf(direction, source, rect2)) { 526 return true; 527 } 528 529 // for horizontal directions, being exclusively in beam always wins 530 if ((direction == View.FOCUS_LEFT || direction == View.FOCUS_RIGHT)) { 531 return true; 532 } 533 534 // for vertical directions, beams only beat up to a point: 535 // now, as long as rect2 isn't completely closer, rect1 wins 536 // e.g for direction down, completely closer means for rect2's top 537 // edge to be closer to the source's top edge than rect1's bottom edge. 538 return (majorAxisDistance(direction, source, rect1) 539 < majorAxisDistanceToFarEdge(direction, source, rect2)); 540 } 541 542 /** 543 * Fudge-factor opportunity: how to calculate distance given major and minor 544 * axis distances. Warning: this fudge factor is finely tuned, be sure to 545 * run all focus tests if you dare tweak it. 546 */ getWeightedDistanceFor(long majorAxisDistance, long minorAxisDistance)547 long getWeightedDistanceFor(long majorAxisDistance, long minorAxisDistance) { 548 return 13 * majorAxisDistance * majorAxisDistance 549 + minorAxisDistance * minorAxisDistance; 550 } 551 552 /** 553 * Is destRect a candidate for the next focus given the direction? This 554 * checks whether the dest is at least partially to the direction of (e.g left of) 555 * from source. 556 * 557 * Includes an edge case for an empty rect (which is used in some cases when 558 * searching from a point on the screen). 559 */ isCandidate(Rect srcRect, Rect destRect, int direction)560 boolean isCandidate(Rect srcRect, Rect destRect, int direction) { 561 switch (direction) { 562 case View.FOCUS_LEFT: 563 return (srcRect.right > destRect.right || srcRect.left >= destRect.right) 564 && srcRect.left > destRect.left; 565 case View.FOCUS_RIGHT: 566 return (srcRect.left < destRect.left || srcRect.right <= destRect.left) 567 && srcRect.right < destRect.right; 568 case View.FOCUS_UP: 569 return (srcRect.bottom > destRect.bottom || srcRect.top >= destRect.bottom) 570 && srcRect.top > destRect.top; 571 case View.FOCUS_DOWN: 572 return (srcRect.top < destRect.top || srcRect.bottom <= destRect.top) 573 && srcRect.bottom < destRect.bottom; 574 } 575 throw new IllegalArgumentException("direction must be one of " 576 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); 577 } 578 579 580 /** 581 * Do the "beams" w.r.t the given direction's axis of rect1 and rect2 overlap? 582 * @param direction the direction (up, down, left, right) 583 * @param rect1 The first rectangle 584 * @param rect2 The second rectangle 585 * @return whether the beams overlap 586 */ beamsOverlap(int direction, Rect rect1, Rect rect2)587 boolean beamsOverlap(int direction, Rect rect1, Rect rect2) { 588 switch (direction) { 589 case View.FOCUS_LEFT: 590 case View.FOCUS_RIGHT: 591 return (rect2.bottom > rect1.top) && (rect2.top < rect1.bottom); 592 case View.FOCUS_UP: 593 case View.FOCUS_DOWN: 594 return (rect2.right > rect1.left) && (rect2.left < rect1.right); 595 } 596 throw new IllegalArgumentException("direction must be one of " 597 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); 598 } 599 600 /** 601 * e.g for left, is 'to left of' 602 */ isToDirectionOf(int direction, Rect src, Rect dest)603 boolean isToDirectionOf(int direction, Rect src, Rect dest) { 604 switch (direction) { 605 case View.FOCUS_LEFT: 606 return src.left >= dest.right; 607 case View.FOCUS_RIGHT: 608 return src.right <= dest.left; 609 case View.FOCUS_UP: 610 return src.top >= dest.bottom; 611 case View.FOCUS_DOWN: 612 return src.bottom <= dest.top; 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 from the edge furthest in the given direction 620 * of source to the edge nearest in the given direction of dest. If the 621 * dest is not in the direction from source, return 0. 622 */ majorAxisDistance(int direction, Rect source, Rect dest)623 static int majorAxisDistance(int direction, Rect source, Rect dest) { 624 return Math.max(0, majorAxisDistanceRaw(direction, source, dest)); 625 } 626 majorAxisDistanceRaw(int direction, Rect source, Rect dest)627 static int majorAxisDistanceRaw(int direction, Rect source, Rect dest) { 628 switch (direction) { 629 case View.FOCUS_LEFT: 630 return source.left - dest.right; 631 case View.FOCUS_RIGHT: 632 return dest.left - source.right; 633 case View.FOCUS_UP: 634 return source.top - dest.bottom; 635 case View.FOCUS_DOWN: 636 return dest.top - source.bottom; 637 } 638 throw new IllegalArgumentException("direction must be one of " 639 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); 640 } 641 642 /** 643 * @return The distance along the major axis w.r.t the direction from the 644 * edge of source to the far edge of dest. If the 645 * dest is not in the direction from source, return 1 (to break ties with 646 * {@link #majorAxisDistance}). 647 */ majorAxisDistanceToFarEdge(int direction, Rect source, Rect dest)648 static int majorAxisDistanceToFarEdge(int direction, Rect source, Rect dest) { 649 return Math.max(1, majorAxisDistanceToFarEdgeRaw(direction, source, dest)); 650 } 651 majorAxisDistanceToFarEdgeRaw(int direction, Rect source, Rect dest)652 static int majorAxisDistanceToFarEdgeRaw(int direction, Rect source, Rect dest) { 653 switch (direction) { 654 case View.FOCUS_LEFT: 655 return source.left - dest.left; 656 case View.FOCUS_RIGHT: 657 return dest.right - source.right; 658 case View.FOCUS_UP: 659 return source.top - dest.top; 660 case View.FOCUS_DOWN: 661 return dest.bottom - source.bottom; 662 } 663 throw new IllegalArgumentException("direction must be one of " 664 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); 665 } 666 667 /** 668 * Find the distance on the minor axis w.r.t the direction to the nearest 669 * edge of the destination rectangle. 670 * @param direction the direction (up, down, left, right) 671 * @param source The source rect. 672 * @param dest The destination rect. 673 * @return The distance. 674 */ minorAxisDistance(int direction, Rect source, Rect dest)675 static int minorAxisDistance(int direction, Rect source, Rect dest) { 676 switch (direction) { 677 case View.FOCUS_LEFT: 678 case View.FOCUS_RIGHT: 679 // the distance between the center verticals 680 return Math.abs( 681 ((source.top + source.height() / 2) - 682 ((dest.top + dest.height() / 2)))); 683 case View.FOCUS_UP: 684 case View.FOCUS_DOWN: 685 // the distance between the center horizontals 686 return Math.abs( 687 ((source.left + source.width() / 2) - 688 ((dest.left + dest.width() / 2)))); 689 } 690 throw new IllegalArgumentException("direction must be one of " 691 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); 692 } 693 694 /** 695 * Find the nearest touchable view to the specified view. 696 * 697 * @param root The root of the tree in which to search 698 * @param x X coordinate from which to start the search 699 * @param y Y coordinate from which to start the search 700 * @param direction Direction to look 701 * @param deltas Offset from the <x, y> to the edge of the nearest view. Note that this array 702 * may already be populated with values. 703 * @return The nearest touchable view, or null if none exists. 704 */ findNearestTouchable(ViewGroup root, int x, int y, int direction, int[] deltas)705 public View findNearestTouchable(ViewGroup root, int x, int y, int direction, int[] deltas) { 706 ArrayList<View> touchables = root.getTouchables(); 707 int minDistance = Integer.MAX_VALUE; 708 View closest = null; 709 710 int numTouchables = touchables.size(); 711 712 int edgeSlop = ViewConfiguration.get(root.mContext).getScaledEdgeSlop(); 713 714 Rect closestBounds = new Rect(); 715 Rect touchableBounds = mOtherRect; 716 717 for (int i = 0; i < numTouchables; i++) { 718 View touchable = touchables.get(i); 719 720 // get visible bounds of other view in same coordinate system 721 touchable.getDrawingRect(touchableBounds); 722 723 root.offsetRectBetweenParentAndChild(touchable, touchableBounds, true, true); 724 725 if (!isTouchCandidate(x, y, touchableBounds, direction)) { 726 continue; 727 } 728 729 int distance = Integer.MAX_VALUE; 730 731 switch (direction) { 732 case View.FOCUS_LEFT: 733 distance = x - touchableBounds.right + 1; 734 break; 735 case View.FOCUS_RIGHT: 736 distance = touchableBounds.left; 737 break; 738 case View.FOCUS_UP: 739 distance = y - touchableBounds.bottom + 1; 740 break; 741 case View.FOCUS_DOWN: 742 distance = touchableBounds.top; 743 break; 744 } 745 746 if (distance < edgeSlop) { 747 // Give preference to innermost views 748 if (closest == null || 749 closestBounds.contains(touchableBounds) || 750 (!touchableBounds.contains(closestBounds) && distance < minDistance)) { 751 minDistance = distance; 752 closest = touchable; 753 closestBounds.set(touchableBounds); 754 switch (direction) { 755 case View.FOCUS_LEFT: 756 deltas[0] = -distance; 757 break; 758 case View.FOCUS_RIGHT: 759 deltas[0] = distance; 760 break; 761 case View.FOCUS_UP: 762 deltas[1] = -distance; 763 break; 764 case View.FOCUS_DOWN: 765 deltas[1] = distance; 766 break; 767 } 768 } 769 } 770 } 771 return closest; 772 } 773 774 775 /** 776 * Is destRect a candidate for the next touch given the direction? 777 */ isTouchCandidate(int x, int y, Rect destRect, int direction)778 private boolean isTouchCandidate(int x, int y, Rect destRect, int direction) { 779 switch (direction) { 780 case View.FOCUS_LEFT: 781 return destRect.left <= x && destRect.top <= y && y <= destRect.bottom; 782 case View.FOCUS_RIGHT: 783 return destRect.left >= x && destRect.top <= y && y <= destRect.bottom; 784 case View.FOCUS_UP: 785 return destRect.top <= y && destRect.left <= x && x <= destRect.right; 786 case View.FOCUS_DOWN: 787 return destRect.top >= y && destRect.left <= x && x <= destRect.right; 788 } 789 throw new IllegalArgumentException("direction must be one of " 790 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); 791 } 792 isValidId(final int id)793 private static final boolean isValidId(final int id) { 794 return id != 0 && id != View.NO_ID; 795 } 796 797 static final class FocusSorter { 798 private ArrayList<Rect> mRectPool = new ArrayList<>(); 799 private int mLastPoolRect; 800 private int mRtlMult; 801 private HashMap<View, Rect> mRectByView = null; 802 803 private Comparator<View> mTopsComparator = (first, second) -> { 804 if (first == second) { 805 return 0; 806 } 807 808 Rect firstRect = mRectByView.get(first); 809 Rect secondRect = mRectByView.get(second); 810 811 int result = firstRect.top - secondRect.top; 812 if (result == 0) { 813 return firstRect.bottom - secondRect.bottom; 814 } 815 return result; 816 }; 817 818 private Comparator<View> mSidesComparator = (first, second) -> { 819 if (first == second) { 820 return 0; 821 } 822 823 Rect firstRect = mRectByView.get(first); 824 Rect secondRect = mRectByView.get(second); 825 826 int result = firstRect.left - secondRect.left; 827 if (result == 0) { 828 return firstRect.right - secondRect.right; 829 } 830 return mRtlMult * result; 831 }; 832 sort(View[] views, int start, int end, ViewGroup root, boolean isRtl)833 public void sort(View[] views, int start, int end, ViewGroup root, boolean isRtl) { 834 int count = end - start; 835 if (count < 2) { 836 return; 837 } 838 if (mRectByView == null) { 839 mRectByView = new HashMap<>(); 840 } 841 mRtlMult = isRtl ? -1 : 1; 842 for (int i = mRectPool.size(); i < count; ++i) { 843 mRectPool.add(new Rect()); 844 } 845 for (int i = start; i < end; ++i) { 846 Rect next = mRectPool.get(mLastPoolRect++); 847 views[i].getDrawingRect(next); 848 root.offsetDescendantRectToMyCoords(views[i], next); 849 mRectByView.put(views[i], next); 850 } 851 852 // Sort top-to-bottom 853 Arrays.sort(views, start, count, mTopsComparator); 854 // Sweep top-to-bottom to identify rows 855 int sweepBottom = mRectByView.get(views[start]).bottom; 856 int rowStart = start; 857 int sweepIdx = start + 1; 858 for (; sweepIdx < end; ++sweepIdx) { 859 Rect currRect = mRectByView.get(views[sweepIdx]); 860 if (currRect.top >= sweepBottom) { 861 // Next view is on a new row, sort the row we've just finished left-to-right. 862 if ((sweepIdx - rowStart) > 1) { 863 Arrays.sort(views, rowStart, sweepIdx, mSidesComparator); 864 } 865 sweepBottom = currRect.bottom; 866 rowStart = sweepIdx; 867 } else { 868 // Next view vertically overlaps, we need to extend our "row height" 869 sweepBottom = Math.max(sweepBottom, currRect.bottom); 870 } 871 } 872 // Sort whatever's left (final row) left-to-right 873 if ((sweepIdx - rowStart) > 1) { 874 Arrays.sort(views, rowStart, sweepIdx, mSidesComparator); 875 } 876 877 mLastPoolRect = 0; 878 mRectByView.clear(); 879 } 880 } 881 882 /** 883 * Public for testing. 884 * 885 * @hide 886 */ 887 @TestApi sort(View[] views, int start, int end, ViewGroup root, boolean isRtl)888 public static void sort(View[] views, int start, int end, ViewGroup root, boolean isRtl) { 889 getInstance().mFocusSorter.sort(views, start, end, root, isRtl); 890 } 891 892 /** 893 * Sorts views according to any explicitly-specified focus-chains. If there are no explicitly 894 * specified focus chains (eg. no nextFocusForward attributes defined), this should be a no-op. 895 */ 896 private static final class UserSpecifiedFocusComparator implements Comparator<View> { 897 private final ArrayMap<View, View> mNextFoci = new ArrayMap<>(); 898 private final ArraySet<View> mIsConnectedTo = new ArraySet<>(); 899 private final ArrayMap<View, View> mHeadsOfChains = new ArrayMap<View, View>(); 900 private final ArrayMap<View, Integer> mOriginalOrdinal = new ArrayMap<>(); 901 private final NextFocusGetter mNextFocusGetter; 902 private View mRoot; 903 904 public interface NextFocusGetter { get(View root, View view)905 View get(View root, View view); 906 } 907 UserSpecifiedFocusComparator(NextFocusGetter nextFocusGetter)908 UserSpecifiedFocusComparator(NextFocusGetter nextFocusGetter) { 909 mNextFocusGetter = nextFocusGetter; 910 } 911 recycle()912 public void recycle() { 913 mRoot = null; 914 mHeadsOfChains.clear(); 915 mIsConnectedTo.clear(); 916 mOriginalOrdinal.clear(); 917 mNextFoci.clear(); 918 } 919 setFocusables(List<View> focusables, View root)920 public void setFocusables(List<View> focusables, View root) { 921 mRoot = root; 922 for (int i = 0; i < focusables.size(); ++i) { 923 mOriginalOrdinal.put(focusables.get(i), i); 924 } 925 926 for (int i = focusables.size() - 1; i >= 0; i--) { 927 final View view = focusables.get(i); 928 final View next = mNextFocusGetter.get(mRoot, view); 929 if (next != null && mOriginalOrdinal.containsKey(next)) { 930 mNextFoci.put(view, next); 931 mIsConnectedTo.add(next); 932 } 933 } 934 935 for (int i = focusables.size() - 1; i >= 0; i--) { 936 final View view = focusables.get(i); 937 final View next = mNextFoci.get(view); 938 if (next != null && !mIsConnectedTo.contains(view)) { 939 setHeadOfChain(view); 940 } 941 } 942 } 943 setHeadOfChain(View head)944 private void setHeadOfChain(View head) { 945 for (View view = head; view != null; view = mNextFoci.get(view)) { 946 final View otherHead = mHeadsOfChains.get(view); 947 if (otherHead != null) { 948 if (otherHead == head) { 949 return; // This view has already had its head set properly 950 } 951 // A hydra -- multi-headed focus chain (e.g. A->C and B->C) 952 // Use the one we've already chosen instead and reset this chain. 953 view = head; 954 head = otherHead; 955 } 956 mHeadsOfChains.put(view, head); 957 } 958 } 959 compare(View first, View second)960 public int compare(View first, View second) { 961 if (first == second) { 962 return 0; 963 } 964 // Order between views within a chain is immaterial -- next/previous is 965 // within a chain is handled elsewhere. 966 View firstHead = mHeadsOfChains.get(first); 967 View secondHead = mHeadsOfChains.get(second); 968 if (firstHead == secondHead && firstHead != null) { 969 if (first == firstHead) { 970 return -1; // first is the head, it should be first 971 } else if (second == firstHead) { 972 return 1; // second is the head, it should be first 973 } else if (mNextFoci.get(first) != null) { 974 return -1; // first is not the end of the chain 975 } else { 976 return 1; // first is end of chain 977 } 978 } 979 boolean involvesChain = false; 980 if (firstHead != null) { 981 first = firstHead; 982 involvesChain = true; 983 } 984 if (secondHead != null) { 985 second = secondHead; 986 involvesChain = true; 987 } 988 989 if (involvesChain) { 990 // keep original order between chains 991 return mOriginalOrdinal.get(first) < mOriginalOrdinal.get(second) ? -1 : 1; 992 } else { 993 return 0; 994 } 995 } 996 } 997 } 998