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