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