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