blob: d563f51106001d069c890346c1874272c21707a0 [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
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
17package android.view;
18
19import android.graphics.Rect;
George Mount140ea622015-10-27 08:46:44 -070020import android.util.ArrayMap;
21import android.util.SparseArray;
22import android.util.SparseBooleanArray;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080023
24import java.util.ArrayList;
Jeff Brown4e6319b2010-12-13 10:36:51 -080025import java.util.Collections;
26import java.util.Comparator;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080027
28/**
29 * The algorithm used for finding the next focusable view in a given direction
30 * from a view that currently has focus.
31 */
32public class FocusFinder {
33
Svetoslav Ganov76f287e2012-04-23 11:02:36 -070034 private static final ThreadLocal<FocusFinder> tlFocusFinder =
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080035 new ThreadLocal<FocusFinder>() {
Svetoslav Ganov42138042012-03-20 11:51:39 -070036 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080037 protected FocusFinder initialValue() {
38 return new FocusFinder();
39 }
40 };
41
42 /**
43 * Get the focus finder for this thread.
44 */
45 public static FocusFinder getInstance() {
46 return tlFocusFinder.get();
47 }
48
Svetoslav Ganov76f287e2012-04-23 11:02:36 -070049 final Rect mFocusedRect = new Rect();
50 final Rect mOtherRect = new Rect();
51 final Rect mBestCandidateRect = new Rect();
52 final SequentialFocusComparator mSequentialFocusComparator = new SequentialFocusComparator();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080053
Svetoslav Ganov42138042012-03-20 11:51:39 -070054 private final ArrayList<View> mTempList = new ArrayList<View>();
55
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080056 // enforce thread local access
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.
Fabrice Di Meglio7e0a3722012-03-27 16:06:25 -070062 * @param root Contains focused. Cannot be null.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080063 * @param focused Has focus now.
64 * @param direction Direction to look.
65 * @return The next focusable view, or null if none exists.
66 */
67 public final View findNextFocus(ViewGroup root, View focused, int direction) {
Svetoslav Ganov951bb422012-04-30 13:15:21 -070068 return findNextFocus(root, focused, null, direction);
Svetoslav Ganov42138042012-03-20 11:51:39 -070069 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080070
Svetoslav Ganov42138042012-03-20 11:51:39 -070071 /**
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 */
79 public View findNextFocusFromRect(ViewGroup root, Rect focusedRect, int direction) {
Svetoslav Ganov76f287e2012-04-23 11:02:36 -070080 mFocusedRect.set(focusedRect);
81 return findNextFocus(root, null, mFocusedRect, direction);
Svetoslav Ganov42138042012-03-20 11:51:39 -070082 }
83
84 private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
Svetoslav Ganov76f287e2012-04-23 11:02:36 -070085 View next = null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080086 if (focused != null) {
Svetoslav Ganov27e2da72012-07-02 18:12:00 -070087 next = findNextUserSpecifiedFocus(root, focused, direction);
Svetoslav Ganov76f287e2012-04-23 11:02:36 -070088 }
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);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080098 }
Svetoslav Ganov76f287e2012-04-23 11:02:36 -070099 } finally {
100 focusables.clear();
101 }
102 return next;
103 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800104
Svetoslav Ganov27e2da72012-07-02 18:12:00 -0700105 private View findNextUserSpecifiedFocus(ViewGroup root, View focused, int direction) {
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700106 // 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
116 private View findNextFocus(ViewGroup root, View focused, Rect focusedRect,
117 int direction, ArrayList<View> focusables) {
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700118 if (focused != null) {
Svetoslav Ganov951bb422012-04-30 13:15:21 -0700119 if (focusedRect == null) {
120 focusedRect = mFocusedRect;
121 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800122 // fill in interesting rect from focused
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700123 focused.getFocusedRect(focusedRect);
124 root.offsetDescendantRectToMyCoords(focused, focusedRect);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800125 } else {
Svetoslav Ganov951bb422012-04-30 13:15:21 -0700126 if (focusedRect == null) {
127 focusedRect = mFocusedRect;
128 // make up a rect at top left or bottom right of root
Svetoslav Ganov27e2da72012-07-02 18:12:00 -0700129 switch (direction) {
Svetoslav Ganov951bb422012-04-30 13:15:21 -0700130 case View.FOCUS_RIGHT:
131 case View.FOCUS_DOWN:
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700132 setFocusTopLeft(root, focusedRect);
Svetoslav Ganov951bb422012-04-30 13:15:21 -0700133 break;
134 case View.FOCUS_FORWARD:
135 if (root.isLayoutRtl()) {
136 setFocusBottomRight(root, focusedRect);
137 } else {
138 setFocusTopLeft(root, focusedRect);
139 }
140 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800141
Svetoslav Ganov951bb422012-04-30 13:15:21 -0700142 case View.FOCUS_LEFT:
143 case View.FOCUS_UP:
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700144 setFocusBottomRight(root, focusedRect);
Svetoslav Ganov951bb422012-04-30 13:15:21 -0700145 break;
146 case View.FOCUS_BACKWARD:
147 if (root.isLayoutRtl()) {
148 setFocusTopLeft(root, focusedRect);
149 } else {
150 setFocusBottomRight(root, focusedRect);
151 break;
152 }
Fabrice Di Meglio702e8f92012-03-23 18:09:20 -0700153 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800154 }
155 }
Svetoslav Ganov42138042012-03-20 11:51:39 -0700156
Svetoslav Ganov27e2da72012-07-02 18:12:00 -0700157 switch (direction) {
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700158 case View.FOCUS_FORWARD:
159 case View.FOCUS_BACKWARD:
Svetoslav Ganov27e2da72012-07-02 18:12:00 -0700160 return findNextFocusInRelativeDirection(focusables, root, focused, focusedRect,
161 direction);
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700162 case View.FOCUS_UP:
163 case View.FOCUS_DOWN:
164 case View.FOCUS_LEFT:
165 case View.FOCUS_RIGHT:
Svetoslav Ganov27e2da72012-07-02 18:12:00 -0700166 return findNextFocusInAbsoluteDirection(focusables, root, focused,
167 focusedRect, direction);
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700168 default:
Svetoslav Ganov27e2da72012-07-02 18:12:00 -0700169 throw new IllegalArgumentException("Unknown direction: " + direction);
Svetoslav Ganov42138042012-03-20 11:51:39 -0700170 }
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700171 }
Svetoslav Ganov42138042012-03-20 11:51:39 -0700172
Svetoslav Ganov27e2da72012-07-02 18:12:00 -0700173 private View findNextFocusInRelativeDirection(ArrayList<View> focusables, ViewGroup root,
Svetoslav Ganov42138042012-03-20 11:51:39 -0700174 View focused, Rect focusedRect, int direction) {
175 try {
176 // Note: This sort is stable.
177 mSequentialFocusComparator.setRoot(root);
Fabrice Di Meglio3167c882013-05-07 15:39:55 -0700178 mSequentialFocusComparator.setIsLayoutRtl(root.isLayoutRtl());
George Mount140ea622015-10-27 08:46:44 -0700179 mSequentialFocusComparator.setFocusables(focusables);
Svetoslav Ganov42138042012-03-20 11:51:39 -0700180 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:
Fabrice Di Meglio3167c882013-05-07 15:39:55 -0700188 return getNextFocusable(focused, focusables, count);
Svetoslav Ganov42138042012-03-20 11:51:39 -0700189 case View.FOCUS_BACKWARD:
Fabrice Di Meglio3167c882013-05-07 15:39:55 -0700190 return getPreviousFocusable(focused, focusables, count);
Svetoslav Ganov42138042012-03-20 11:51:39 -0700191 }
192 return focusables.get(count - 1);
193 }
194
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700195 private void setFocusBottomRight(ViewGroup root, Rect focusedRect) {
Fabrice Di Meglio702e8f92012-03-23 18:09:20 -0700196 final int rootBottom = root.getScrollY() + root.getHeight();
197 final int rootRight = root.getScrollX() + root.getWidth();
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700198 focusedRect.set(rootRight, rootBottom, rootRight, rootBottom);
Fabrice Di Meglio702e8f92012-03-23 18:09:20 -0700199 }
200
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700201 private void setFocusTopLeft(ViewGroup root, Rect focusedRect) {
Fabrice Di Meglio702e8f92012-03-23 18:09:20 -0700202 final int rootTop = root.getScrollY();
203 final int rootLeft = root.getScrollX();
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700204 focusedRect.set(rootLeft, rootTop, rootLeft, rootTop);
Fabrice Di Meglio702e8f92012-03-23 18:09:20 -0700205 }
206
Svetoslav Ganov27e2da72012-07-02 18:12:00 -0700207 View findNextFocusInAbsoluteDirection(ArrayList<View> focusables, ViewGroup root, View focused,
Svetoslav Ganov42138042012-03-20 11:51:39 -0700208 Rect focusedRect, int direction) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800209 // 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
Tobias Duboisdefdb1e2010-12-15 11:35:30 +0100235 // get focus bounds of other view in same coordinate system
Fabrice Di Meglioc11f77f2012-09-18 15:33:07 -0700236 focusable.getFocusedRect(mOtherRect);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800237 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
Fabrice Di Meglio7e0a3722012-03-27 16:06:25 -0700247 private static View getNextFocusable(View focused, ArrayList<View> focusables, int count) {
Fabrice Di Meglio702e8f92012-03-23 18:09:20 -0700248 if (focused != null) {
249 int position = focusables.lastIndexOf(focused);
250 if (position >= 0 && position + 1 < count) {
251 return focusables.get(position + 1);
252 }
253 }
Svetoslav Ganove5dfa47d2012-05-08 15:58:32 -0700254 if (!focusables.isEmpty()) {
255 return focusables.get(0);
256 }
257 return null;
Fabrice Di Meglio702e8f92012-03-23 18:09:20 -0700258 }
259
Fabrice Di Meglio7e0a3722012-03-27 16:06:25 -0700260 private static View getPreviousFocusable(View focused, ArrayList<View> focusables, int count) {
Fabrice Di Meglio702e8f92012-03-23 18:09:20 -0700261 if (focused != null) {
262 int position = focusables.indexOf(focused);
263 if (position > 0) {
264 return focusables.get(position - 1);
265 }
266 }
Svetoslav Ganove5dfa47d2012-05-08 15:58:32 -0700267 if (!focusables.isEmpty()) {
268 return focusables.get(count - 1);
269 }
270 return null;
Fabrice Di Meglio702e8f92012-03-23 18:09:20 -0700271 }
272
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800273 /**
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 */
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 */
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 */
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 */
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 /**
Fabrice Di Meglio7e0a3722012-03-27 16:06:25 -0700393 * Do the "beams" w.r.t the given direction's axis of rect1 and rect2 overlap?
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800394 * @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 */
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 */
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 */
435 static int majorAxisDistance(int direction, Rect source, Rect dest) {
436 return Math.max(0, majorAxisDistanceRaw(direction, source, dest));
437 }
438
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 */
460 static int majorAxisDistanceToFarEdge(int direction, Rect source, Rect dest) {
461 return Math.max(1, majorAxisDistanceToFarEdgeRaw(direction, source, dest));
462 }
463
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
Fabrice Di Meglio7e0a3722012-03-27 16:06:25 -0700481 * edge of the destination rectangle.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800482 * @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 */
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 */
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 */
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 }
Jeff Brown4e6319b2010-12-13 10:36:51 -0800604
George Mount140ea622015-10-27 08:46:44 -0700605 private static final boolean isValidId(final int id) {
606 return id != 0 && id != View.NO_ID;
607 }
608
Jeff Brown4e6319b2010-12-13 10:36:51 -0800609 /**
610 * Sorts views according to their visual layout and geometry for default tab order.
George Mount140ea622015-10-27 08:46:44 -0700611 * 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.
Jeff Brown4e6319b2010-12-13 10:36:51 -0800615 * 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;
Fabrice Di Meglio3167c882013-05-07 15:39:55 -0700621 private boolean mIsLayoutRtl;
George Mount140ea622015-10-27 08:46:44 -0700622 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>();
Jeff Brown4e6319b2010-12-13 10:36:51 -0800625
626 public void recycle() {
627 mRoot = null;
George Mount140ea622015-10-27 08:46:44 -0700628 mFocusables.clear();
629 mHeadsOfChains.clear();
630 mIsConnectedTo.clear();
Jeff Brown4e6319b2010-12-13 10:36:51 -0800631 }
632
633 public void setRoot(ViewGroup root) {
634 mRoot = root;
635 }
636
Fabrice Di Meglio3167c882013-05-07 15:39:55 -0700637 public void setIsLayoutRtl(boolean b) {
638 mIsLayoutRtl = b;
639 }
640
George Mount140ea622015-10-27 08:46:44 -0700641 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
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
Jeff Brown4e6319b2010-12-13 10:36:51 -0800680 public int compare(View first, View second) {
681 if (first == second) {
682 return 0;
683 }
George Mount140ea622015-10-27 08:46:44 -0700684 // 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 }
Jeff Brown4e6319b2010-12-13 10:36:51 -0800705
George Mount140ea622015-10-27 08:46:44 -0700706 // First see if they belong to the same focus chain.
Jeff Brown4e6319b2010-12-13 10:36:51 -0800707 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) {
Fabrice Di Meglio3167c882013-05-07 15:39:55 -0700715 return mIsLayoutRtl ? 1 : -1;
Jeff Brown4e6319b2010-12-13 10:36:51 -0800716 } else if (mFirstRect.left > mSecondRect.left) {
Fabrice Di Meglio3167c882013-05-07 15:39:55 -0700717 return mIsLayoutRtl ? -1 : 1;
Jeff Brown4e6319b2010-12-13 10:36:51 -0800718 } 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) {
Fabrice Di Meglio3167c882013-05-07 15:39:55 -0700723 return mIsLayoutRtl ? 1 : -1;
Jeff Brown4e6319b2010-12-13 10:36:51 -0800724 } else if (mFirstRect.right > mSecondRect.right) {
Fabrice Di Meglio3167c882013-05-07 15:39:55 -0700725 return mIsLayoutRtl ? -1 : 1;
Jeff Brown4e6319b2010-12-13 10:36:51 -0800726 } 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
734 private void getRect(View view, Rect rect) {
735 view.getDrawingRect(rect);
736 mRoot.offsetDescendantRectToMyCoords(view, rect);
737 }
738 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800739}