Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2015 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.support.v4.widget;
     18 
     19 import android.graphics.Rect;
     20 import android.support.annotation.NonNull;
     21 import android.support.annotation.Nullable;
     22 import android.view.View;
     23 
     24 import android.support.v4.view.ViewCompat.FocusRealDirection;
     25 import android.support.v4.view.ViewCompat.FocusRelativeDirection;
     26 
     27 import java.util.ArrayList;
     28 import java.util.Collections;
     29 import java.util.Comparator;
     30 
     31 /**
     32  * Implements absolute and relative focus movement strategies. Adapted from
     33  * android.view.FocusFinder to work with generic collections of bounded items.
     34  */
     35 class FocusStrategy {
     36     public static <L, T> T findNextFocusInRelativeDirection(@NonNull L focusables,
     37             @NonNull CollectionAdapter<L, T> collectionAdapter, @NonNull BoundsAdapter<T> adapter,
     38             @Nullable T focused, @FocusRelativeDirection int direction, boolean isLayoutRtl,
     39             boolean wrap) {
     40         final int count = collectionAdapter.size(focusables);
     41         final ArrayList<T> sortedFocusables = new ArrayList<>(count);
     42         for (int i = 0; i < count; i++) {
     43             sortedFocusables.add(collectionAdapter.get(focusables, i));
     44         }
     45 
     46         final SequentialComparator<T> comparator = new SequentialComparator<>(isLayoutRtl, adapter);
     47         Collections.sort(sortedFocusables, comparator);
     48 
     49         switch (direction) {
     50             case View.FOCUS_FORWARD:
     51                 return getNextFocusable(focused, sortedFocusables, wrap);
     52             case View.FOCUS_BACKWARD:
     53                 return getPreviousFocusable(focused, sortedFocusables, wrap);
     54             default:
     55                 throw new IllegalArgumentException("direction must be one of "
     56                         + "{FOCUS_FORWARD, FOCUS_BACKWARD}.");
     57         }
     58     }
     59 
     60     private static <T> T getNextFocusable(T focused, ArrayList<T> focusables, boolean wrap) {
     61         final int count = focusables.size();
     62 
     63         // The position of the next focusable item, which is the first item if
     64         // no item is currently focused.
     65         final int position = (focused == null ? -1 : focusables.lastIndexOf(focused)) + 1;
     66         if (position < count) {
     67             return focusables.get(position);
     68         } else if (wrap && count > 0) {
     69             return focusables.get(0);
     70         } else {
     71             return null;
     72         }
     73     }
     74 
     75     private static <T> T getPreviousFocusable(T focused, ArrayList<T> focusables, boolean wrap) {
     76         final int count = focusables.size();
     77 
     78         // The position of the previous focusable item, which is the last item
     79         // if no item is currently focused.
     80         final int position = (focused == null ? count : focusables.indexOf(focused)) - 1;
     81         if (position >= 0) {
     82             return focusables.get(position);
     83         } else if (wrap && count > 0) {
     84             return focusables.get(count - 1);
     85         } else {
     86             return null;
     87         }
     88     }
     89 
     90     /**
     91      * Sorts views according to their visual layout and geometry for default tab order.
     92      * This is used for sequential focus traversal.
     93      */
     94     private static class SequentialComparator<T> implements Comparator<T> {
     95         private final Rect mTemp1 = new Rect();
     96         private final Rect mTemp2 = new Rect();
     97 
     98         private final boolean mIsLayoutRtl;
     99         private final BoundsAdapter<T> mAdapter;
    100 
    101         SequentialComparator(boolean isLayoutRtl, BoundsAdapter<T> adapter) {
    102             mIsLayoutRtl = isLayoutRtl;
    103             mAdapter = adapter;
    104         }
    105 
    106         @Override
    107         public int compare(T first, T second) {
    108             final Rect firstRect = mTemp1;
    109             final Rect secondRect = mTemp2;
    110 
    111             mAdapter.obtainBounds(first, firstRect);
    112             mAdapter.obtainBounds(second, secondRect);
    113 
    114             if (firstRect.top < secondRect.top) {
    115                 return -1;
    116             } else if (firstRect.top > secondRect.top) {
    117                 return 1;
    118             } else if (firstRect.left < secondRect.left) {
    119                 return mIsLayoutRtl ? 1 : -1;
    120             } else if (firstRect.left > secondRect.left) {
    121                 return mIsLayoutRtl ? -1 : 1;
    122             } else if (firstRect.bottom < secondRect.bottom) {
    123                 return -1;
    124             } else if (firstRect.bottom > secondRect.bottom) {
    125                 return 1;
    126             } else if (firstRect.right < secondRect.right) {
    127                 return mIsLayoutRtl ? 1 : -1;
    128             } else if (firstRect.right > secondRect.right) {
    129                 return mIsLayoutRtl ? -1 : 1;
    130             } else {
    131                 // The view are distinct but completely coincident so we
    132                 // consider them equal for our purposes. Since the sort is
    133                 // stable, this means that the views will retain their
    134                 // layout order relative to one another.
    135                 return 0;
    136             }
    137         }
    138     }
    139 
    140     public static <L, T> T findNextFocusInAbsoluteDirection(@NonNull L focusables,
    141             @NonNull CollectionAdapter<L, T> collectionAdapter, @NonNull BoundsAdapter<T> adapter,
    142             @Nullable T focused, @NonNull Rect focusedRect, int direction) {
    143         // Initialize the best candidate to something impossible so that
    144         // the first plausible view will become the best choice.
    145         final Rect bestCandidateRect = new Rect(focusedRect);
    146 
    147         switch (direction) {
    148             case View.FOCUS_LEFT:
    149                 bestCandidateRect.offset(focusedRect.width() + 1, 0);
    150                 break;
    151             case View.FOCUS_RIGHT:
    152                 bestCandidateRect.offset(-(focusedRect.width() + 1), 0);
    153                 break;
    154             case View.FOCUS_UP:
    155                 bestCandidateRect.offset(0, focusedRect.height() + 1);
    156                 break;
    157             case View.FOCUS_DOWN:
    158                 bestCandidateRect.offset(0, -(focusedRect.height() + 1));
    159                 break;
    160             default:
    161                 throw new IllegalArgumentException("direction must be one of "
    162                         + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
    163         }
    164 
    165         T closest = null;
    166 
    167         final int count = collectionAdapter.size(focusables);
    168         final Rect focusableRect = new Rect();
    169         for (int i = 0; i < count; i++) {
    170             final T focusable = collectionAdapter.get(focusables, i);
    171             if (focusable == focused) {
    172                 continue;
    173             }
    174 
    175             // get focus bounds of other view
    176             adapter.obtainBounds(focusable, focusableRect);
    177             if (isBetterCandidate(direction, focusedRect, focusableRect, bestCandidateRect)) {
    178                 bestCandidateRect.set(focusableRect);
    179                 closest = focusable;
    180             }
    181         }
    182 
    183         return closest;
    184     }
    185 
    186     /**
    187      * Is candidate a better candidate than currentBest for a focus search
    188      * in a particular direction from a source rect? This is the core
    189      * routine that determines the order of focus searching.
    190      *
    191      * @param direction   the direction (up, down, left, right)
    192      * @param source      the source from which we are searching
    193      * @param candidate   the candidate rectangle
    194      * @param currentBest the current best rectangle
    195      * @return {@code true} if the candidate rectangle is a better than the
    196      * current best rectangle, {@code false} otherwise
    197      */
    198     private static boolean isBetterCandidate(
    199             @FocusRealDirection int direction, @NonNull Rect source,
    200             @NonNull Rect candidate, @NonNull Rect currentBest) {
    201         // To be a better candidate, need to at least be a candidate in the
    202         // first place. :)
    203         if (!isCandidate(source, candidate, direction)) {
    204             return false;
    205         }
    206 
    207         // We know that candidateRect is a candidate. If currentBest is not
    208         // a candidate, candidateRect is better.
    209         if (!isCandidate(source, currentBest, direction)) {
    210             return true;
    211         }
    212 
    213         // If candidateRect is better by beam, it wins.
    214         if (beamBeats(direction, source, candidate, currentBest)) {
    215             return true;
    216         }
    217 
    218         // If currentBest is better, then candidateRect cant' be. :)
    219         if (beamBeats(direction, source, currentBest, candidate)) {
    220             return false;
    221         }
    222 
    223         // Otherwise, do fudge-tastic comparison of the major and minor
    224         // axis.
    225         final int candidateDist = getWeightedDistanceFor(
    226                 majorAxisDistance(direction, source, candidate),
    227                 minorAxisDistance(direction, source, candidate));
    228         final int currentBestDist = getWeightedDistanceFor(
    229                 majorAxisDistance(direction, source, currentBest),
    230                 minorAxisDistance(direction, source, currentBest));
    231         return candidateDist < currentBestDist;
    232     }
    233 
    234     /**
    235      * One rectangle may be another candidate than another by virtue of
    236      * being exclusively in the beam of the source rect.
    237      *
    238      * @return whether rect1 is a better candidate than rect2 by virtue of
    239      * it being in source's beam
    240      */
    241     private static boolean beamBeats(@FocusRealDirection int direction,
    242             @NonNull Rect source, @NonNull Rect rect1, @NonNull Rect rect2) {
    243         final boolean rect1InSrcBeam = beamsOverlap(direction, source, rect1);
    244         final boolean rect2InSrcBeam = beamsOverlap(direction, source, rect2);
    245 
    246         // If rect1 isn't exclusively in the src beam, it doesn't win.
    247         if (rect2InSrcBeam || !rect1InSrcBeam) {
    248             return false;
    249         }
    250 
    251         // We know rect1 is in the beam, and rect2 is not.
    252 
    253         // If rect1 is to the direction of, and rect2 is not, rect1 wins.
    254         // For example, for direction left, if rect1 is to the left of the
    255         // source and rect2 is below, then we always prefer the in beam
    256         // rect1, since rect2 could be reached by going down.
    257         if (!isToDirectionOf(direction, source, rect2)) {
    258             return true;
    259         }
    260 
    261         // For horizontal directions, being exclusively in beam always
    262         // wins.
    263         if (direction == View.FOCUS_LEFT || direction == View.FOCUS_RIGHT) {
    264             return true;
    265         }
    266 
    267         // For vertical directions, beams only beat up to a point: now, as
    268         // long as rect2 isn't completely closer, rect1 wins, e.g. for
    269         // direction down, completely closer means for rect2's top edge to
    270         // be closer to the source's top edge than rect1's bottom edge.
    271         return majorAxisDistance(direction, source, rect1)
    272                 < majorAxisDistanceToFarEdge(direction, source, rect2);
    273     }
    274 
    275     /**
    276      * Fudge-factor opportunity: how to calculate distance given major and
    277      * minor axis distances.
    278      * <p/>
    279      * Warning: this fudge factor is finely tuned, be sure to run all focus
    280      * tests if you dare tweak it.
    281      */
    282     private static 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
    290      * (e.g. left of) from source.
    291      * <p/>
    292      * Includes an edge case for an empty rect,which is used in some cases
    293      * when searching from a point on the screen.
    294      */
    295     private static boolean isCandidate(@NonNull Rect srcRect, @NonNull Rect destRect,
    296             @FocusRealDirection int direction) {
    297         switch (direction) {
    298             case View.FOCUS_LEFT:
    299                 return (srcRect.right > destRect.right || srcRect.left >= destRect.right)
    300                         && srcRect.left > destRect.left;
    301             case View.FOCUS_RIGHT:
    302                 return (srcRect.left < destRect.left || srcRect.right <= destRect.left)
    303                         && srcRect.right < destRect.right;
    304             case View.FOCUS_UP:
    305                 return (srcRect.bottom > destRect.bottom || srcRect.top >= destRect.bottom)
    306                         && srcRect.top > destRect.top;
    307             case View.FOCUS_DOWN:
    308                 return (srcRect.top < destRect.top || srcRect.bottom <= destRect.top)
    309                         && srcRect.bottom < destRect.bottom;
    310         }
    311         throw new IllegalArgumentException("direction must be one of "
    312                 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
    313     }
    314 
    315 
    316     /**
    317      * Do the "beams" w.r.t the given direction's axis of rect1 and rect2 overlap?
    318      *
    319      * @param direction the direction (up, down, left, right)
    320      * @param rect1     the first rectangle
    321      * @param rect2     the second rectangle
    322      * @return whether the beams overlap
    323      */
    324     private static boolean beamsOverlap(@FocusRealDirection int direction,
    325             @NonNull Rect rect1, @NonNull Rect rect2) {
    326         switch (direction) {
    327             case View.FOCUS_LEFT:
    328             case View.FOCUS_RIGHT:
    329                 return (rect2.bottom >= rect1.top) && (rect2.top <= rect1.bottom);
    330             case View.FOCUS_UP:
    331             case View.FOCUS_DOWN:
    332                 return (rect2.right >= rect1.left) && (rect2.left <= rect1.right);
    333         }
    334         throw new IllegalArgumentException("direction must be one of "
    335                 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
    336     }
    337 
    338     /**
    339      * e.g for left, is 'to left of'
    340      */
    341     private static boolean isToDirectionOf(@FocusRealDirection int direction,
    342             @NonNull Rect src, @NonNull Rect dest) {
    343         switch (direction) {
    344             case View.FOCUS_LEFT:
    345                 return src.left >= dest.right;
    346             case View.FOCUS_RIGHT:
    347                 return src.right <= dest.left;
    348             case View.FOCUS_UP:
    349                 return src.top >= dest.bottom;
    350             case View.FOCUS_DOWN:
    351                 return src.bottom <= dest.top;
    352         }
    353         throw new IllegalArgumentException("direction must be one of "
    354                 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
    355     }
    356 
    357     /**
    358      * @return the distance from the edge furthest in the given direction
    359      * of source to the edge nearest in the given direction of
    360      * dest. If the dest is not in the direction from source,
    361      * returns 0.
    362      */
    363     private static int majorAxisDistance(@FocusRealDirection int direction,
    364             @NonNull Rect source, @NonNull Rect dest) {
    365         return Math.max(0, majorAxisDistanceRaw(direction, source, dest));
    366     }
    367 
    368     private static int majorAxisDistanceRaw(@FocusRealDirection int direction,
    369             @NonNull Rect source, @NonNull Rect dest) {
    370         switch (direction) {
    371             case View.FOCUS_LEFT:
    372                 return source.left - dest.right;
    373             case View.FOCUS_RIGHT:
    374                 return dest.left - source.right;
    375             case View.FOCUS_UP:
    376                 return source.top - dest.bottom;
    377             case View.FOCUS_DOWN:
    378                 return dest.top - source.bottom;
    379         }
    380         throw new IllegalArgumentException("direction must be one of "
    381                 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
    382     }
    383 
    384     /**
    385      * @return the distance along the major axis w.r.t the direction from
    386      * the edge of source to the far edge of dest. If the dest is
    387      * not in the direction from source, returns 1 to break ties
    388      * with {@link #majorAxisDistance}.
    389      */
    390     private static int majorAxisDistanceToFarEdge(@FocusRealDirection int direction,
    391             @NonNull Rect source, @NonNull Rect dest) {
    392         return Math.max(1, majorAxisDistanceToFarEdgeRaw(direction, source, dest));
    393     }
    394 
    395     private static int majorAxisDistanceToFarEdgeRaw(
    396             @FocusRealDirection int direction, @NonNull Rect source,
    397             @NonNull Rect dest) {
    398         switch (direction) {
    399             case View.FOCUS_LEFT:
    400                 return source.left - dest.left;
    401             case View.FOCUS_RIGHT:
    402                 return dest.right - source.right;
    403             case View.FOCUS_UP:
    404                 return source.top - dest.top;
    405             case View.FOCUS_DOWN:
    406                 return dest.bottom - source.bottom;
    407         }
    408         throw new IllegalArgumentException("direction must be one of "
    409                 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
    410     }
    411 
    412     /**
    413      * Finds the distance on the minor axis w.r.t the direction to the
    414      * nearest edge of the destination rectangle.
    415      *
    416      * @param direction the direction (up, down, left, right)
    417      * @param source the source rect
    418      * @param dest the destination rect
    419      * @return the distance
    420      */
    421     private static int minorAxisDistance(@FocusRealDirection int direction, @NonNull Rect source,
    422             @NonNull Rect dest) {
    423         switch (direction) {
    424             case View.FOCUS_LEFT:
    425             case View.FOCUS_RIGHT:
    426                 // the distance between the center verticals
    427                 return Math.abs(
    428                         ((source.top + source.height() / 2) - ((dest.top + dest.height() / 2))));
    429             case View.FOCUS_UP:
    430             case View.FOCUS_DOWN:
    431                 // the distance between the center horizontals
    432                 return Math.abs(
    433                         ((source.left + source.width() / 2) -
    434                                 ((dest.left + dest.width() / 2))));
    435         }
    436         throw new IllegalArgumentException("direction must be one of "
    437                 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
    438     }
    439 
    440     /**
    441      * Adapter used to obtain bounds from a generic data type.
    442      */
    443     public interface BoundsAdapter<T> {
    444         void obtainBounds(T data, Rect outBounds);
    445     }
    446 
    447     /**
    448      * Adapter used to obtain items from a generic collection type.
    449      */
    450     public interface CollectionAdapter<T, V> {
    451         V get(T collection, int index);
    452         int size(T collection);
    453     }
    454 }
    455