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