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.graphics.Rect; 20 import android.util.ArrayMap; 21 import android.util.SparseArray; 22 import android.util.SparseBooleanArray; 23 24 import java.util.ArrayList; 25 import java.util.Collections; 26 import java.util.Comparator; 27 28 /** 29 * The algorithm used for finding the next focusable view in a given direction 30 * from a view that currently has focus. 31 */ 32 public class FocusFinder { 33 34 private static final ThreadLocal<FocusFinder> tlFocusFinder = 35 new ThreadLocal<FocusFinder>() { 36 @Override 37 protected FocusFinder initialValue() { 38 return new FocusFinder(); 39 } 40 }; 41 42 /** 43 * Get the focus finder for this thread. 44 */ getInstance()45 public static FocusFinder getInstance() { 46 return tlFocusFinder.get(); 47 } 48 49 final Rect mFocusedRect = new Rect(); 50 final Rect mOtherRect = new Rect(); 51 final Rect mBestCandidateRect = new Rect(); 52 final SequentialFocusComparator mSequentialFocusComparator = new SequentialFocusComparator(); 53 54 private final ArrayList<View> mTempList = new ArrayList<View>(); 55 56 // enforce thread local access FocusFinder()57 private FocusFinder() {} 58 59 /** 60 * Find the next view to take focus in root's descendants, starting from the view 61 * that currently is focused. 62 * @param root Contains focused. Cannot be null. 63 * @param focused Has focus now. 64 * @param direction Direction to look. 65 * @return The next focusable view, or null if none exists. 66 */ findNextFocus(ViewGroup root, View focused, int direction)67 public final View findNextFocus(ViewGroup root, View focused, int direction) { 68 return findNextFocus(root, focused, null, direction); 69 } 70 71 /** 72 * Find the next view to take focus in root's descendants, searching from 73 * a particular rectangle in root's coordinates. 74 * @param root Contains focusedRect. Cannot be null. 75 * @param focusedRect The starting point of the search. 76 * @param direction Direction to look. 77 * @return The next focusable view, or null if none exists. 78 */ findNextFocusFromRect(ViewGroup root, Rect focusedRect, int direction)79 public View findNextFocusFromRect(ViewGroup root, Rect focusedRect, int direction) { 80 mFocusedRect.set(focusedRect); 81 return findNextFocus(root, null, mFocusedRect, direction); 82 } 83 findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction)84 private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) { 85 View next = null; 86 if (focused != null) { 87 next = findNextUserSpecifiedFocus(root, focused, direction); 88 } 89 if (next != null) { 90 return next; 91 } 92 ArrayList<View> focusables = mTempList; 93 try { 94 focusables.clear(); 95 root.addFocusables(focusables, direction); 96 if (!focusables.isEmpty()) { 97 next = findNextFocus(root, focused, focusedRect, direction, focusables); 98 } 99 } finally { 100 focusables.clear(); 101 } 102 return next; 103 } 104 findNextUserSpecifiedFocus(ViewGroup root, View focused, int direction)105 private View findNextUserSpecifiedFocus(ViewGroup root, View focused, int direction) { 106 // check for user specified next focus 107 View userSetNextFocus = focused.findUserSetNextFocus(root, direction); 108 if (userSetNextFocus != null && userSetNextFocus.isFocusable() 109 && (!userSetNextFocus.isInTouchMode() 110 || userSetNextFocus.isFocusableInTouchMode())) { 111 return userSetNextFocus; 112 } 113 return null; 114 } 115 findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction, ArrayList<View> focusables)116 private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, 117 int direction, ArrayList<View> focusables) { 118 if (focused != null) { 119 if (focusedRect == null) { 120 focusedRect = mFocusedRect; 121 } 122 // fill in interesting rect from focused 123 focused.getFocusedRect(focusedRect); 124 root.offsetDescendantRectToMyCoords(focused, focusedRect); 125 } else { 126 if (focusedRect == null) { 127 focusedRect = mFocusedRect; 128 // make up a rect at top left or bottom right of root 129 switch (direction) { 130 case View.FOCUS_RIGHT: 131 case View.FOCUS_DOWN: 132 setFocusTopLeft(root, focusedRect); 133 break; 134 case View.FOCUS_FORWARD: 135 if (root.isLayoutRtl()) { 136 setFocusBottomRight(root, focusedRect); 137 } else { 138 setFocusTopLeft(root, focusedRect); 139 } 140 break; 141 142 case View.FOCUS_LEFT: 143 case View.FOCUS_UP: 144 setFocusBottomRight(root, focusedRect); 145 break; 146 case View.FOCUS_BACKWARD: 147 if (root.isLayoutRtl()) { 148 setFocusTopLeft(root, focusedRect); 149 } else { 150 setFocusBottomRight(root, focusedRect); 151 break; 152 } 153 } 154 } 155 } 156 157 switch (direction) { 158 case View.FOCUS_FORWARD: 159 case View.FOCUS_BACKWARD: 160 return findNextFocusInRelativeDirection(focusables, root, focused, focusedRect, 161 direction); 162 case View.FOCUS_UP: 163 case View.FOCUS_DOWN: 164 case View.FOCUS_LEFT: 165 case View.FOCUS_RIGHT: 166 return findNextFocusInAbsoluteDirection(focusables, root, focused, 167 focusedRect, direction); 168 default: 169 throw new IllegalArgumentException("Unknown direction: " + direction); 170 } 171 } 172 findNextFocusInRelativeDirection(ArrayList<View> focusables, ViewGroup root, View focused, Rect focusedRect, int direction)173 private View findNextFocusInRelativeDirection(ArrayList<View> focusables, ViewGroup root, 174 View focused, Rect focusedRect, int direction) { 175 try { 176 // Note: This sort is stable. 177 mSequentialFocusComparator.setRoot(root); 178 mSequentialFocusComparator.setIsLayoutRtl(root.isLayoutRtl()); 179 mSequentialFocusComparator.setFocusables(focusables); 180 Collections.sort(focusables, mSequentialFocusComparator); 181 } finally { 182 mSequentialFocusComparator.recycle(); 183 } 184 185 final int count = focusables.size(); 186 switch (direction) { 187 case View.FOCUS_FORWARD: 188 return getNextFocusable(focused, focusables, count); 189 case View.FOCUS_BACKWARD: 190 return getPreviousFocusable(focused, focusables, count); 191 } 192 return focusables.get(count - 1); 193 } 194 setFocusBottomRight(ViewGroup root, Rect focusedRect)195 private void setFocusBottomRight(ViewGroup root, Rect focusedRect) { 196 final int rootBottom = root.getScrollY() + root.getHeight(); 197 final int rootRight = root.getScrollX() + root.getWidth(); 198 focusedRect.set(rootRight, rootBottom, rootRight, rootBottom); 199 } 200 setFocusTopLeft(ViewGroup root, Rect focusedRect)201 private void setFocusTopLeft(ViewGroup root, Rect focusedRect) { 202 final int rootTop = root.getScrollY(); 203 final int rootLeft = root.getScrollX(); 204 focusedRect.set(rootLeft, rootTop, rootLeft, rootTop); 205 } 206 findNextFocusInAbsoluteDirection(ArrayList<View> focusables, ViewGroup root, View focused, Rect focusedRect, int direction)207 View findNextFocusInAbsoluteDirection(ArrayList<View> focusables, ViewGroup root, View focused, 208 Rect focusedRect, int direction) { 209 // initialize the best candidate to something impossible 210 // (so the first plausible view will become the best choice) 211 mBestCandidateRect.set(focusedRect); 212 switch(direction) { 213 case View.FOCUS_LEFT: 214 mBestCandidateRect.offset(focusedRect.width() + 1, 0); 215 break; 216 case View.FOCUS_RIGHT: 217 mBestCandidateRect.offset(-(focusedRect.width() + 1), 0); 218 break; 219 case View.FOCUS_UP: 220 mBestCandidateRect.offset(0, focusedRect.height() + 1); 221 break; 222 case View.FOCUS_DOWN: 223 mBestCandidateRect.offset(0, -(focusedRect.height() + 1)); 224 } 225 226 View closest = null; 227 228 int numFocusables = focusables.size(); 229 for (int i = 0; i < numFocusables; i++) { 230 View focusable = focusables.get(i); 231 232 // only interested in other non-root views 233 if (focusable == focused || focusable == root) continue; 234 235 // get focus bounds of other view in same coordinate system 236 focusable.getFocusedRect(mOtherRect); 237 root.offsetDescendantRectToMyCoords(focusable, mOtherRect); 238 239 if (isBetterCandidate(direction, focusedRect, mOtherRect, mBestCandidateRect)) { 240 mBestCandidateRect.set(mOtherRect); 241 closest = focusable; 242 } 243 } 244 return closest; 245 } 246 getNextFocusable(View focused, ArrayList<View> focusables, int count)247 private static View getNextFocusable(View focused, ArrayList<View> focusables, int count) { 248 if (focused != null) { 249 int position = focusables.lastIndexOf(focused); 250 if (position >= 0 && position + 1 < count) { 251 return focusables.get(position + 1); 252 } 253 } 254 if (!focusables.isEmpty()) { 255 return focusables.get(0); 256 } 257 return null; 258 } 259 getPreviousFocusable(View focused, ArrayList<View> focusables, int count)260 private static View getPreviousFocusable(View focused, ArrayList<View> focusables, int count) { 261 if (focused != null) { 262 int position = focusables.indexOf(focused); 263 if (position > 0) { 264 return focusables.get(position - 1); 265 } 266 } 267 if (!focusables.isEmpty()) { 268 return focusables.get(count - 1); 269 } 270 return null; 271 } 272 273 /** 274 * Is rect1 a better candidate than rect2 for a focus search in a particular 275 * direction from a source rect? This is the core routine that determines 276 * the order of focus searching. 277 * @param direction the direction (up, down, left, right) 278 * @param source The source we are searching from 279 * @param rect1 The candidate rectangle 280 * @param rect2 The current best candidate. 281 * @return Whether the candidate is the new best. 282 */ isBetterCandidate(int direction, Rect source, Rect rect1, Rect rect2)283 boolean isBetterCandidate(int direction, Rect source, Rect rect1, Rect rect2) { 284 285 // to be a better candidate, need to at least be a candidate in the first 286 // place :) 287 if (!isCandidate(source, rect1, direction)) { 288 return false; 289 } 290 291 // we know that rect1 is a candidate.. if rect2 is not a candidate, 292 // rect1 is better 293 if (!isCandidate(source, rect2, direction)) { 294 return true; 295 } 296 297 // if rect1 is better by beam, it wins 298 if (beamBeats(direction, source, rect1, rect2)) { 299 return true; 300 } 301 302 // if rect2 is better, then rect1 cant' be :) 303 if (beamBeats(direction, source, rect2, rect1)) { 304 return false; 305 } 306 307 // otherwise, do fudge-tastic comparison of the major and minor axis 308 return (getWeightedDistanceFor( 309 majorAxisDistance(direction, source, rect1), 310 minorAxisDistance(direction, source, rect1)) 311 < getWeightedDistanceFor( 312 majorAxisDistance(direction, source, rect2), 313 minorAxisDistance(direction, source, rect2))); 314 } 315 316 /** 317 * One rectangle may be another candidate than another by virtue of being 318 * exclusively in the beam of the source rect. 319 * @return Whether rect1 is a better candidate than rect2 by virtue of it being in src's 320 * beam 321 */ beamBeats(int direction, Rect source, Rect rect1, Rect rect2)322 boolean beamBeats(int direction, Rect source, Rect rect1, Rect rect2) { 323 final boolean rect1InSrcBeam = beamsOverlap(direction, source, rect1); 324 final boolean rect2InSrcBeam = beamsOverlap(direction, source, rect2); 325 326 // if rect1 isn't exclusively in the src beam, it doesn't win 327 if (rect2InSrcBeam || !rect1InSrcBeam) { 328 return false; 329 } 330 331 // we know rect1 is in the beam, and rect2 is not 332 333 // if rect1 is to the direction of, and rect2 is not, rect1 wins. 334 // for example, for direction left, if rect1 is to the left of the source 335 // and rect2 is below, then we always prefer the in beam rect1, since rect2 336 // could be reached by going down. 337 if (!isToDirectionOf(direction, source, rect2)) { 338 return true; 339 } 340 341 // for horizontal directions, being exclusively in beam always wins 342 if ((direction == View.FOCUS_LEFT || direction == View.FOCUS_RIGHT)) { 343 return true; 344 } 345 346 // for vertical directions, beams only beat up to a point: 347 // now, as long as rect2 isn't completely closer, rect1 wins 348 // e.g for direction down, completely closer means for rect2's top 349 // edge to be closer to the source's top edge than rect1's bottom edge. 350 return (majorAxisDistance(direction, source, rect1) 351 < majorAxisDistanceToFarEdge(direction, source, rect2)); 352 } 353 354 /** 355 * Fudge-factor opportunity: how to calculate distance given major and minor 356 * axis distances. Warning: this fudge factor is finely tuned, be sure to 357 * run all focus tests if you dare tweak it. 358 */ getWeightedDistanceFor(int majorAxisDistance, int minorAxisDistance)359 int getWeightedDistanceFor(int majorAxisDistance, int minorAxisDistance) { 360 return 13 * majorAxisDistance * majorAxisDistance 361 + minorAxisDistance * minorAxisDistance; 362 } 363 364 /** 365 * Is destRect a candidate for the next focus given the direction? This 366 * checks whether the dest is at least partially to the direction of (e.g left of) 367 * from source. 368 * 369 * Includes an edge case for an empty rect (which is used in some cases when 370 * searching from a point on the screen). 371 */ isCandidate(Rect srcRect, Rect destRect, int direction)372 boolean isCandidate(Rect srcRect, Rect destRect, int direction) { 373 switch (direction) { 374 case View.FOCUS_LEFT: 375 return (srcRect.right > destRect.right || srcRect.left >= destRect.right) 376 && srcRect.left > destRect.left; 377 case View.FOCUS_RIGHT: 378 return (srcRect.left < destRect.left || srcRect.right <= destRect.left) 379 && srcRect.right < destRect.right; 380 case View.FOCUS_UP: 381 return (srcRect.bottom > destRect.bottom || srcRect.top >= destRect.bottom) 382 && srcRect.top > destRect.top; 383 case View.FOCUS_DOWN: 384 return (srcRect.top < destRect.top || srcRect.bottom <= destRect.top) 385 && srcRect.bottom < destRect.bottom; 386 } 387 throw new IllegalArgumentException("direction must be one of " 388 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); 389 } 390 391 392 /** 393 * Do the "beams" w.r.t the given direction's axis of rect1 and rect2 overlap? 394 * @param direction the direction (up, down, left, right) 395 * @param rect1 The first rectangle 396 * @param rect2 The second rectangle 397 * @return whether the beams overlap 398 */ beamsOverlap(int direction, Rect rect1, Rect rect2)399 boolean beamsOverlap(int direction, Rect rect1, Rect rect2) { 400 switch (direction) { 401 case View.FOCUS_LEFT: 402 case View.FOCUS_RIGHT: 403 return (rect2.bottom >= rect1.top) && (rect2.top <= rect1.bottom); 404 case View.FOCUS_UP: 405 case View.FOCUS_DOWN: 406 return (rect2.right >= rect1.left) && (rect2.left <= rect1.right); 407 } 408 throw new IllegalArgumentException("direction must be one of " 409 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); 410 } 411 412 /** 413 * e.g for left, is 'to left of' 414 */ isToDirectionOf(int direction, Rect src, Rect dest)415 boolean isToDirectionOf(int direction, Rect src, Rect dest) { 416 switch (direction) { 417 case View.FOCUS_LEFT: 418 return src.left >= dest.right; 419 case View.FOCUS_RIGHT: 420 return src.right <= dest.left; 421 case View.FOCUS_UP: 422 return src.top >= dest.bottom; 423 case View.FOCUS_DOWN: 424 return src.bottom <= dest.top; 425 } 426 throw new IllegalArgumentException("direction must be one of " 427 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); 428 } 429 430 /** 431 * @return The distance from the edge furthest in the given direction 432 * of source to the edge nearest in the given direction of dest. If the 433 * dest is not in the direction from source, return 0. 434 */ majorAxisDistance(int direction, Rect source, Rect dest)435 static int majorAxisDistance(int direction, Rect source, Rect dest) { 436 return Math.max(0, majorAxisDistanceRaw(direction, source, dest)); 437 } 438 majorAxisDistanceRaw(int direction, Rect source, Rect dest)439 static int majorAxisDistanceRaw(int direction, Rect source, Rect dest) { 440 switch (direction) { 441 case View.FOCUS_LEFT: 442 return source.left - dest.right; 443 case View.FOCUS_RIGHT: 444 return dest.left - source.right; 445 case View.FOCUS_UP: 446 return source.top - dest.bottom; 447 case View.FOCUS_DOWN: 448 return dest.top - source.bottom; 449 } 450 throw new IllegalArgumentException("direction must be one of " 451 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); 452 } 453 454 /** 455 * @return The distance along the major axis w.r.t the direction from the 456 * edge of source to the far edge of dest. If the 457 * dest is not in the direction from source, return 1 (to break ties with 458 * {@link #majorAxisDistance}). 459 */ majorAxisDistanceToFarEdge(int direction, Rect source, Rect dest)460 static int majorAxisDistanceToFarEdge(int direction, Rect source, Rect dest) { 461 return Math.max(1, majorAxisDistanceToFarEdgeRaw(direction, source, dest)); 462 } 463 majorAxisDistanceToFarEdgeRaw(int direction, Rect source, Rect dest)464 static int majorAxisDistanceToFarEdgeRaw(int direction, Rect source, Rect dest) { 465 switch (direction) { 466 case View.FOCUS_LEFT: 467 return source.left - dest.left; 468 case View.FOCUS_RIGHT: 469 return dest.right - source.right; 470 case View.FOCUS_UP: 471 return source.top - dest.top; 472 case View.FOCUS_DOWN: 473 return dest.bottom - source.bottom; 474 } 475 throw new IllegalArgumentException("direction must be one of " 476 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); 477 } 478 479 /** 480 * Find the distance on the minor axis w.r.t the direction to the nearest 481 * edge of the destination rectangle. 482 * @param direction the direction (up, down, left, right) 483 * @param source The source rect. 484 * @param dest The destination rect. 485 * @return The distance. 486 */ minorAxisDistance(int direction, Rect source, Rect dest)487 static int minorAxisDistance(int direction, Rect source, Rect dest) { 488 switch (direction) { 489 case View.FOCUS_LEFT: 490 case View.FOCUS_RIGHT: 491 // the distance between the center verticals 492 return Math.abs( 493 ((source.top + source.height() / 2) - 494 ((dest.top + dest.height() / 2)))); 495 case View.FOCUS_UP: 496 case View.FOCUS_DOWN: 497 // the distance between the center horizontals 498 return Math.abs( 499 ((source.left + source.width() / 2) - 500 ((dest.left + dest.width() / 2)))); 501 } 502 throw new IllegalArgumentException("direction must be one of " 503 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); 504 } 505 506 /** 507 * Find the nearest touchable view to the specified view. 508 * 509 * @param root The root of the tree in which to search 510 * @param x X coordinate from which to start the search 511 * @param y Y coordinate from which to start the search 512 * @param direction Direction to look 513 * @param deltas Offset from the <x, y> to the edge of the nearest view. Note that this array 514 * may already be populated with values. 515 * @return The nearest touchable view, or null if none exists. 516 */ findNearestTouchable(ViewGroup root, int x, int y, int direction, int[] deltas)517 public View findNearestTouchable(ViewGroup root, int x, int y, int direction, int[] deltas) { 518 ArrayList<View> touchables = root.getTouchables(); 519 int minDistance = Integer.MAX_VALUE; 520 View closest = null; 521 522 int numTouchables = touchables.size(); 523 524 int edgeSlop = ViewConfiguration.get(root.mContext).getScaledEdgeSlop(); 525 526 Rect closestBounds = new Rect(); 527 Rect touchableBounds = mOtherRect; 528 529 for (int i = 0; i < numTouchables; i++) { 530 View touchable = touchables.get(i); 531 532 // get visible bounds of other view in same coordinate system 533 touchable.getDrawingRect(touchableBounds); 534 535 root.offsetRectBetweenParentAndChild(touchable, touchableBounds, true, true); 536 537 if (!isTouchCandidate(x, y, touchableBounds, direction)) { 538 continue; 539 } 540 541 int distance = Integer.MAX_VALUE; 542 543 switch (direction) { 544 case View.FOCUS_LEFT: 545 distance = x - touchableBounds.right + 1; 546 break; 547 case View.FOCUS_RIGHT: 548 distance = touchableBounds.left; 549 break; 550 case View.FOCUS_UP: 551 distance = y - touchableBounds.bottom + 1; 552 break; 553 case View.FOCUS_DOWN: 554 distance = touchableBounds.top; 555 break; 556 } 557 558 if (distance < edgeSlop) { 559 // Give preference to innermost views 560 if (closest == null || 561 closestBounds.contains(touchableBounds) || 562 (!touchableBounds.contains(closestBounds) && distance < minDistance)) { 563 minDistance = distance; 564 closest = touchable; 565 closestBounds.set(touchableBounds); 566 switch (direction) { 567 case View.FOCUS_LEFT: 568 deltas[0] = -distance; 569 break; 570 case View.FOCUS_RIGHT: 571 deltas[0] = distance; 572 break; 573 case View.FOCUS_UP: 574 deltas[1] = -distance; 575 break; 576 case View.FOCUS_DOWN: 577 deltas[1] = distance; 578 break; 579 } 580 } 581 } 582 } 583 return closest; 584 } 585 586 587 /** 588 * Is destRect a candidate for the next touch given the direction? 589 */ isTouchCandidate(int x, int y, Rect destRect, int direction)590 private boolean isTouchCandidate(int x, int y, Rect destRect, int direction) { 591 switch (direction) { 592 case View.FOCUS_LEFT: 593 return destRect.left <= x && destRect.top <= y && y <= destRect.bottom; 594 case View.FOCUS_RIGHT: 595 return destRect.left >= x && destRect.top <= y && y <= destRect.bottom; 596 case View.FOCUS_UP: 597 return destRect.top <= y && destRect.left <= x && x <= destRect.right; 598 case View.FOCUS_DOWN: 599 return destRect.top >= y && destRect.left <= x && x <= destRect.right; 600 } 601 throw new IllegalArgumentException("direction must be one of " 602 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); 603 } 604 isValidId(final int id)605 private static final boolean isValidId(final int id) { 606 return id != 0 && id != View.NO_ID; 607 } 608 609 /** 610 * Sorts views according to their visual layout and geometry for default tab order. 611 * If views are part of a focus chain (nextFocusForwardId), then they are all grouped 612 * together. The head of the chain is used to determine the order of the chain and is 613 * first in the order and the tail of the chain is the last in the order. The views 614 * in the middle of the chain can be arbitrary order. 615 * This is used for sequential focus traversal. 616 */ 617 private static final class SequentialFocusComparator implements Comparator<View> { 618 private final Rect mFirstRect = new Rect(); 619 private final Rect mSecondRect = new Rect(); 620 private ViewGroup mRoot; 621 private boolean mIsLayoutRtl; 622 private final SparseArray<View> mFocusables = new SparseArray<View>(); 623 private final SparseBooleanArray mIsConnectedTo = new SparseBooleanArray(); 624 private final ArrayMap<View, View> mHeadsOfChains = new ArrayMap<View, View>(); 625 recycle()626 public void recycle() { 627 mRoot = null; 628 mFocusables.clear(); 629 mHeadsOfChains.clear(); 630 mIsConnectedTo.clear(); 631 } 632 setRoot(ViewGroup root)633 public void setRoot(ViewGroup root) { 634 mRoot = root; 635 } 636 setIsLayoutRtl(boolean b)637 public void setIsLayoutRtl(boolean b) { 638 mIsLayoutRtl = b; 639 } 640 setFocusables(ArrayList<View> focusables)641 public void setFocusables(ArrayList<View> focusables) { 642 for (int i = focusables.size() - 1; i >= 0; i--) { 643 final View view = focusables.get(i); 644 final int id = view.getId(); 645 if (isValidId(id)) { 646 mFocusables.put(id, view); 647 } 648 final int nextId = view.getNextFocusForwardId(); 649 if (isValidId(nextId)) { 650 mIsConnectedTo.put(nextId, true); 651 } 652 } 653 654 for (int i = focusables.size() - 1; i >= 0; i--) { 655 final View view = focusables.get(i); 656 final int nextId = view.getNextFocusForwardId(); 657 if (isValidId(nextId) && !mIsConnectedTo.get(view.getId())) { 658 setHeadOfChain(view); 659 } 660 } 661 } 662 setHeadOfChain(View head)663 private void setHeadOfChain(View head) { 664 for (View view = head; view != null; 665 view = mFocusables.get(view.getNextFocusForwardId())) { 666 final View otherHead = mHeadsOfChains.get(view); 667 if (otherHead != null) { 668 if (otherHead == head) { 669 return; // This view has already had its head set properly 670 } 671 // A hydra -- multi-headed focus chain (e.g. A->C and B->C) 672 // Use the one we've already chosen instead and reset this chain. 673 view = head; 674 head = otherHead; 675 } 676 mHeadsOfChains.put(view, head); 677 } 678 } 679 compare(View first, View second)680 public int compare(View first, View second) { 681 if (first == second) { 682 return 0; 683 } 684 // Order between views within a chain is immaterial -- next/previous is 685 // within a chain is handled elsewhere. 686 View firstHead = mHeadsOfChains.get(first); 687 View secondHead = mHeadsOfChains.get(second); 688 if (firstHead == secondHead && firstHead != null) { 689 if (first == firstHead) { 690 return -1; // first is the head, it should be first 691 } else if (second == firstHead) { 692 return 1; // second is the head, it should be first 693 } else if (isValidId(first.getNextFocusForwardId())) { 694 return -1; // first is not the end of the chain 695 } else { 696 return 1; // first is end of chain 697 } 698 } 699 if (firstHead != null) { 700 first = firstHead; 701 } 702 if (secondHead != null) { 703 second = secondHead; 704 } 705 706 // First see if they belong to the same focus chain. 707 getRect(first, mFirstRect); 708 getRect(second, mSecondRect); 709 710 if (mFirstRect.top < mSecondRect.top) { 711 return -1; 712 } else if (mFirstRect.top > mSecondRect.top) { 713 return 1; 714 } else if (mFirstRect.left < mSecondRect.left) { 715 return mIsLayoutRtl ? 1 : -1; 716 } else if (mFirstRect.left > mSecondRect.left) { 717 return mIsLayoutRtl ? -1 : 1; 718 } else if (mFirstRect.bottom < mSecondRect.bottom) { 719 return -1; 720 } else if (mFirstRect.bottom > mSecondRect.bottom) { 721 return 1; 722 } else if (mFirstRect.right < mSecondRect.right) { 723 return mIsLayoutRtl ? 1 : -1; 724 } else if (mFirstRect.right > mSecondRect.right) { 725 return mIsLayoutRtl ? -1 : 1; 726 } else { 727 // The view are distinct but completely coincident so we consider 728 // them equal for our purposes. Since the sort is stable, this 729 // means that the views will retain their layout order relative to one another. 730 return 0; 731 } 732 } 733 getRect(View view, Rect rect)734 private void getRect(View view, Rect rect) { 735 view.getDrawingRect(rect); 736 mRoot.offsetDescendantRectToMyCoords(view, rect); 737 } 738 } 739 } 740