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 23 /** 24 * The algorithm used for finding the next focusable view in a given direction 25 * from a view that currently has focus. 26 */ 27 public class FocusFinder { 28 29 private static ThreadLocal<FocusFinder> tlFocusFinder = 30 new ThreadLocal<FocusFinder>() { 31 32 protected FocusFinder initialValue() { 33 return new FocusFinder(); 34 } 35 }; 36 37 /** 38 * Get the focus finder for this thread. 39 */ 40 public static FocusFinder getInstance() { 41 return tlFocusFinder.get(); 42 } 43 44 Rect mFocusedRect = new Rect(); 45 Rect mOtherRect = new Rect(); 46 Rect mBestCandidateRect = new Rect(); 47 48 // enforce thread local access 49 private FocusFinder() {} 50 51 /** 52 * Find the next view to take focus in root's descendants, starting from the view 53 * that currently is focused. 54 * @param root Contains focused 55 * @param focused Has focus now. 56 * @param direction Direction to look. 57 * @return The next focusable view, or null if none exists. 58 */ 59 public final View findNextFocus(ViewGroup root, View focused, int direction) { 60 61 if (focused != null) { 62 // check for user specified next focus 63 View userSetNextFocus = focused.findUserSetNextFocus(root, direction); 64 if (userSetNextFocus != null && 65 userSetNextFocus.isFocusable() && 66 (!userSetNextFocus.isInTouchMode() || 67 userSetNextFocus.isFocusableInTouchMode())) { 68 return userSetNextFocus; 69 } 70 71 // fill in interesting rect from focused 72 focused.getFocusedRect(mFocusedRect); 73 root.offsetDescendantRectToMyCoords(focused, mFocusedRect); 74 } else { 75 // make up a rect at top left or bottom right of root 76 switch (direction) { 77 case View.FOCUS_RIGHT: 78 case View.FOCUS_DOWN: 79 final int rootTop = root.getScrollY(); 80 final int rootLeft = root.getScrollX(); 81 mFocusedRect.set(rootLeft, rootTop, rootLeft, rootTop); 82 break; 83 84 case View.FOCUS_LEFT: 85 case View.FOCUS_UP: 86 final int rootBottom = root.getScrollY() + root.getHeight(); 87 final int rootRight = root.getScrollX() + root.getWidth(); 88 mFocusedRect.set(rootRight, rootBottom, 89 rootRight, rootBottom); 90 break; 91 } 92 } 93 return findNextFocus(root, focused, mFocusedRect, direction); 94 } 95 96 /** 97 * Find the next view to take focus in root's descendants, searching from 98 * a particular rectangle in root's coordinates. 99 * @param root Contains focusedRect. 100 * @param focusedRect The starting point of the search. 101 * @param direction Direction to look. 102 * @return The next focusable view, or null if none exists. 103 */ 104 public View findNextFocusFromRect(ViewGroup root, Rect focusedRect, int direction) { 105 return findNextFocus(root, null, focusedRect, direction); 106 } 107 108 private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) { 109 ArrayList<View> focusables = root.getFocusables(direction); 110 111 // initialize the best candidate to something impossible 112 // (so the first plausible view will become the best choice) 113 mBestCandidateRect.set(focusedRect); 114 switch(direction) { 115 case View.FOCUS_LEFT: 116 mBestCandidateRect.offset(focusedRect.width() + 1, 0); 117 break; 118 case View.FOCUS_RIGHT: 119 mBestCandidateRect.offset(-(focusedRect.width() + 1), 0); 120 break; 121 case View.FOCUS_UP: 122 mBestCandidateRect.offset(0, focusedRect.height() + 1); 123 break; 124 case View.FOCUS_DOWN: 125 mBestCandidateRect.offset(0, -(focusedRect.height() + 1)); 126 } 127 128 View closest = null; 129 130 int numFocusables = focusables.size(); 131 for (int i = 0; i < numFocusables; i++) { 132 View focusable = focusables.get(i); 133 134 // only interested in other non-root views 135 if (focusable == focused || focusable == root) continue; 136 137 // get visible bounds of other view in same coordinate system 138 focusable.getDrawingRect(mOtherRect); 139 root.offsetDescendantRectToMyCoords(focusable, mOtherRect); 140 141 if (isBetterCandidate(direction, focusedRect, mOtherRect, mBestCandidateRect)) { 142 mBestCandidateRect.set(mOtherRect); 143 closest = focusable; 144 } 145 } 146 return closest; 147 } 148 149 /** 150 * Is rect1 a better candidate than rect2 for a focus search in a particular 151 * direction from a source rect? This is the core routine that determines 152 * the order of focus searching. 153 * @param direction the direction (up, down, left, right) 154 * @param source The source we are searching from 155 * @param rect1 The candidate rectangle 156 * @param rect2 The current best candidate. 157 * @return Whether the candidate is the new best. 158 */ 159 boolean isBetterCandidate(int direction, Rect source, Rect rect1, Rect rect2) { 160 161 // to be a better candidate, need to at least be a candidate in the first 162 // place :) 163 if (!isCandidate(source, rect1, direction)) { 164 return false; 165 } 166 167 // we know that rect1 is a candidate.. if rect2 is not a candidate, 168 // rect1 is better 169 if (!isCandidate(source, rect2, direction)) { 170 return true; 171 } 172 173 // if rect1 is better by beam, it wins 174 if (beamBeats(direction, source, rect1, rect2)) { 175 return true; 176 } 177 178 // if rect2 is better, then rect1 cant' be :) 179 if (beamBeats(direction, source, rect2, rect1)) { 180 return false; 181 } 182 183 // otherwise, do fudge-tastic comparison of the major and minor axis 184 return (getWeightedDistanceFor( 185 majorAxisDistance(direction, source, rect1), 186 minorAxisDistance(direction, source, rect1)) 187 < getWeightedDistanceFor( 188 majorAxisDistance(direction, source, rect2), 189 minorAxisDistance(direction, source, rect2))); 190 } 191 192 /** 193 * One rectangle may be another candidate than another by virtue of being 194 * exclusively in the beam of the source rect. 195 * @return Whether rect1 is a better candidate than rect2 by virtue of it being in src's 196 * beam 197 */ 198 boolean beamBeats(int direction, Rect source, Rect rect1, Rect rect2) { 199 final boolean rect1InSrcBeam = beamsOverlap(direction, source, rect1); 200 final boolean rect2InSrcBeam = beamsOverlap(direction, source, rect2); 201 202 // if rect1 isn't exclusively in the src beam, it doesn't win 203 if (rect2InSrcBeam || !rect1InSrcBeam) { 204 return false; 205 } 206 207 // we know rect1 is in the beam, and rect2 is not 208 209 // if rect1 is to the direction of, and rect2 is not, rect1 wins. 210 // for example, for direction left, if rect1 is to the left of the source 211 // and rect2 is below, then we always prefer the in beam rect1, since rect2 212 // could be reached by going down. 213 if (!isToDirectionOf(direction, source, rect2)) { 214 return true; 215 } 216 217 // for horizontal directions, being exclusively in beam always wins 218 if ((direction == View.FOCUS_LEFT || direction == View.FOCUS_RIGHT)) { 219 return true; 220 } 221 222 // for vertical directions, beams only beat up to a point: 223 // now, as long as rect2 isn't completely closer, rect1 wins 224 // e.g for direction down, completely closer means for rect2's top 225 // edge to be closer to the source's top edge than rect1's bottom edge. 226 return (majorAxisDistance(direction, source, rect1) 227 < majorAxisDistanceToFarEdge(direction, source, rect2)); 228 } 229 230 /** 231 * Fudge-factor opportunity: how to calculate distance given major and minor 232 * axis distances. Warning: this fudge factor is finely tuned, be sure to 233 * run all focus tests if you dare tweak it. 234 */ 235 int getWeightedDistanceFor(int majorAxisDistance, int minorAxisDistance) { 236 return 13 * majorAxisDistance * majorAxisDistance 237 + minorAxisDistance * minorAxisDistance; 238 } 239 240 /** 241 * Is destRect a candidate for the next focus given the direction? This 242 * checks whether the dest is at least partially to the direction of (e.g left of) 243 * from source. 244 * 245 * Includes an edge case for an empty rect (which is used in some cases when 246 * searching from a point on the screen). 247 */ 248 boolean isCandidate(Rect srcRect, Rect destRect, int direction) { 249 switch (direction) { 250 case View.FOCUS_LEFT: 251 return (srcRect.right > destRect.right || srcRect.left >= destRect.right) 252 && srcRect.left > destRect.left; 253 case View.FOCUS_RIGHT: 254 return (srcRect.left < destRect.left || srcRect.right <= destRect.left) 255 && srcRect.right < destRect.right; 256 case View.FOCUS_UP: 257 return (srcRect.bottom > destRect.bottom || srcRect.top >= destRect.bottom) 258 && srcRect.top > destRect.top; 259 case View.FOCUS_DOWN: 260 return (srcRect.top < destRect.top || srcRect.bottom <= destRect.top) 261 && srcRect.bottom < destRect.bottom; 262 } 263 throw new IllegalArgumentException("direction must be one of " 264 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); 265 } 266 267 268 /** 269 * Do the "beams" w.r.t the given direcition's axos of rect1 and rect2 overlap? 270 * @param direction the direction (up, down, left, right) 271 * @param rect1 The first rectangle 272 * @param rect2 The second rectangle 273 * @return whether the beams overlap 274 */ 275 boolean beamsOverlap(int direction, Rect rect1, Rect rect2) { 276 switch (direction) { 277 case View.FOCUS_LEFT: 278 case View.FOCUS_RIGHT: 279 return (rect2.bottom >= rect1.top) && (rect2.top <= rect1.bottom); 280 case View.FOCUS_UP: 281 case View.FOCUS_DOWN: 282 return (rect2.right >= rect1.left) && (rect2.left <= rect1.right); 283 } 284 throw new IllegalArgumentException("direction must be one of " 285 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); 286 } 287 288 /** 289 * e.g for left, is 'to left of' 290 */ 291 boolean isToDirectionOf(int direction, Rect src, Rect dest) { 292 switch (direction) { 293 case View.FOCUS_LEFT: 294 return src.left >= dest.right; 295 case View.FOCUS_RIGHT: 296 return src.right <= dest.left; 297 case View.FOCUS_UP: 298 return src.top >= dest.bottom; 299 case View.FOCUS_DOWN: 300 return src.bottom <= dest.top; 301 } 302 throw new IllegalArgumentException("direction must be one of " 303 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); 304 } 305 306 /** 307 * @return The distance from the edge furthest in the given direction 308 * of source to the edge nearest in the given direction of dest. If the 309 * dest is not in the direction from source, return 0. 310 */ 311 static int majorAxisDistance(int direction, Rect source, Rect dest) { 312 return Math.max(0, majorAxisDistanceRaw(direction, source, dest)); 313 } 314 315 static int majorAxisDistanceRaw(int direction, Rect source, Rect dest) { 316 switch (direction) { 317 case View.FOCUS_LEFT: 318 return source.left - dest.right; 319 case View.FOCUS_RIGHT: 320 return dest.left - source.right; 321 case View.FOCUS_UP: 322 return source.top - dest.bottom; 323 case View.FOCUS_DOWN: 324 return dest.top - source.bottom; 325 } 326 throw new IllegalArgumentException("direction must be one of " 327 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); 328 } 329 330 /** 331 * @return The distance along the major axis w.r.t the direction from the 332 * edge of source to the far edge of dest. If the 333 * dest is not in the direction from source, return 1 (to break ties with 334 * {@link #majorAxisDistance}). 335 */ 336 static int majorAxisDistanceToFarEdge(int direction, Rect source, Rect dest) { 337 return Math.max(1, majorAxisDistanceToFarEdgeRaw(direction, source, dest)); 338 } 339 340 static int majorAxisDistanceToFarEdgeRaw(int direction, Rect source, Rect dest) { 341 switch (direction) { 342 case View.FOCUS_LEFT: 343 return source.left - dest.left; 344 case View.FOCUS_RIGHT: 345 return dest.right - source.right; 346 case View.FOCUS_UP: 347 return source.top - dest.top; 348 case View.FOCUS_DOWN: 349 return dest.bottom - source.bottom; 350 } 351 throw new IllegalArgumentException("direction must be one of " 352 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); 353 } 354 355 /** 356 * Find the distance on the minor axis w.r.t the direction to the nearest 357 * edge of the destination rectange. 358 * @param direction the direction (up, down, left, right) 359 * @param source The source rect. 360 * @param dest The destination rect. 361 * @return The distance. 362 */ 363 static int minorAxisDistance(int direction, Rect source, Rect dest) { 364 switch (direction) { 365 case View.FOCUS_LEFT: 366 case View.FOCUS_RIGHT: 367 // the distance between the center verticals 368 return Math.abs( 369 ((source.top + source.height() / 2) - 370 ((dest.top + dest.height() / 2)))); 371 case View.FOCUS_UP: 372 case View.FOCUS_DOWN: 373 // the distance between the center horizontals 374 return Math.abs( 375 ((source.left + source.width() / 2) - 376 ((dest.left + dest.width() / 2)))); 377 } 378 throw new IllegalArgumentException("direction must be one of " 379 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); 380 } 381 382 /** 383 * Find the nearest touchable view to the specified view. 384 * 385 * @param root The root of the tree in which to search 386 * @param x X coordinate from which to start the search 387 * @param y Y coordinate from which to start the search 388 * @param direction Direction to look 389 * @param deltas Offset from the <x, y> to the edge of the nearest view. Note that this array 390 * may already be populated with values. 391 * @return The nearest touchable view, or null if none exists. 392 */ 393 public View findNearestTouchable(ViewGroup root, int x, int y, int direction, int[] deltas) { 394 ArrayList<View> touchables = root.getTouchables(); 395 int minDistance = Integer.MAX_VALUE; 396 View closest = null; 397 398 int numTouchables = touchables.size(); 399 400 int edgeSlop = ViewConfiguration.get(root.mContext).getScaledEdgeSlop(); 401 402 Rect closestBounds = new Rect(); 403 Rect touchableBounds = mOtherRect; 404 405 for (int i = 0; i < numTouchables; i++) { 406 View touchable = touchables.get(i); 407 408 // get visible bounds of other view in same coordinate system 409 touchable.getDrawingRect(touchableBounds); 410 411 root.offsetRectBetweenParentAndChild(touchable, touchableBounds, true, true); 412 413 if (!isTouchCandidate(x, y, touchableBounds, direction)) { 414 continue; 415 } 416 417 int distance = Integer.MAX_VALUE; 418 419 switch (direction) { 420 case View.FOCUS_LEFT: 421 distance = x - touchableBounds.right + 1; 422 break; 423 case View.FOCUS_RIGHT: 424 distance = touchableBounds.left; 425 break; 426 case View.FOCUS_UP: 427 distance = y - touchableBounds.bottom + 1; 428 break; 429 case View.FOCUS_DOWN: 430 distance = touchableBounds.top; 431 break; 432 } 433 434 if (distance < edgeSlop) { 435 // Give preference to innermost views 436 if (closest == null || 437 closestBounds.contains(touchableBounds) || 438 (!touchableBounds.contains(closestBounds) && distance < minDistance)) { 439 minDistance = distance; 440 closest = touchable; 441 closestBounds.set(touchableBounds); 442 switch (direction) { 443 case View.FOCUS_LEFT: 444 deltas[0] = -distance; 445 break; 446 case View.FOCUS_RIGHT: 447 deltas[0] = distance; 448 break; 449 case View.FOCUS_UP: 450 deltas[1] = -distance; 451 break; 452 case View.FOCUS_DOWN: 453 deltas[1] = distance; 454 break; 455 } 456 } 457 } 458 } 459 return closest; 460 } 461 462 463 /** 464 * Is destRect a candidate for the next touch given the direction? 465 */ 466 private boolean isTouchCandidate(int x, int y, Rect destRect, int direction) { 467 switch (direction) { 468 case View.FOCUS_LEFT: 469 return destRect.left <= x && destRect.top <= y && y <= destRect.bottom; 470 case View.FOCUS_RIGHT: 471 return destRect.left >= x && destRect.top <= y && y <= destRect.bottom; 472 case View.FOCUS_UP: 473 return destRect.top <= y && destRect.left <= x && x <= destRect.right; 474 case View.FOCUS_DOWN: 475 return destRect.top >= y && destRect.left <= x && x <= destRect.right; 476 } 477 throw new IllegalArgumentException("direction must be one of " 478 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); 479 } 480 } 481