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