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.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.annotation.TestApi;
22 import android.content.pm.PackageManager;
23 import android.graphics.Rect;
24 import android.util.ArrayMap;
25 import android.util.ArraySet;
26 
27 import java.util.ArrayList;
28 import java.util.Arrays;
29 import java.util.Collections;
30 import java.util.Comparator;
31 import java.util.HashMap;
32 import java.util.List;
33 
34 /**
35  * The algorithm used for finding the next focusable view in a given direction
36  * from a view that currently has focus.
37  */
38 public class FocusFinder {
39 
40     private static final ThreadLocal<FocusFinder> tlFocusFinder =
41             new ThreadLocal<FocusFinder>() {
42                 @Override
43                 protected FocusFinder initialValue() {
44                     return new FocusFinder();
45                 }
46             };
47 
48     /**
49      * Get the focus finder for this thread.
50      */
getInstance()51     public static FocusFinder getInstance() {
52         return tlFocusFinder.get();
53     }
54 
55     final Rect mFocusedRect = new Rect();
56     final Rect mOtherRect = new Rect();
57     final Rect mBestCandidateRect = new Rect();
58     private final UserSpecifiedFocusComparator mUserSpecifiedFocusComparator =
59             new UserSpecifiedFocusComparator((r, v) -> isValidId(v.getNextFocusForwardId())
60                             ? v.findUserSetNextFocus(r, View.FOCUS_FORWARD) : null);
61     private final UserSpecifiedFocusComparator mUserSpecifiedClusterComparator =
62             new UserSpecifiedFocusComparator((r, v) -> isValidId(v.getNextClusterForwardId())
63                     ? v.findUserSetNextKeyboardNavigationCluster(r, View.FOCUS_FORWARD) : null);
64     private final FocusSorter mFocusSorter = new FocusSorter();
65 
66     private final ArrayList<View> mTempList = new ArrayList<View>();
67 
68     // enforce thread local access
FocusFinder()69     private FocusFinder() {}
70 
71     /**
72      * Find the next view to take focus in root's descendants, starting from the view
73      * that currently is focused.
74      * @param root Contains focused. Cannot be null.
75      * @param focused Has focus now.
76      * @param direction Direction to look.
77      * @return The next focusable view, or null if none exists.
78      */
findNextFocus(ViewGroup root, View focused, int direction)79     public final View findNextFocus(ViewGroup root, View focused, int direction) {
80         return findNextFocus(root, focused, null, direction);
81     }
82 
83     /**
84      * Find the next view to take focus in root's descendants, searching from
85      * a particular rectangle in root's coordinates.
86      * @param root Contains focusedRect. Cannot be null.
87      * @param focusedRect The starting point of the search.
88      * @param direction Direction to look.
89      * @return The next focusable view, or null if none exists.
90      */
findNextFocusFromRect(ViewGroup root, Rect focusedRect, int direction)91     public View findNextFocusFromRect(ViewGroup root, Rect focusedRect, int direction) {
92         mFocusedRect.set(focusedRect);
93         return findNextFocus(root, null, mFocusedRect, direction);
94     }
95 
findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction)96     private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
97         View next = null;
98         ViewGroup effectiveRoot = getEffectiveRoot(root, focused);
99         if (focused != null) {
100             next = findNextUserSpecifiedFocus(effectiveRoot, focused, direction);
101         }
102         if (next != null) {
103             return next;
104         }
105         ArrayList<View> focusables = mTempList;
106         try {
107             focusables.clear();
108             effectiveRoot.addFocusables(focusables, direction);
109             if (!focusables.isEmpty()) {
110                 next = findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables);
111             }
112         } finally {
113             focusables.clear();
114         }
115         return next;
116     }
117 
118     /**
119      * Returns the "effective" root of a view. The "effective" root is the closest ancestor
120      * within-which focus should cycle.
121      * <p>
122      * For example: normal focus navigation would stay within a ViewGroup marked as
123      * touchscreenBlocksFocus and keyboardNavigationCluster until a cluster-jump out.
124      * @return the "effective" root of {@param focused}
125      */
getEffectiveRoot(ViewGroup root, View focused)126     private ViewGroup getEffectiveRoot(ViewGroup root, View focused) {
127         if (focused == null || focused == root) {
128             return root;
129         }
130         ViewGroup effective = null;
131         ViewParent nextParent = focused.getParent();
132         do {
133             if (nextParent == root) {
134                 return effective != null ? effective : root;
135             }
136             ViewGroup vg = (ViewGroup) nextParent;
137             if (vg.getTouchscreenBlocksFocus()
138                     && focused.getContext().getPackageManager().hasSystemFeature(
139                             PackageManager.FEATURE_TOUCHSCREEN)
140                     && vg.isKeyboardNavigationCluster()) {
141                 // Don't stop and return here because the cluster could be nested and we only
142                 // care about the top-most one.
143                 effective = vg;
144             }
145             nextParent = nextParent.getParent();
146         } while (nextParent instanceof ViewGroup);
147         return root;
148     }
149 
150     /**
151      * Find the root of the next keyboard navigation cluster after the current one.
152      * @param root The view tree to look inside. Cannot be null
153      * @param currentCluster The starting point of the search. Null means the default cluster
154      * @param direction Direction to look
155      * @return The next cluster, or null if none exists
156      */
findNextKeyboardNavigationCluster( @onNull View root, @Nullable View currentCluster, @View.FocusDirection int direction)157     public View findNextKeyboardNavigationCluster(
158             @NonNull View root,
159             @Nullable View currentCluster,
160             @View.FocusDirection int direction) {
161         View next = null;
162         if (currentCluster != null) {
163             next = findNextUserSpecifiedKeyboardNavigationCluster(root, currentCluster, direction);
164             if (next != null) {
165                 return next;
166             }
167         }
168 
169         final ArrayList<View> clusters = mTempList;
170         try {
171             clusters.clear();
172             root.addKeyboardNavigationClusters(clusters, direction);
173             if (!clusters.isEmpty()) {
174                 next = findNextKeyboardNavigationCluster(
175                         root, currentCluster, clusters, direction);
176             }
177         } finally {
178             clusters.clear();
179         }
180         return next;
181     }
182 
findNextUserSpecifiedKeyboardNavigationCluster(View root, View currentCluster, int direction)183     private View findNextUserSpecifiedKeyboardNavigationCluster(View root, View currentCluster,
184             int direction) {
185         View userSetNextCluster =
186                 currentCluster.findUserSetNextKeyboardNavigationCluster(root, direction);
187         if (userSetNextCluster != null && userSetNextCluster.hasFocusable()) {
188             return userSetNextCluster;
189         }
190         return null;
191     }
192 
findNextUserSpecifiedFocus(ViewGroup root, View focused, int direction)193     private View findNextUserSpecifiedFocus(ViewGroup root, View focused, int direction) {
194         // check for user specified next focus
195         View userSetNextFocus = focused.findUserSetNextFocus(root, direction);
196         while (userSetNextFocus != null) {
197             if (userSetNextFocus.isFocusable()
198                     && userSetNextFocus.getVisibility() == View.VISIBLE
199                     && (!userSetNextFocus.isInTouchMode()
200                             || userSetNextFocus.isFocusableInTouchMode())) {
201                 return userSetNextFocus;
202             }
203             userSetNextFocus = userSetNextFocus.findUserSetNextFocus(root, direction);
204         }
205         return null;
206     }
207 
findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction, ArrayList<View> focusables)208     private View findNextFocus(ViewGroup root, View focused, Rect focusedRect,
209             int direction, ArrayList<View> focusables) {
210         if (focused != null) {
211             if (focusedRect == null) {
212                 focusedRect = mFocusedRect;
213             }
214             // fill in interesting rect from focused
215             focused.getFocusedRect(focusedRect);
216             root.offsetDescendantRectToMyCoords(focused, focusedRect);
217         } else {
218             if (focusedRect == null) {
219                 focusedRect = mFocusedRect;
220                 // make up a rect at top left or bottom right of root
221                 switch (direction) {
222                     case View.FOCUS_RIGHT:
223                     case View.FOCUS_DOWN:
224                         setFocusTopLeft(root, focusedRect);
225                         break;
226                     case View.FOCUS_FORWARD:
227                         if (root.isLayoutRtl()) {
228                             setFocusBottomRight(root, focusedRect);
229                         } else {
230                             setFocusTopLeft(root, focusedRect);
231                         }
232                         break;
233 
234                     case View.FOCUS_LEFT:
235                     case View.FOCUS_UP:
236                         setFocusBottomRight(root, focusedRect);
237                         break;
238                     case View.FOCUS_BACKWARD:
239                         if (root.isLayoutRtl()) {
240                             setFocusTopLeft(root, focusedRect);
241                         } else {
242                             setFocusBottomRight(root, focusedRect);
243                         break;
244                     }
245                 }
246             }
247         }
248 
249         switch (direction) {
250             case View.FOCUS_FORWARD:
251             case View.FOCUS_BACKWARD:
252                 return findNextFocusInRelativeDirection(focusables, root, focused, focusedRect,
253                         direction);
254             case View.FOCUS_UP:
255             case View.FOCUS_DOWN:
256             case View.FOCUS_LEFT:
257             case View.FOCUS_RIGHT:
258                 return findNextFocusInAbsoluteDirection(focusables, root, focused,
259                         focusedRect, direction);
260             default:
261                 throw new IllegalArgumentException("Unknown direction: " + direction);
262         }
263     }
264 
findNextKeyboardNavigationCluster( View root, View currentCluster, List<View> clusters, @View.FocusDirection int direction)265     private View findNextKeyboardNavigationCluster(
266             View root,
267             View currentCluster,
268             List<View> clusters,
269             @View.FocusDirection int direction) {
270         try {
271             // Note: This sort is stable.
272             mUserSpecifiedClusterComparator.setFocusables(clusters, root);
273             Collections.sort(clusters, mUserSpecifiedClusterComparator);
274         } finally {
275             mUserSpecifiedClusterComparator.recycle();
276         }
277         final int count = clusters.size();
278 
279         switch (direction) {
280             case View.FOCUS_FORWARD:
281             case View.FOCUS_DOWN:
282             case View.FOCUS_RIGHT:
283                 return getNextKeyboardNavigationCluster(root, currentCluster, clusters, count);
284             case View.FOCUS_BACKWARD:
285             case View.FOCUS_UP:
286             case View.FOCUS_LEFT:
287                 return getPreviousKeyboardNavigationCluster(root, currentCluster, clusters, count);
288             default:
289                 throw new IllegalArgumentException("Unknown direction: " + direction);
290         }
291     }
292 
findNextFocusInRelativeDirection(ArrayList<View> focusables, ViewGroup root, View focused, Rect focusedRect, int direction)293     private View findNextFocusInRelativeDirection(ArrayList<View> focusables, ViewGroup root,
294             View focused, Rect focusedRect, int direction) {
295         try {
296             // Note: This sort is stable.
297             mUserSpecifiedFocusComparator.setFocusables(focusables, root);
298             Collections.sort(focusables, mUserSpecifiedFocusComparator);
299         } finally {
300             mUserSpecifiedFocusComparator.recycle();
301         }
302 
303         final int count = focusables.size();
304         switch (direction) {
305             case View.FOCUS_FORWARD:
306                 return getNextFocusable(focused, focusables, count);
307             case View.FOCUS_BACKWARD:
308                 return getPreviousFocusable(focused, focusables, count);
309         }
310         return focusables.get(count - 1);
311     }
312 
setFocusBottomRight(ViewGroup root, Rect focusedRect)313     private void setFocusBottomRight(ViewGroup root, Rect focusedRect) {
314         final int rootBottom = root.getScrollY() + root.getHeight();
315         final int rootRight = root.getScrollX() + root.getWidth();
316         focusedRect.set(rootRight, rootBottom, rootRight, rootBottom);
317     }
318 
setFocusTopLeft(ViewGroup root, Rect focusedRect)319     private void setFocusTopLeft(ViewGroup root, Rect focusedRect) {
320         final int rootTop = root.getScrollY();
321         final int rootLeft = root.getScrollX();
322         focusedRect.set(rootLeft, rootTop, rootLeft, rootTop);
323     }
324 
findNextFocusInAbsoluteDirection(ArrayList<View> focusables, ViewGroup root, View focused, Rect focusedRect, int direction)325     View findNextFocusInAbsoluteDirection(ArrayList<View> focusables, ViewGroup root, View focused,
326             Rect focusedRect, int direction) {
327         // initialize the best candidate to something impossible
328         // (so the first plausible view will become the best choice)
329         mBestCandidateRect.set(focusedRect);
330         switch(direction) {
331             case View.FOCUS_LEFT:
332                 mBestCandidateRect.offset(focusedRect.width() + 1, 0);
333                 break;
334             case View.FOCUS_RIGHT:
335                 mBestCandidateRect.offset(-(focusedRect.width() + 1), 0);
336                 break;
337             case View.FOCUS_UP:
338                 mBestCandidateRect.offset(0, focusedRect.height() + 1);
339                 break;
340             case View.FOCUS_DOWN:
341                 mBestCandidateRect.offset(0, -(focusedRect.height() + 1));
342         }
343 
344         View closest = null;
345 
346         int numFocusables = focusables.size();
347         for (int i = 0; i < numFocusables; i++) {
348             View focusable = focusables.get(i);
349 
350             // only interested in other non-root views
351             if (focusable == focused || focusable == root) continue;
352 
353             // get focus bounds of other view in same coordinate system
354             focusable.getFocusedRect(mOtherRect);
355             root.offsetDescendantRectToMyCoords(focusable, mOtherRect);
356 
357             if (isBetterCandidate(direction, focusedRect, mOtherRect, mBestCandidateRect)) {
358                 mBestCandidateRect.set(mOtherRect);
359                 closest = focusable;
360             }
361         }
362         return closest;
363     }
364 
getNextFocusable(View focused, ArrayList<View> focusables, int count)365     private static View getNextFocusable(View focused, ArrayList<View> focusables, int count) {
366         if (focused != null) {
367             int position = focusables.lastIndexOf(focused);
368             if (position >= 0 && position + 1 < count) {
369                 return focusables.get(position + 1);
370             }
371         }
372         if (!focusables.isEmpty()) {
373             return focusables.get(0);
374         }
375         return null;
376     }
377 
getPreviousFocusable(View focused, ArrayList<View> focusables, int count)378     private static View getPreviousFocusable(View focused, ArrayList<View> focusables, int count) {
379         if (focused != null) {
380             int position = focusables.indexOf(focused);
381             if (position > 0) {
382                 return focusables.get(position - 1);
383             }
384         }
385         if (!focusables.isEmpty()) {
386             return focusables.get(count - 1);
387         }
388         return null;
389     }
390 
getNextKeyboardNavigationCluster( View root, View currentCluster, List<View> clusters, int count)391     private static View getNextKeyboardNavigationCluster(
392             View root,
393             View currentCluster,
394             List<View> clusters,
395             int count) {
396         if (currentCluster == null) {
397             // The current cluster is the default one.
398             // The next cluster after the default one is the first one.
399             // Note that the caller guarantees that 'clusters' is not empty.
400             return clusters.get(0);
401         }
402 
403         final int position = clusters.lastIndexOf(currentCluster);
404         if (position >= 0 && position + 1 < count) {
405             // Return the next non-default cluster if we can find it.
406             return clusters.get(position + 1);
407         }
408 
409         // The current cluster is the last one. The next one is the default one, i.e. the
410         // root.
411         return root;
412     }
413 
getPreviousKeyboardNavigationCluster( View root, View currentCluster, List<View> clusters, int count)414     private static View getPreviousKeyboardNavigationCluster(
415             View root,
416             View currentCluster,
417             List<View> clusters,
418             int count) {
419         if (currentCluster == null) {
420             // The current cluster is the default one.
421             // The previous cluster before the default one is the last one.
422             // Note that the caller guarantees that 'clusters' is not empty.
423             return clusters.get(count - 1);
424         }
425 
426         final int position = clusters.indexOf(currentCluster);
427         if (position > 0) {
428             // Return the previous non-default cluster if we can find it.
429             return clusters.get(position - 1);
430         }
431 
432         // The current cluster is the first one. The previous one is the default one, i.e.
433         // the root.
434         return root;
435     }
436 
437     /**
438      * Is rect1 a better candidate than rect2 for a focus search in a particular
439      * direction from a source rect?  This is the core routine that determines
440      * the order of focus searching.
441      * @param direction the direction (up, down, left, right)
442      * @param source The source we are searching from
443      * @param rect1 The candidate rectangle
444      * @param rect2 The current best candidate.
445      * @return Whether the candidate is the new best.
446      */
isBetterCandidate(int direction, Rect source, Rect rect1, Rect rect2)447     boolean isBetterCandidate(int direction, Rect source, Rect rect1, Rect rect2) {
448 
449         // to be a better candidate, need to at least be a candidate in the first
450         // place :)
451         if (!isCandidate(source, rect1, direction)) {
452             return false;
453         }
454 
455         // we know that rect1 is a candidate.. if rect2 is not a candidate,
456         // rect1 is better
457         if (!isCandidate(source, rect2, direction)) {
458             return true;
459         }
460 
461         // if rect1 is better by beam, it wins
462         if (beamBeats(direction, source, rect1, rect2)) {
463             return true;
464         }
465 
466         // if rect2 is better, then rect1 cant' be :)
467         if (beamBeats(direction, source, rect2, rect1)) {
468             return false;
469         }
470 
471         // otherwise, do fudge-tastic comparison of the major and minor axis
472         return (getWeightedDistanceFor(
473                         majorAxisDistance(direction, source, rect1),
474                         minorAxisDistance(direction, source, rect1))
475                 < getWeightedDistanceFor(
476                         majorAxisDistance(direction, source, rect2),
477                         minorAxisDistance(direction, source, rect2)));
478     }
479 
480     /**
481      * One rectangle may be another candidate than another by virtue of being
482      * exclusively in the beam of the source rect.
483      * @return Whether rect1 is a better candidate than rect2 by virtue of it being in src's
484      *      beam
485      */
beamBeats(int direction, Rect source, Rect rect1, Rect rect2)486     boolean beamBeats(int direction, Rect source, Rect rect1, Rect rect2) {
487         final boolean rect1InSrcBeam = beamsOverlap(direction, source, rect1);
488         final boolean rect2InSrcBeam = beamsOverlap(direction, source, rect2);
489 
490         // if rect1 isn't exclusively in the src beam, it doesn't win
491         if (rect2InSrcBeam || !rect1InSrcBeam) {
492             return false;
493         }
494 
495         // we know rect1 is in the beam, and rect2 is not
496 
497         // if rect1 is to the direction of, and rect2 is not, rect1 wins.
498         // for example, for direction left, if rect1 is to the left of the source
499         // and rect2 is below, then we always prefer the in beam rect1, since rect2
500         // could be reached by going down.
501         if (!isToDirectionOf(direction, source, rect2)) {
502             return true;
503         }
504 
505         // for horizontal directions, being exclusively in beam always wins
506         if ((direction == View.FOCUS_LEFT || direction == View.FOCUS_RIGHT)) {
507             return true;
508         }
509 
510         // for vertical directions, beams only beat up to a point:
511         // now, as long as rect2 isn't completely closer, rect1 wins
512         // e.g for direction down, completely closer means for rect2's top
513         // edge to be closer to the source's top edge than rect1's bottom edge.
514         return (majorAxisDistance(direction, source, rect1)
515                 < majorAxisDistanceToFarEdge(direction, source, rect2));
516     }
517 
518     /**
519      * Fudge-factor opportunity: how to calculate distance given major and minor
520      * axis distances.  Warning: this fudge factor is finely tuned, be sure to
521      * run all focus tests if you dare tweak it.
522      */
getWeightedDistanceFor(int majorAxisDistance, int minorAxisDistance)523     int getWeightedDistanceFor(int majorAxisDistance, int minorAxisDistance) {
524         return 13 * majorAxisDistance * majorAxisDistance
525                 + minorAxisDistance * minorAxisDistance;
526     }
527 
528     /**
529      * Is destRect a candidate for the next focus given the direction?  This
530      * checks whether the dest is at least partially to the direction of (e.g left of)
531      * from source.
532      *
533      * Includes an edge case for an empty rect (which is used in some cases when
534      * searching from a point on the screen).
535      */
isCandidate(Rect srcRect, Rect destRect, int direction)536     boolean isCandidate(Rect srcRect, Rect destRect, int direction) {
537         switch (direction) {
538             case View.FOCUS_LEFT:
539                 return (srcRect.right > destRect.right || srcRect.left >= destRect.right)
540                         && srcRect.left > destRect.left;
541             case View.FOCUS_RIGHT:
542                 return (srcRect.left < destRect.left || srcRect.right <= destRect.left)
543                         && srcRect.right < destRect.right;
544             case View.FOCUS_UP:
545                 return (srcRect.bottom > destRect.bottom || srcRect.top >= destRect.bottom)
546                         && srcRect.top > destRect.top;
547             case View.FOCUS_DOWN:
548                 return (srcRect.top < destRect.top || srcRect.bottom <= destRect.top)
549                         && srcRect.bottom < destRect.bottom;
550         }
551         throw new IllegalArgumentException("direction must be one of "
552                 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
553     }
554 
555 
556     /**
557      * Do the "beams" w.r.t the given direction's axis of rect1 and rect2 overlap?
558      * @param direction the direction (up, down, left, right)
559      * @param rect1 The first rectangle
560      * @param rect2 The second rectangle
561      * @return whether the beams overlap
562      */
beamsOverlap(int direction, Rect rect1, Rect rect2)563     boolean beamsOverlap(int direction, Rect rect1, Rect rect2) {
564         switch (direction) {
565             case View.FOCUS_LEFT:
566             case View.FOCUS_RIGHT:
567                 return (rect2.bottom >= rect1.top) && (rect2.top <= rect1.bottom);
568             case View.FOCUS_UP:
569             case View.FOCUS_DOWN:
570                 return (rect2.right >= rect1.left) && (rect2.left <= rect1.right);
571         }
572         throw new IllegalArgumentException("direction must be one of "
573                 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
574     }
575 
576     /**
577      * e.g for left, is 'to left of'
578      */
isToDirectionOf(int direction, Rect src, Rect dest)579     boolean isToDirectionOf(int direction, Rect src, Rect dest) {
580         switch (direction) {
581             case View.FOCUS_LEFT:
582                 return src.left >= dest.right;
583             case View.FOCUS_RIGHT:
584                 return src.right <= dest.left;
585             case View.FOCUS_UP:
586                 return src.top >= dest.bottom;
587             case View.FOCUS_DOWN:
588                 return src.bottom <= dest.top;
589         }
590         throw new IllegalArgumentException("direction must be one of "
591                 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
592     }
593 
594     /**
595      * @return The distance from the edge furthest in the given direction
596      *   of source to the edge nearest in the given direction of dest.  If the
597      *   dest is not in the direction from source, return 0.
598      */
majorAxisDistance(int direction, Rect source, Rect dest)599     static int majorAxisDistance(int direction, Rect source, Rect dest) {
600         return Math.max(0, majorAxisDistanceRaw(direction, source, dest));
601     }
602 
majorAxisDistanceRaw(int direction, Rect source, Rect dest)603     static int majorAxisDistanceRaw(int direction, Rect source, Rect dest) {
604         switch (direction) {
605             case View.FOCUS_LEFT:
606                 return source.left - dest.right;
607             case View.FOCUS_RIGHT:
608                 return dest.left - source.right;
609             case View.FOCUS_UP:
610                 return source.top - dest.bottom;
611             case View.FOCUS_DOWN:
612                 return dest.top - source.bottom;
613         }
614         throw new IllegalArgumentException("direction must be one of "
615                 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
616     }
617 
618     /**
619      * @return The distance along the major axis w.r.t the direction from the
620      *   edge of source to the far edge of dest. If the
621      *   dest is not in the direction from source, return 1 (to break ties with
622      *   {@link #majorAxisDistance}).
623      */
majorAxisDistanceToFarEdge(int direction, Rect source, Rect dest)624     static int majorAxisDistanceToFarEdge(int direction, Rect source, Rect dest) {
625         return Math.max(1, majorAxisDistanceToFarEdgeRaw(direction, source, dest));
626     }
627 
majorAxisDistanceToFarEdgeRaw(int direction, Rect source, Rect dest)628     static int majorAxisDistanceToFarEdgeRaw(int direction, Rect source, Rect dest) {
629         switch (direction) {
630             case View.FOCUS_LEFT:
631                 return source.left - dest.left;
632             case View.FOCUS_RIGHT:
633                 return dest.right - source.right;
634             case View.FOCUS_UP:
635                 return source.top - dest.top;
636             case View.FOCUS_DOWN:
637                 return dest.bottom - source.bottom;
638         }
639         throw new IllegalArgumentException("direction must be one of "
640                 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
641     }
642 
643     /**
644      * Find the distance on the minor axis w.r.t the direction to the nearest
645      * edge of the destination rectangle.
646      * @param direction the direction (up, down, left, right)
647      * @param source The source rect.
648      * @param dest The destination rect.
649      * @return The distance.
650      */
minorAxisDistance(int direction, Rect source, Rect dest)651     static int minorAxisDistance(int direction, Rect source, Rect dest) {
652         switch (direction) {
653             case View.FOCUS_LEFT:
654             case View.FOCUS_RIGHT:
655                 // the distance between the center verticals
656                 return Math.abs(
657                         ((source.top + source.height() / 2) -
658                         ((dest.top + dest.height() / 2))));
659             case View.FOCUS_UP:
660             case View.FOCUS_DOWN:
661                 // the distance between the center horizontals
662                 return Math.abs(
663                         ((source.left + source.width() / 2) -
664                         ((dest.left + dest.width() / 2))));
665         }
666         throw new IllegalArgumentException("direction must be one of "
667                 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
668     }
669 
670     /**
671      * Find the nearest touchable view to the specified view.
672      *
673      * @param root The root of the tree in which to search
674      * @param x X coordinate from which to start the search
675      * @param y Y coordinate from which to start the search
676      * @param direction Direction to look
677      * @param deltas Offset from the <x, y> to the edge of the nearest view. Note that this array
678      *        may already be populated with values.
679      * @return The nearest touchable view, or null if none exists.
680      */
findNearestTouchable(ViewGroup root, int x, int y, int direction, int[] deltas)681     public View findNearestTouchable(ViewGroup root, int x, int y, int direction, int[] deltas) {
682         ArrayList<View> touchables = root.getTouchables();
683         int minDistance = Integer.MAX_VALUE;
684         View closest = null;
685 
686         int numTouchables = touchables.size();
687 
688         int edgeSlop = ViewConfiguration.get(root.mContext).getScaledEdgeSlop();
689 
690         Rect closestBounds = new Rect();
691         Rect touchableBounds = mOtherRect;
692 
693         for (int i = 0; i < numTouchables; i++) {
694             View touchable = touchables.get(i);
695 
696             // get visible bounds of other view in same coordinate system
697             touchable.getDrawingRect(touchableBounds);
698 
699             root.offsetRectBetweenParentAndChild(touchable, touchableBounds, true, true);
700 
701             if (!isTouchCandidate(x, y, touchableBounds, direction)) {
702                 continue;
703             }
704 
705             int distance = Integer.MAX_VALUE;
706 
707             switch (direction) {
708             case View.FOCUS_LEFT:
709                 distance = x - touchableBounds.right + 1;
710                 break;
711             case View.FOCUS_RIGHT:
712                 distance = touchableBounds.left;
713                 break;
714             case View.FOCUS_UP:
715                 distance = y - touchableBounds.bottom + 1;
716                 break;
717             case View.FOCUS_DOWN:
718                 distance = touchableBounds.top;
719                 break;
720             }
721 
722             if (distance < edgeSlop) {
723                 // Give preference to innermost views
724                 if (closest == null ||
725                         closestBounds.contains(touchableBounds) ||
726                         (!touchableBounds.contains(closestBounds) && distance < minDistance)) {
727                     minDistance = distance;
728                     closest = touchable;
729                     closestBounds.set(touchableBounds);
730                     switch (direction) {
731                     case View.FOCUS_LEFT:
732                         deltas[0] = -distance;
733                         break;
734                     case View.FOCUS_RIGHT:
735                         deltas[0] = distance;
736                         break;
737                     case View.FOCUS_UP:
738                         deltas[1] = -distance;
739                         break;
740                     case View.FOCUS_DOWN:
741                         deltas[1] = distance;
742                         break;
743                     }
744                 }
745             }
746         }
747         return closest;
748     }
749 
750 
751     /**
752      * Is destRect a candidate for the next touch given the direction?
753      */
isTouchCandidate(int x, int y, Rect destRect, int direction)754     private boolean isTouchCandidate(int x, int y, Rect destRect, int direction) {
755         switch (direction) {
756             case View.FOCUS_LEFT:
757                 return destRect.left <= x && destRect.top <= y && y <= destRect.bottom;
758             case View.FOCUS_RIGHT:
759                 return destRect.left >= x && destRect.top <= y && y <= destRect.bottom;
760             case View.FOCUS_UP:
761                 return destRect.top <= y && destRect.left <= x && x <= destRect.right;
762             case View.FOCUS_DOWN:
763                 return destRect.top >= y && destRect.left <= x && x <= destRect.right;
764         }
765         throw new IllegalArgumentException("direction must be one of "
766                 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
767     }
768 
isValidId(final int id)769     private static final boolean isValidId(final int id) {
770         return id != 0 && id != View.NO_ID;
771     }
772 
773     static final class FocusSorter {
774         private ArrayList<Rect> mRectPool = new ArrayList<>();
775         private int mLastPoolRect;
776         private int mRtlMult;
777         private HashMap<View, Rect> mRectByView = null;
778 
779         private Comparator<View> mTopsComparator = (first, second) -> {
780             if (first == second) {
781                 return 0;
782             }
783 
784             Rect firstRect = mRectByView.get(first);
785             Rect secondRect = mRectByView.get(second);
786 
787             int result = firstRect.top - secondRect.top;
788             if (result == 0) {
789                 return firstRect.bottom - secondRect.bottom;
790             }
791             return result;
792         };
793 
794         private Comparator<View> mSidesComparator = (first, second) -> {
795             if (first == second) {
796                 return 0;
797             }
798 
799             Rect firstRect = mRectByView.get(first);
800             Rect secondRect = mRectByView.get(second);
801 
802             int result = firstRect.left - secondRect.left;
803             if (result == 0) {
804                 return firstRect.right - secondRect.right;
805             }
806             return mRtlMult * result;
807         };
808 
sort(View[] views, int start, int end, ViewGroup root, boolean isRtl)809         public void sort(View[] views, int start, int end, ViewGroup root, boolean isRtl) {
810             int count = end - start;
811             if (count < 2) {
812                 return;
813             }
814             if (mRectByView == null) {
815                 mRectByView = new HashMap<>();
816             }
817             mRtlMult = isRtl ? -1 : 1;
818             for (int i = mRectPool.size(); i < count; ++i) {
819                 mRectPool.add(new Rect());
820             }
821             for (int i = start; i < end; ++i) {
822                 Rect next = mRectPool.get(mLastPoolRect++);
823                 views[i].getDrawingRect(next);
824                 root.offsetDescendantRectToMyCoords(views[i], next);
825                 mRectByView.put(views[i], next);
826             }
827 
828             // Sort top-to-bottom
829             Arrays.sort(views, start, count, mTopsComparator);
830             // Sweep top-to-bottom to identify rows
831             int sweepBottom = mRectByView.get(views[start]).bottom;
832             int rowStart = start;
833             int sweepIdx = start + 1;
834             for (; sweepIdx < end; ++sweepIdx) {
835                 Rect currRect = mRectByView.get(views[sweepIdx]);
836                 if (currRect.top >= sweepBottom) {
837                     // Next view is on a new row, sort the row we've just finished left-to-right.
838                     if ((sweepIdx - rowStart) > 1) {
839                         Arrays.sort(views, rowStart, sweepIdx, mSidesComparator);
840                     }
841                     sweepBottom = currRect.bottom;
842                     rowStart = sweepIdx;
843                 } else {
844                     // Next view vertically overlaps, we need to extend our "row height"
845                     sweepBottom = Math.max(sweepBottom, currRect.bottom);
846                 }
847             }
848             // Sort whatever's left (final row) left-to-right
849             if ((sweepIdx - rowStart) > 1) {
850                 Arrays.sort(views, rowStart, sweepIdx, mSidesComparator);
851             }
852 
853             mLastPoolRect = 0;
854             mRectByView.clear();
855         }
856     }
857 
858     /**
859      * Public for testing.
860      *
861      * @hide
862      */
863     @TestApi
sort(View[] views, int start, int end, ViewGroup root, boolean isRtl)864     public static void sort(View[] views, int start, int end, ViewGroup root, boolean isRtl) {
865         getInstance().mFocusSorter.sort(views, start, end, root, isRtl);
866     }
867 
868     /**
869      * Sorts views according to any explicitly-specified focus-chains. If there are no explicitly
870      * specified focus chains (eg. no nextFocusForward attributes defined), this should be a no-op.
871      */
872     private static final class UserSpecifiedFocusComparator implements Comparator<View> {
873         private final ArrayMap<View, View> mNextFoci = new ArrayMap<>();
874         private final ArraySet<View> mIsConnectedTo = new ArraySet<>();
875         private final ArrayMap<View, View> mHeadsOfChains = new ArrayMap<View, View>();
876         private final ArrayMap<View, Integer> mOriginalOrdinal = new ArrayMap<>();
877         private final NextFocusGetter mNextFocusGetter;
878         private View mRoot;
879 
880         public interface NextFocusGetter {
get(View root, View view)881             View get(View root, View view);
882         }
883 
UserSpecifiedFocusComparator(NextFocusGetter nextFocusGetter)884         UserSpecifiedFocusComparator(NextFocusGetter nextFocusGetter) {
885             mNextFocusGetter = nextFocusGetter;
886         }
887 
recycle()888         public void recycle() {
889             mRoot = null;
890             mHeadsOfChains.clear();
891             mIsConnectedTo.clear();
892             mOriginalOrdinal.clear();
893             mNextFoci.clear();
894         }
895 
setFocusables(List<View> focusables, View root)896         public void setFocusables(List<View> focusables, View root) {
897             mRoot = root;
898             for (int i = 0; i < focusables.size(); ++i) {
899                 mOriginalOrdinal.put(focusables.get(i), i);
900             }
901 
902             for (int i = focusables.size() - 1; i >= 0; i--) {
903                 final View view = focusables.get(i);
904                 final View next = mNextFocusGetter.get(mRoot, view);
905                 if (next != null && mOriginalOrdinal.containsKey(next)) {
906                     mNextFoci.put(view, next);
907                     mIsConnectedTo.add(next);
908                 }
909             }
910 
911             for (int i = focusables.size() - 1; i >= 0; i--) {
912                 final View view = focusables.get(i);
913                 final View next = mNextFoci.get(view);
914                 if (next != null && !mIsConnectedTo.contains(view)) {
915                     setHeadOfChain(view);
916                 }
917             }
918         }
919 
setHeadOfChain(View head)920         private void setHeadOfChain(View head) {
921             for (View view = head; view != null; view = mNextFoci.get(view)) {
922                 final View otherHead = mHeadsOfChains.get(view);
923                 if (otherHead != null) {
924                     if (otherHead == head) {
925                         return; // This view has already had its head set properly
926                     }
927                     // A hydra -- multi-headed focus chain (e.g. A->C and B->C)
928                     // Use the one we've already chosen instead and reset this chain.
929                     view = head;
930                     head = otherHead;
931                 }
932                 mHeadsOfChains.put(view, head);
933             }
934         }
935 
compare(View first, View second)936         public int compare(View first, View second) {
937             if (first == second) {
938                 return 0;
939             }
940             // Order between views within a chain is immaterial -- next/previous is
941             // within a chain is handled elsewhere.
942             View firstHead = mHeadsOfChains.get(first);
943             View secondHead = mHeadsOfChains.get(second);
944             if (firstHead == secondHead && firstHead != null) {
945                 if (first == firstHead) {
946                     return -1; // first is the head, it should be first
947                 } else if (second == firstHead) {
948                     return 1; // second is the head, it should be first
949                 } else if (mNextFoci.get(first) != null) {
950                     return -1; // first is not the end of the chain
951                 } else {
952                     return 1; // first is end of chain
953                 }
954             }
955             boolean involvesChain = false;
956             if (firstHead != null) {
957                 first = firstHead;
958                 involvesChain = true;
959             }
960             if (secondHead != null) {
961                 second = secondHead;
962                 involvesChain = true;
963             }
964 
965             if (involvesChain) {
966                 // keep original order between chains
967                 return mOriginalOrdinal.get(first) < mOriginalOrdinal.get(second) ? -1 : 1;
968             } else {
969                 return 0;
970             }
971         }
972     }
973 }
974