| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.support.v4.widget; |
| |
| import android.graphics.Rect; |
| import android.support.annotation.NonNull; |
| import android.support.annotation.Nullable; |
| import android.view.View; |
| |
| import android.support.v4.view.ViewCompat.FocusRealDirection; |
| import android.support.v4.view.ViewCompat.FocusRelativeDirection; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Comparator; |
| |
| /** |
| * Implements absolute and relative focus movement strategies. Adapted from |
| * android.view.FocusFinder to work with generic collections of bounded items. |
| */ |
| class FocusStrategy { |
| public static <L,T> T findNextFocusInRelativeDirection(@NonNull L focusables, |
| @NonNull CollectionAdapter<L,T> collectionAdapter, @NonNull BoundsAdapter<T> adapter, |
| @Nullable T focused, @FocusRelativeDirection int direction, boolean isLayoutRtl, |
| boolean wrap) { |
| final int count = collectionAdapter.size(focusables); |
| final ArrayList<T> sortedFocusables = new ArrayList<>(count); |
| for (int i = 0; i < count; i++) { |
| sortedFocusables.add(collectionAdapter.get(focusables, i)); |
| } |
| |
| final SequentialComparator<T> comparator = new SequentialComparator<>(isLayoutRtl, adapter); |
| Collections.sort(sortedFocusables, comparator); |
| |
| switch (direction) { |
| case View.FOCUS_FORWARD: |
| return getNextFocusable(focused, sortedFocusables, wrap); |
| case View.FOCUS_BACKWARD: |
| return getPreviousFocusable(focused, sortedFocusables, wrap); |
| default: |
| throw new IllegalArgumentException("direction must be one of " |
| + "{FOCUS_FORWARD, FOCUS_BACKWARD}."); |
| } |
| } |
| |
| private static <T> T getNextFocusable(T focused, ArrayList<T> focusables, boolean wrap) { |
| final int count = focusables.size(); |
| |
| // The position of the next focusable item, which is the first item if |
| // no item is currently focused. |
| final int position = (focused == null ? -1 : focusables.lastIndexOf(focused)) + 1; |
| if (position < count) { |
| return focusables.get(position); |
| } else if (wrap && count > 0) { |
| return focusables.get(0); |
| } else { |
| return null; |
| } |
| } |
| |
| private static <T> T getPreviousFocusable(T focused, ArrayList<T> focusables, boolean wrap) { |
| final int count = focusables.size(); |
| |
| // The position of the previous focusable item, which is the last item |
| // if no item is currently focused. |
| final int position = (focused == null ? count : focusables.indexOf(focused)) - 1; |
| if (position >= 0) { |
| return focusables.get(position); |
| } else if (wrap && count > 0) { |
| return focusables.get(count - 1); |
| } else { |
| return null; |
| } |
| } |
| |
| /** |
| * Sorts views according to their visual layout and geometry for default tab order. |
| * This is used for sequential focus traversal. |
| */ |
| private static class SequentialComparator<T> implements Comparator<T> { |
| private final Rect mTemp1 = new Rect(); |
| private final Rect mTemp2 = new Rect(); |
| |
| private final boolean mIsLayoutRtl; |
| private final BoundsAdapter<T> mAdapter; |
| |
| public SequentialComparator(boolean isLayoutRtl, BoundsAdapter<T> adapter) { |
| mIsLayoutRtl = isLayoutRtl; |
| mAdapter = adapter; |
| } |
| |
| public int compare(T first, T second) { |
| final Rect firstRect = mTemp1; |
| final Rect secondRect = mTemp2; |
| |
| mAdapter.obtainBounds(first, firstRect); |
| mAdapter.obtainBounds(second, secondRect); |
| |
| if (firstRect.top < secondRect.top) { |
| return -1; |
| } else if (firstRect.top > secondRect.top) { |
| return 1; |
| } else if (firstRect.left < secondRect.left) { |
| return mIsLayoutRtl ? 1 : -1; |
| } else if (firstRect.left > secondRect.left) { |
| return mIsLayoutRtl ? -1 : 1; |
| } else if (firstRect.bottom < secondRect.bottom) { |
| return -1; |
| } else if (firstRect.bottom > secondRect.bottom) { |
| return 1; |
| } else if (firstRect.right < secondRect.right) { |
| return mIsLayoutRtl ? 1 : -1; |
| } else if (firstRect.right > secondRect.right) { |
| return mIsLayoutRtl ? -1 : 1; |
| } else { |
| // The view are distinct but completely coincident so we |
| // consider them equal for our purposes. Since the sort is |
| // stable, this means that the views will retain their |
| // layout order relative to one another. |
| return 0; |
| } |
| } |
| } |
| |
| public static <L,T> T findNextFocusInAbsoluteDirection(@NonNull L focusables, |
| @NonNull CollectionAdapter<L,T> collectionAdapter, @NonNull BoundsAdapter<T> adapter, |
| @Nullable T focused, @NonNull Rect focusedRect, int direction) { |
| // Initialize the best candidate to something impossible so that |
| // the first plausible view will become the best choice. |
| final Rect bestCandidateRect = new Rect(focusedRect); |
| |
| switch (direction) { |
| case View.FOCUS_LEFT: |
| bestCandidateRect.offset(focusedRect.width() + 1, 0); |
| break; |
| case View.FOCUS_RIGHT: |
| bestCandidateRect.offset(-(focusedRect.width() + 1), 0); |
| break; |
| case View.FOCUS_UP: |
| bestCandidateRect.offset(0, focusedRect.height() + 1); |
| break; |
| case View.FOCUS_DOWN: |
| bestCandidateRect.offset(0, -(focusedRect.height() + 1)); |
| break; |
| default: |
| throw new IllegalArgumentException("direction must be one of " |
| + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); |
| } |
| |
| T closest = null; |
| |
| final int count = collectionAdapter.size(focusables); |
| final Rect focusableRect = new Rect(); |
| for (int i = 0; i < count; i++) { |
| final T focusable = collectionAdapter.get(focusables, i); |
| if (focusable == focused) { |
| continue; |
| } |
| |
| // get focus bounds of other view |
| adapter.obtainBounds(focusable, focusableRect); |
| if (isBetterCandidate(direction, focusedRect, focusableRect, bestCandidateRect)) { |
| bestCandidateRect.set(focusableRect); |
| closest = focusable; |
| } |
| } |
| |
| return closest; |
| } |
| |
| /** |
| * Is candidate a better candidate than currentBest for a focus search |
| * in a particular direction from a source rect? This is the core |
| * routine that determines the order of focus searching. |
| * |
| * @param direction the direction (up, down, left, right) |
| * @param source the source from which we are searching |
| * @param candidate the candidate rectangle |
| * @param currentBest the current best rectangle |
| * @return {@code true} if the candidate rectangle is a better than the |
| * current best rectangle, {@code false} otherwise |
| */ |
| private static boolean isBetterCandidate( |
| @FocusRealDirection int direction, @NonNull Rect source, |
| @NonNull Rect candidate, @NonNull Rect currentBest) { |
| // To be a better candidate, need to at least be a candidate in the |
| // first place. :) |
| if (!isCandidate(source, candidate, direction)) { |
| return false; |
| } |
| |
| // We know that candidateRect is a candidate. If currentBest is not |
| // a candidate, candidateRect is better. |
| if (!isCandidate(source, currentBest, direction)) { |
| return true; |
| } |
| |
| // If candidateRect is better by beam, it wins. |
| if (beamBeats(direction, source, candidate, currentBest)) { |
| return true; |
| } |
| |
| // If currentBest is better, then candidateRect cant' be. :) |
| if (beamBeats(direction, source, currentBest, candidate)) { |
| return false; |
| } |
| |
| // Otherwise, do fudge-tastic comparison of the major and minor |
| // axis. |
| final int candidateDist = getWeightedDistanceFor( |
| majorAxisDistance(direction, source, candidate), |
| minorAxisDistance(direction, source, candidate)); |
| final int currentBestDist = getWeightedDistanceFor( |
| majorAxisDistance(direction, source, currentBest), |
| minorAxisDistance(direction, source, currentBest)); |
| return candidateDist < currentBestDist; |
| } |
| |
| /** |
| * One rectangle may be another candidate than another by virtue of |
| * being exclusively in the beam of the source rect. |
| * |
| * @return whether rect1 is a better candidate than rect2 by virtue of |
| * it being in source's beam |
| */ |
| private static boolean beamBeats(@FocusRealDirection int direction, |
| @NonNull Rect source, @NonNull Rect rect1, @NonNull Rect rect2) { |
| final boolean rect1InSrcBeam = beamsOverlap(direction, source, rect1); |
| final boolean rect2InSrcBeam = beamsOverlap(direction, source, rect2); |
| |
| // If rect1 isn't exclusively in the src beam, it doesn't win. |
| if (rect2InSrcBeam || !rect1InSrcBeam) { |
| return false; |
| } |
| |
| // We know rect1 is in the beam, and rect2 is not. |
| |
| // If rect1 is to the direction of, and rect2 is not, rect1 wins. |
| // For example, for direction left, if rect1 is to the left of the |
| // source and rect2 is below, then we always prefer the in beam |
| // rect1, since rect2 could be reached by going down. |
| if (!isToDirectionOf(direction, source, rect2)) { |
| return true; |
| } |
| |
| // For horizontal directions, being exclusively in beam always |
| // wins. |
| if (direction == View.FOCUS_LEFT || direction == View.FOCUS_RIGHT) { |
| return true; |
| } |
| |
| // For vertical directions, beams only beat up to a point: now, as |
| // long as rect2 isn't completely closer, rect1 wins, e.g. for |
| // direction down, completely closer means for rect2's top edge to |
| // be closer to the source's top edge than rect1's bottom edge. |
| return majorAxisDistance(direction, source, rect1) |
| < majorAxisDistanceToFarEdge(direction, source, rect2); |
| } |
| |
| /** |
| * Fudge-factor opportunity: how to calculate distance given major and |
| * minor axis distances. |
| * <p/> |
| * Warning: this fudge factor is finely tuned, be sure to run all focus |
| * tests if you dare tweak it. |
| */ |
| private static int getWeightedDistanceFor(int majorAxisDistance, int minorAxisDistance) { |
| return 13 * majorAxisDistance * majorAxisDistance |
| + minorAxisDistance * minorAxisDistance; |
| } |
| |
| /** |
| * Is destRect a candidate for the next focus given the direction? This |
| * checks whether the dest is at least partially to the direction of |
| * (e.g. left of) from source. |
| * <p/> |
| * Includes an edge case for an empty rect,which is used in some cases |
| * when searching from a point on the screen. |
| */ |
| private static boolean isCandidate(@NonNull Rect srcRect, @NonNull Rect destRect, |
| @FocusRealDirection int direction) { |
| switch (direction) { |
| case View.FOCUS_LEFT: |
| return (srcRect.right > destRect.right || srcRect.left >= destRect.right) |
| && srcRect.left > destRect.left; |
| case View.FOCUS_RIGHT: |
| return (srcRect.left < destRect.left || srcRect.right <= destRect.left) |
| && srcRect.right < destRect.right; |
| case View.FOCUS_UP: |
| return (srcRect.bottom > destRect.bottom || srcRect.top >= destRect.bottom) |
| && srcRect.top > destRect.top; |
| case View.FOCUS_DOWN: |
| return (srcRect.top < destRect.top || srcRect.bottom <= destRect.top) |
| && srcRect.bottom < destRect.bottom; |
| } |
| throw new IllegalArgumentException("direction must be one of " |
| + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); |
| } |
| |
| |
| /** |
| * Do the "beams" w.r.t the given direction's axis of rect1 and rect2 overlap? |
| * |
| * @param direction the direction (up, down, left, right) |
| * @param rect1 the first rectangle |
| * @param rect2 the second rectangle |
| * @return whether the beams overlap |
| */ |
| private static boolean beamsOverlap(@FocusRealDirection int direction, |
| @NonNull Rect rect1, @NonNull Rect rect2) { |
| switch (direction) { |
| case View.FOCUS_LEFT: |
| case View.FOCUS_RIGHT: |
| return (rect2.bottom >= rect1.top) && (rect2.top <= rect1.bottom); |
| case View.FOCUS_UP: |
| case View.FOCUS_DOWN: |
| return (rect2.right >= rect1.left) && (rect2.left <= rect1.right); |
| } |
| throw new IllegalArgumentException("direction must be one of " |
| + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); |
| } |
| |
| /** |
| * e.g for left, is 'to left of' |
| */ |
| private static boolean isToDirectionOf(@FocusRealDirection int direction, |
| @NonNull Rect src, @NonNull Rect dest) { |
| switch (direction) { |
| case View.FOCUS_LEFT: |
| return src.left >= dest.right; |
| case View.FOCUS_RIGHT: |
| return src.right <= dest.left; |
| case View.FOCUS_UP: |
| return src.top >= dest.bottom; |
| case View.FOCUS_DOWN: |
| return src.bottom <= dest.top; |
| } |
| throw new IllegalArgumentException("direction must be one of " |
| + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); |
| } |
| |
| /** |
| * @return the distance from the edge furthest in the given direction |
| * of source to the edge nearest in the given direction of |
| * dest. If the dest is not in the direction from source, |
| * returns 0. |
| */ |
| private static int majorAxisDistance(@FocusRealDirection int direction, |
| @NonNull Rect source, @NonNull Rect dest) { |
| return Math.max(0, majorAxisDistanceRaw(direction, source, dest)); |
| } |
| |
| private static int majorAxisDistanceRaw(@FocusRealDirection int direction, |
| @NonNull Rect source, @NonNull Rect dest) { |
| switch (direction) { |
| case View.FOCUS_LEFT: |
| return source.left - dest.right; |
| case View.FOCUS_RIGHT: |
| return dest.left - source.right; |
| case View.FOCUS_UP: |
| return source.top - dest.bottom; |
| case View.FOCUS_DOWN: |
| return dest.top - source.bottom; |
| } |
| throw new IllegalArgumentException("direction must be one of " |
| + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); |
| } |
| |
| /** |
| * @return the distance along the major axis w.r.t the direction from |
| * the edge of source to the far edge of dest. If the dest is |
| * not in the direction from source, returns 1 to break ties |
| * with {@link #majorAxisDistance}. |
| */ |
| private static int majorAxisDistanceToFarEdge(@FocusRealDirection int direction, |
| @NonNull Rect source, @NonNull Rect dest) { |
| return Math.max(1, majorAxisDistanceToFarEdgeRaw(direction, source, dest)); |
| } |
| |
| private static int majorAxisDistanceToFarEdgeRaw( |
| @FocusRealDirection int direction, @NonNull Rect source, |
| @NonNull Rect dest) { |
| switch (direction) { |
| case View.FOCUS_LEFT: |
| return source.left - dest.left; |
| case View.FOCUS_RIGHT: |
| return dest.right - source.right; |
| case View.FOCUS_UP: |
| return source.top - dest.top; |
| case View.FOCUS_DOWN: |
| return dest.bottom - source.bottom; |
| } |
| throw new IllegalArgumentException("direction must be one of " |
| + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); |
| } |
| |
| /** |
| * Finds the distance on the minor axis w.r.t the direction to the |
| * nearest edge of the destination rectangle. |
| * |
| * @param direction the direction (up, down, left, right) |
| * @param source the source rect |
| * @param dest the destination rect |
| * @return the distance |
| */ |
| private static int minorAxisDistance(@FocusRealDirection int direction, @NonNull Rect source, |
| @NonNull Rect dest) { |
| switch (direction) { |
| case View.FOCUS_LEFT: |
| case View.FOCUS_RIGHT: |
| // the distance between the center verticals |
| return Math.abs( |
| ((source.top + source.height() / 2) - |
| ((dest.top + dest.height() / 2)))); |
| case View.FOCUS_UP: |
| case View.FOCUS_DOWN: |
| // the distance between the center horizontals |
| return Math.abs( |
| ((source.left + source.width() / 2) - |
| ((dest.left + dest.width() / 2)))); |
| } |
| throw new IllegalArgumentException("direction must be one of " |
| + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); |
| } |
| |
| /** |
| * Adapter used to obtain bounds from a generic data type. |
| */ |
| public interface BoundsAdapter<T> { |
| void obtainBounds(T data, Rect outBounds); |
| } |
| |
| /** |
| * Adapter used to obtain items from a generic collection type. |
| */ |
| public interface CollectionAdapter<T, V> { |
| V get(T collection, int index); |
| int size(T collection); |
| } |
| } |