Home | History | Annotate | Download | only in view
      1 /*
      2  * Copyright (C) 2007 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.view;
     18 
     19 import android.graphics.Rect;
     20 
     21 import java.util.ArrayList;
     22 import java.util.Collections;
     23 import java.util.Comparator;
     24 
     25 /**
     26  * The algorithm used for finding the next focusable view in a given direction
     27  * from a view that currently has focus.
     28  */
     29 public class FocusFinder {
     30 
     31     private static ThreadLocal<FocusFinder> tlFocusFinder =
     32             new ThreadLocal<FocusFinder>() {
     33 
     34                 protected FocusFinder initialValue() {
     35                     return new FocusFinder();
     36                 }
     37             };
     38 
     39     /**
     40      * Get the focus finder for this thread.
     41      */
     42     public static FocusFinder getInstance() {
     43         return tlFocusFinder.get();
     44     }
     45 
     46     Rect mFocusedRect = new Rect();
     47     Rect mOtherRect = new Rect();
     48     Rect mBestCandidateRect = new Rect();
     49     SequentialFocusComparator mSequentialFocusComparator = new SequentialFocusComparator();
     50 
     51     // enforce thread local access
     52     private FocusFinder() {}
     53 
     54     /**
     55      * Find the next view to take focus in root's descendants, starting from the view
     56      * that currently is focused.
     57      * @param root Contains focused
     58      * @param focused Has focus now.
     59      * @param direction Direction to look.
     60      * @return The next focusable view, or null if none exists.
     61      */
     62     public final View findNextFocus(ViewGroup root, View focused, int direction) {
     63 
     64         if (focused != null) {
     65             // check for user specified next focus
     66             View userSetNextFocus = focused.findUserSetNextFocus(root, direction);
     67             if (userSetNextFocus != null &&
     68                 userSetNextFocus.isFocusable() &&
     69                 (!userSetNextFocus.isInTouchMode() ||
     70                  userSetNextFocus.isFocusableInTouchMode())) {
     71                 return userSetNextFocus;
     72             }
     73 
     74             // fill in interesting rect from focused
     75             focused.getFocusedRect(mFocusedRect);
     76             root.offsetDescendantRectToMyCoords(focused, mFocusedRect);
     77         } else {
     78             // make up a rect at top left or bottom right of root
     79             switch (direction) {
     80                 case View.FOCUS_RIGHT:
     81                 case View.FOCUS_DOWN:
     82                 case View.FOCUS_FORWARD:
     83                     final int rootTop = root.getScrollY();
     84                     final int rootLeft = root.getScrollX();
     85                     mFocusedRect.set(rootLeft, rootTop, rootLeft, rootTop);
     86                     break;
     87 
     88                 case View.FOCUS_LEFT:
     89                 case View.FOCUS_UP:
     90                 case View.FOCUS_BACKWARD:
     91                     final int rootBottom = root.getScrollY() + root.getHeight();
     92                     final int rootRight = root.getScrollX() + root.getWidth();
     93                     mFocusedRect.set(rootRight, rootBottom,
     94                             rootRight, rootBottom);
     95                     break;
     96             }
     97         }
     98         return findNextFocus(root, focused, mFocusedRect, direction);
     99     }
    100 
    101     /**
    102      * Find the next view to take focus in root's descendants, searching from
    103      * a particular rectangle in root's coordinates.
    104      * @param root Contains focusedRect.
    105      * @param focusedRect The starting point of the search.
    106      * @param direction Direction to look.
    107      * @return The next focusable view, or null if none exists.
    108      */
    109     public View findNextFocusFromRect(ViewGroup root, Rect focusedRect, int direction) {
    110         return findNextFocus(root, null, focusedRect, direction);
    111     }
    112 
    113     private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
    114         ArrayList<View> focusables = root.getFocusables(direction);
    115         if (focusables.isEmpty()) {
    116             // The focus cannot change.
    117             return null;
    118         }
    119 
    120         if (direction == View.FOCUS_FORWARD || direction == View.FOCUS_BACKWARD) {
    121             if (focused != null && !focusables.contains(focused)) {
    122                 // Add the currently focused view to the list to have it sorted
    123                 // along with the other views.
    124                 focusables.add(focused);
    125             }
    126 
    127             try {
    128                 // Note: This sort is stable.
    129                 mSequentialFocusComparator.setRoot(root);
    130                 Collections.sort(focusables, mSequentialFocusComparator);
    131             } finally {
    132                 mSequentialFocusComparator.recycle();
    133             }
    134 
    135             final int count = focusables.size();
    136             switch (direction) {
    137                 case View.FOCUS_FORWARD:
    138                     if (focused != null) {
    139                         int position = focusables.lastIndexOf(focused);
    140                         if (position >= 0 && position + 1 < count) {
    141                             return focusables.get(position + 1);
    142                         }
    143                     }
    144                     return focusables.get(0);
    145 
    146                 case View.FOCUS_BACKWARD:
    147                     if (focused != null) {
    148                         int position = focusables.indexOf(focused);
    149                         if (position > 0) {
    150                             return focusables.get(position - 1);
    151                         }
    152                     }
    153                     return focusables.get(count - 1);
    154             }
    155             return null;
    156         }
    157 
    158         // initialize the best candidate to something impossible
    159         // (so the first plausible view will become the best choice)
    160         mBestCandidateRect.set(focusedRect);
    161         switch(direction) {
    162             case View.FOCUS_LEFT:
    163                 mBestCandidateRect.offset(focusedRect.width() + 1, 0);
    164                 break;
    165             case View.FOCUS_RIGHT:
    166                 mBestCandidateRect.offset(-(focusedRect.width() + 1), 0);
    167                 break;
    168             case View.FOCUS_UP:
    169                 mBestCandidateRect.offset(0, focusedRect.height() + 1);
    170                 break;
    171             case View.FOCUS_DOWN:
    172                 mBestCandidateRect.offset(0, -(focusedRect.height() + 1));
    173         }
    174 
    175         View closest = null;
    176 
    177         int numFocusables = focusables.size();
    178         for (int i = 0; i < numFocusables; i++) {
    179             View focusable = focusables.get(i);
    180 
    181             // only interested in other non-root views
    182             if (focusable == focused || focusable == root) continue;
    183 
    184             // get visible bounds of other view in same coordinate system
    185             focusable.getDrawingRect(mOtherRect);
    186             root.offsetDescendantRectToMyCoords(focusable, mOtherRect);
    187 
    188             if (isBetterCandidate(direction, focusedRect, mOtherRect, mBestCandidateRect)) {
    189                 mBestCandidateRect.set(mOtherRect);
    190                 closest = focusable;
    191             }
    192         }
    193         return closest;
    194     }
    195 
    196     /**
    197      * Is rect1 a better candidate than rect2 for a focus search in a particular
    198      * direction from a source rect?  This is the core routine that determines
    199      * the order of focus searching.
    200      * @param direction the direction (up, down, left, right)
    201      * @param source The source we are searching from
    202      * @param rect1 The candidate rectangle
    203      * @param rect2 The current best candidate.
    204      * @return Whether the candidate is the new best.
    205      */
    206     boolean isBetterCandidate(int direction, Rect source, Rect rect1, Rect rect2) {
    207 
    208         // to be a better candidate, need to at least be a candidate in the first
    209         // place :)
    210         if (!isCandidate(source, rect1, direction)) {
    211             return false;
    212         }
    213 
    214         // we know that rect1 is a candidate.. if rect2 is not a candidate,
    215         // rect1 is better
    216         if (!isCandidate(source, rect2, direction)) {
    217             return true;
    218         }
    219 
    220         // if rect1 is better by beam, it wins
    221         if (beamBeats(direction, source, rect1, rect2)) {
    222             return true;
    223         }
    224 
    225         // if rect2 is better, then rect1 cant' be :)
    226         if (beamBeats(direction, source, rect2, rect1)) {
    227             return false;
    228         }
    229 
    230         // otherwise, do fudge-tastic comparison of the major and minor axis
    231         return (getWeightedDistanceFor(
    232                         majorAxisDistance(direction, source, rect1),
    233                         minorAxisDistance(direction, source, rect1))
    234                 < getWeightedDistanceFor(
    235                         majorAxisDistance(direction, source, rect2),
    236                         minorAxisDistance(direction, source, rect2)));
    237     }
    238 
    239     /**
    240      * One rectangle may be another candidate than another by virtue of being
    241      * exclusively in the beam of the source rect.
    242      * @return Whether rect1 is a better candidate than rect2 by virtue of it being in src's
    243      *      beam
    244      */
    245     boolean beamBeats(int direction, Rect source, Rect rect1, Rect rect2) {
    246         final boolean rect1InSrcBeam = beamsOverlap(direction, source, rect1);
    247         final boolean rect2InSrcBeam = beamsOverlap(direction, source, rect2);
    248 
    249         // if rect1 isn't exclusively in the src beam, it doesn't win
    250         if (rect2InSrcBeam || !rect1InSrcBeam) {
    251             return false;
    252         }
    253 
    254         // we know rect1 is in the beam, and rect2 is not
    255 
    256         // if rect1 is to the direction of, and rect2 is not, rect1 wins.
    257         // for example, for direction left, if rect1 is to the left of the source
    258         // and rect2 is below, then we always prefer the in beam rect1, since rect2
    259         // could be reached by going down.
    260         if (!isToDirectionOf(direction, source, rect2)) {
    261             return true;
    262         }
    263 
    264         // for horizontal directions, being exclusively in beam always wins
    265         if ((direction == View.FOCUS_LEFT || direction == View.FOCUS_RIGHT)) {
    266             return true;
    267         }
    268 
    269         // for vertical directions, beams only beat up to a point:
    270         // now, as long as rect2 isn't completely closer, rect1 wins
    271         // e.g for direction down, completely closer means for rect2's top
    272         // edge to be closer to the source's top edge than rect1's bottom edge.
    273         return (majorAxisDistance(direction, source, rect1)
    274                 < majorAxisDistanceToFarEdge(direction, source, rect2));
    275     }
    276 
    277     /**
    278      * Fudge-factor opportunity: how to calculate distance given major and minor
    279      * axis distances.  Warning: this fudge factor is finely tuned, be sure to
    280      * run all focus tests if you dare tweak it.
    281      */
    282     int getWeightedDistanceFor(int majorAxisDistance, int minorAxisDistance) {
    283         return 13 * majorAxisDistance * majorAxisDistance
    284                 + minorAxisDistance * minorAxisDistance;
    285     }
    286 
    287     /**
    288      * Is destRect a candidate for the next focus given the direction?  This
    289      * checks whether the dest is at least partially to the direction of (e.g left of)
    290      * from source.
    291      *
    292      * Includes an edge case for an empty rect (which is used in some cases when
    293      * searching from a point on the screen).
    294      */
    295     boolean isCandidate(Rect srcRect, Rect destRect, int direction) {
    296         switch (direction) {
    297             case View.FOCUS_LEFT:
    298                 return (srcRect.right > destRect.right || srcRect.left >= destRect.right)
    299                         && srcRect.left > destRect.left;
    300             case View.FOCUS_RIGHT:
    301                 return (srcRect.left < destRect.left || srcRect.right <= destRect.left)
    302                         && srcRect.right < destRect.right;
    303             case View.FOCUS_UP:
    304                 return (srcRect.bottom > destRect.bottom || srcRect.top >= destRect.bottom)
    305                         && srcRect.top > destRect.top;
    306             case View.FOCUS_DOWN:
    307                 return (srcRect.top < destRect.top || srcRect.bottom <= destRect.top)
    308                         && srcRect.bottom < destRect.bottom;
    309         }
    310         throw new IllegalArgumentException("direction must be one of "
    311                 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
    312     }
    313 
    314 
    315     /**
    316      * Do the "beams" w.r.t the given direcition's axis of rect1 and rect2 overlap?
    317      * @param direction the direction (up, down, left, right)
    318      * @param rect1 The first rectangle
    319      * @param rect2 The second rectangle
    320      * @return whether the beams overlap
    321      */
    322     boolean beamsOverlap(int direction, Rect rect1, Rect rect2) {
    323         switch (direction) {
    324             case View.FOCUS_LEFT:
    325             case View.FOCUS_RIGHT:
    326                 return (rect2.bottom >= rect1.top) && (rect2.top <= rect1.bottom);
    327             case View.FOCUS_UP:
    328             case View.FOCUS_DOWN:
    329                 return (rect2.right >= rect1.left) && (rect2.left <= rect1.right);
    330         }
    331         throw new IllegalArgumentException("direction must be one of "
    332                 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
    333     }
    334 
    335     /**
    336      * e.g for left, is 'to left of'
    337      */
    338     boolean isToDirectionOf(int direction, Rect src, Rect dest) {
    339         switch (direction) {
    340             case View.FOCUS_LEFT:
    341                 return src.left >= dest.right;
    342             case View.FOCUS_RIGHT:
    343                 return src.right <= dest.left;
    344             case View.FOCUS_UP:
    345                 return src.top >= dest.bottom;
    346             case View.FOCUS_DOWN:
    347                 return src.bottom <= dest.top;
    348         }
    349         throw new IllegalArgumentException("direction must be one of "
    350                 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
    351     }
    352 
    353     /**
    354      * @return The distance from the edge furthest in the given direction
    355      *   of source to the edge nearest in the given direction of dest.  If the
    356      *   dest is not in the direction from source, return 0.
    357      */
    358     static int majorAxisDistance(int direction, Rect source, Rect dest) {
    359         return Math.max(0, majorAxisDistanceRaw(direction, source, dest));
    360     }
    361 
    362     static int majorAxisDistanceRaw(int direction, Rect source, Rect dest) {
    363         switch (direction) {
    364             case View.FOCUS_LEFT:
    365                 return source.left - dest.right;
    366             case View.FOCUS_RIGHT:
    367                 return dest.left - source.right;
    368             case View.FOCUS_UP:
    369                 return source.top - dest.bottom;
    370             case View.FOCUS_DOWN:
    371                 return dest.top - source.bottom;
    372         }
    373         throw new IllegalArgumentException("direction must be one of "
    374                 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
    375     }
    376 
    377     /**
    378      * @return The distance along the major axis w.r.t the direction from the
    379      *   edge of source to the far edge of dest. If the
    380      *   dest is not in the direction from source, return 1 (to break ties with
    381      *   {@link #majorAxisDistance}).
    382      */
    383     static int majorAxisDistanceToFarEdge(int direction, Rect source, Rect dest) {
    384         return Math.max(1, majorAxisDistanceToFarEdgeRaw(direction, source, dest));
    385     }
    386 
    387     static int majorAxisDistanceToFarEdgeRaw(int direction, Rect source, Rect dest) {
    388         switch (direction) {
    389             case View.FOCUS_LEFT:
    390                 return source.left - dest.left;
    391             case View.FOCUS_RIGHT:
    392                 return dest.right - source.right;
    393             case View.FOCUS_UP:
    394                 return source.top - dest.top;
    395             case View.FOCUS_DOWN:
    396                 return dest.bottom - source.bottom;
    397         }
    398         throw new IllegalArgumentException("direction must be one of "
    399                 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
    400     }
    401 
    402     /**
    403      * Find the distance on the minor axis w.r.t the direction to the nearest
    404      * edge of the destination rectange.
    405      * @param direction the direction (up, down, left, right)
    406      * @param source The source rect.
    407      * @param dest The destination rect.
    408      * @return The distance.
    409      */
    410     static int minorAxisDistance(int direction, Rect source, Rect dest) {
    411         switch (direction) {
    412             case View.FOCUS_LEFT:
    413             case View.FOCUS_RIGHT:
    414                 // the distance between the center verticals
    415                 return Math.abs(
    416                         ((source.top + source.height() / 2) -
    417                         ((dest.top + dest.height() / 2))));
    418             case View.FOCUS_UP:
    419             case View.FOCUS_DOWN:
    420                 // the distance between the center horizontals
    421                 return Math.abs(
    422                         ((source.left + source.width() / 2) -
    423                         ((dest.left + dest.width() / 2))));
    424         }
    425         throw new IllegalArgumentException("direction must be one of "
    426                 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
    427     }
    428 
    429     /**
    430      * Find the nearest touchable view to the specified view.
    431      *
    432      * @param root The root of the tree in which to search
    433      * @param x X coordinate from which to start the search
    434      * @param y Y coordinate from which to start the search
    435      * @param direction Direction to look
    436      * @param deltas Offset from the <x, y> to the edge of the nearest view. Note that this array
    437      *        may already be populated with values.
    438      * @return The nearest touchable view, or null if none exists.
    439      */
    440     public View findNearestTouchable(ViewGroup root, int x, int y, int direction, int[] deltas) {
    441         ArrayList<View> touchables = root.getTouchables();
    442         int minDistance = Integer.MAX_VALUE;
    443         View closest = null;
    444 
    445         int numTouchables = touchables.size();
    446 
    447         int edgeSlop = ViewConfiguration.get(root.mContext).getScaledEdgeSlop();
    448 
    449         Rect closestBounds = new Rect();
    450         Rect touchableBounds = mOtherRect;
    451 
    452         for (int i = 0; i < numTouchables; i++) {
    453             View touchable = touchables.get(i);
    454 
    455             // get visible bounds of other view in same coordinate system
    456             touchable.getDrawingRect(touchableBounds);
    457 
    458             root.offsetRectBetweenParentAndChild(touchable, touchableBounds, true, true);
    459 
    460             if (!isTouchCandidate(x, y, touchableBounds, direction)) {
    461                 continue;
    462             }
    463 
    464             int distance = Integer.MAX_VALUE;
    465 
    466             switch (direction) {
    467             case View.FOCUS_LEFT:
    468                 distance = x - touchableBounds.right + 1;
    469                 break;
    470             case View.FOCUS_RIGHT:
    471                 distance = touchableBounds.left;
    472                 break;
    473             case View.FOCUS_UP:
    474                 distance = y - touchableBounds.bottom + 1;
    475                 break;
    476             case View.FOCUS_DOWN:
    477                 distance = touchableBounds.top;
    478                 break;
    479             }
    480 
    481             if (distance < edgeSlop) {
    482                 // Give preference to innermost views
    483                 if (closest == null ||
    484                         closestBounds.contains(touchableBounds) ||
    485                         (!touchableBounds.contains(closestBounds) && distance < minDistance)) {
    486                     minDistance = distance;
    487                     closest = touchable;
    488                     closestBounds.set(touchableBounds);
    489                     switch (direction) {
    490                     case View.FOCUS_LEFT:
    491                         deltas[0] = -distance;
    492                         break;
    493                     case View.FOCUS_RIGHT:
    494                         deltas[0] = distance;
    495                         break;
    496                     case View.FOCUS_UP:
    497                         deltas[1] = -distance;
    498                         break;
    499                     case View.FOCUS_DOWN:
    500                         deltas[1] = distance;
    501                         break;
    502                     }
    503                 }
    504             }
    505         }
    506         return closest;
    507     }
    508 
    509 
    510     /**
    511      * Is destRect a candidate for the next touch given the direction?
    512      */
    513     private boolean isTouchCandidate(int x, int y, Rect destRect, int direction) {
    514         switch (direction) {
    515             case View.FOCUS_LEFT:
    516                 return destRect.left <= x && destRect.top <= y && y <= destRect.bottom;
    517             case View.FOCUS_RIGHT:
    518                 return destRect.left >= x && destRect.top <= y && y <= destRect.bottom;
    519             case View.FOCUS_UP:
    520                 return destRect.top <= y && destRect.left <= x && x <= destRect.right;
    521             case View.FOCUS_DOWN:
    522                 return destRect.top >= y && destRect.left <= x && x <= destRect.right;
    523         }
    524         throw new IllegalArgumentException("direction must be one of "
    525                 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
    526     }
    527 
    528     /**
    529      * Sorts views according to their visual layout and geometry for default tab order.
    530      * This is used for sequential focus traversal.
    531      */
    532     private static final class SequentialFocusComparator implements Comparator<View> {
    533         private final Rect mFirstRect = new Rect();
    534         private final Rect mSecondRect = new Rect();
    535         private ViewGroup mRoot;
    536 
    537         public void recycle() {
    538             mRoot = null;
    539         }
    540 
    541         public void setRoot(ViewGroup root) {
    542             mRoot = root;
    543         }
    544 
    545         public int compare(View first, View second) {
    546             if (first == second) {
    547                 return 0;
    548             }
    549 
    550             getRect(first, mFirstRect);
    551             getRect(second, mSecondRect);
    552 
    553             if (mFirstRect.top < mSecondRect.top) {
    554                 return -1;
    555             } else if (mFirstRect.top > mSecondRect.top) {
    556                 return 1;
    557             } else if (mFirstRect.left < mSecondRect.left) {
    558                 return -1;
    559             } else if (mFirstRect.left > mSecondRect.left) {
    560                 return 1;
    561             } else if (mFirstRect.bottom < mSecondRect.bottom) {
    562                 return -1;
    563             } else if (mFirstRect.bottom > mSecondRect.bottom) {
    564                 return 1;
    565             } else if (mFirstRect.right < mSecondRect.right) {
    566                 return -1;
    567             } else if (mFirstRect.right > mSecondRect.right) {
    568                 return 1;
    569             } else {
    570                 // The view are distinct but completely coincident so we consider
    571                 // them equal for our purposes.  Since the sort is stable, this
    572                 // means that the views will retain their layout order relative to one another.
    573                 return 0;
    574             }
    575         }
    576 
    577         private void getRect(View view, Rect rect) {
    578             view.getDrawingRect(rect);
    579             mRoot.offsetDescendantRectToMyCoords(view, rect);
    580         }
    581     }
    582 }
    583