diff options
Diffstat (limited to 'core/java/android/view/FocusFinder.java')
-rw-r--r-- | core/java/android/view/FocusFinder.java | 480 |
1 files changed, 480 insertions, 0 deletions
diff --git a/core/java/android/view/FocusFinder.java b/core/java/android/view/FocusFinder.java new file mode 100644 index 0000000..15fb839 --- /dev/null +++ b/core/java/android/view/FocusFinder.java @@ -0,0 +1,480 @@ +/* + * Copyright (C) 2007 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.view; + +import android.graphics.Rect; + +import java.util.ArrayList; + +/** + * The algorithm used for finding the next focusable view in a given direction + * from a view that currently has focus. + */ +public class FocusFinder { + + private static ThreadLocal<FocusFinder> tlFocusFinder = + new ThreadLocal<FocusFinder>() { + + protected FocusFinder initialValue() { + return new FocusFinder(); + } + }; + + /** + * Get the focus finder for this thread. + */ + public static FocusFinder getInstance() { + return tlFocusFinder.get(); + } + + Rect mFocusedRect = new Rect(); + Rect mOtherRect = new Rect(); + Rect mBestCandidateRect = new Rect(); + + // enforce thread local access + private FocusFinder() {} + + /** + * Find the next view to take focus in root's descendants, starting from the view + * that currently is focused. + * @param root Contains focused + * @param focused Has focus now. + * @param direction Direction to look. + * @return The next focusable view, or null if none exists. + */ + public final View findNextFocus(ViewGroup root, View focused, int direction) { + + if (focused != null) { + // check for user specified next focus + View userSetNextFocus = focused.findUserSetNextFocus(root, direction); + if (userSetNextFocus != null && + userSetNextFocus.isFocusable() && + (!userSetNextFocus.isInTouchMode() || + userSetNextFocus.isFocusableInTouchMode())) { + return userSetNextFocus; + } + + // fill in interesting rect from focused + focused.getFocusedRect(mFocusedRect); + root.offsetDescendantRectToMyCoords(focused, mFocusedRect); + } else { + // make up a rect at top left or bottom right of root + switch (direction) { + case View.FOCUS_RIGHT: + case View.FOCUS_DOWN: + final int rootTop = root.getScrollY(); + final int rootLeft = root.getScrollX(); + mFocusedRect.set(rootLeft, rootTop, rootLeft, rootTop); + break; + + case View.FOCUS_LEFT: + case View.FOCUS_UP: + final int rootBottom = root.getScrollY() + root.getHeight(); + final int rootRight = root.getScrollX() + root.getWidth(); + mFocusedRect.set(rootRight, rootBottom, + rootRight, rootBottom); + break; + } + } + return findNextFocus(root, focused, mFocusedRect, direction); + } + + /** + * Find the next view to take focus in root's descendants, searching from + * a particular rectangle in root's coordinates. + * @param root Contains focusedRect. + * @param focusedRect The starting point of the search. + * @param direction Direction to look. + * @return The next focusable view, or null if none exists. + */ + public View findNextFocusFromRect(ViewGroup root, Rect focusedRect, int direction) { + return findNextFocus(root, null, focusedRect, direction); + } + + private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) { + ArrayList<View> focusables = root.getFocusables(direction); + + // initialize the best candidate to something impossible + // (so the first plausible view will become the best choice) + mBestCandidateRect.set(focusedRect); + switch(direction) { + case View.FOCUS_LEFT: + mBestCandidateRect.offset(focusedRect.width() + 1, 0); + break; + case View.FOCUS_RIGHT: + mBestCandidateRect.offset(-(focusedRect.width() + 1), 0); + break; + case View.FOCUS_UP: + mBestCandidateRect.offset(0, focusedRect.height() + 1); + break; + case View.FOCUS_DOWN: + mBestCandidateRect.offset(0, -(focusedRect.height() + 1)); + } + + View closest = null; + + int numFocusables = focusables.size(); + for (int i = 0; i < numFocusables; i++) { + View focusable = focusables.get(i); + + // only interested in other non-root views + if (focusable == focused || focusable == root) continue; + + // get visible bounds of other view in same coordinate system + focusable.getDrawingRect(mOtherRect); + root.offsetDescendantRectToMyCoords(focusable, mOtherRect); + + if (isBetterCandidate(direction, focusedRect, mOtherRect, mBestCandidateRect)) { + mBestCandidateRect.set(mOtherRect); + closest = focusable; + } + } + return closest; + } + + /** + * Is rect1 a better candidate than rect2 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 we are searching from + * @param rect1 The candidate rectangle + * @param rect2 The current best candidate. + * @return Whether the candidate is the new best. + */ + boolean isBetterCandidate(int direction, Rect source, Rect rect1, Rect rect2) { + + // to be a better candidate, need to at least be a candidate in the first + // place :) + if (!isCandidate(source, rect1, direction)) { + return false; + } + + // we know that rect1 is a candidate.. if rect2 is not a candidate, + // rect1 is better + if (!isCandidate(source, rect2, direction)) { + return true; + } + + // if rect1 is better by beam, it wins + if (beamBeats(direction, source, rect1, rect2)) { + return true; + } + + // if rect2 is better, then rect1 cant' be :) + if (beamBeats(direction, source, rect2, rect1)) { + return false; + } + + // otherwise, do fudge-tastic comparison of the major and minor axis + return (getWeightedDistanceFor( + majorAxisDistance(direction, source, rect1), + minorAxisDistance(direction, source, rect1)) + < getWeightedDistanceFor( + majorAxisDistance(direction, source, rect2), + minorAxisDistance(direction, source, rect2))); + } + + /** + * 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 src's + * beam + */ + boolean beamBeats(int direction, Rect source, Rect rect1, 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. Warning: this fudge factor is finely tuned, be sure to + * run all focus tests if you dare tweak it. + */ + 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. + * + * Includes an edge case for an empty rect (which is used in some cases when + * searching from a point on the screen). + */ + boolean isCandidate(Rect srcRect, Rect destRect, 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 direcition's axos 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 + */ + boolean beamsOverlap(int direction, Rect rect1, 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' + */ + boolean isToDirectionOf(int direction, Rect src, 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, return 0. + */ + static int majorAxisDistance(int direction, Rect source, Rect dest) { + return Math.max(0, majorAxisDistanceRaw(direction, source, dest)); + } + + static int majorAxisDistanceRaw(int direction, Rect source, 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, return 1 (to break ties with + * {@link #majorAxisDistance}). + */ + static int majorAxisDistanceToFarEdge(int direction, Rect source, Rect dest) { + return Math.max(1, majorAxisDistanceToFarEdgeRaw(direction, source, dest)); + } + + static int majorAxisDistanceToFarEdgeRaw(int direction, Rect source, 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}."); + } + + /** + * Find the distance on the minor axis w.r.t the direction to the nearest + * edge of the destination rectange. + * @param direction the direction (up, down, left, right) + * @param source The source rect. + * @param dest The destination rect. + * @return The distance. + */ + static int minorAxisDistance(int direction, Rect source, 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}."); + } + + /** + * Find the nearest touchable view to the specified view. + * + * @param root The root of the tree in which to search + * @param x X coordinate from which to start the search + * @param y Y coordinate from which to start the search + * @param direction Direction to look + * @param deltas Offset from the <x, y> to the edge of the nearest view. Note that this array + * may already be populated with values. + * @return The nearest touchable view, or null if none exists. + */ + public View findNearestTouchable(ViewGroup root, int x, int y, int direction, int[] deltas) { + ArrayList<View> touchables = root.getTouchables(); + int minDistance = Integer.MAX_VALUE; + View closest = null; + + int numTouchables = touchables.size(); + + int edgeSlop = ViewConfiguration.get(root.mContext).getScaledEdgeSlop(); + + Rect closestBounds = new Rect(); + Rect touchableBounds = mOtherRect; + + for (int i = 0; i < numTouchables; i++) { + View touchable = touchables.get(i); + + // get visible bounds of other view in same coordinate system + touchable.getDrawingRect(touchableBounds); + + root.offsetRectBetweenParentAndChild(touchable, touchableBounds, true, true); + + if (!isTouchCandidate(x, y, touchableBounds, direction)) { + continue; + } + + int distance = Integer.MAX_VALUE; + + switch (direction) { + case View.FOCUS_LEFT: + distance = x - touchableBounds.right + 1; + break; + case View.FOCUS_RIGHT: + distance = touchableBounds.left; + break; + case View.FOCUS_UP: + distance = y - touchableBounds.bottom + 1; + break; + case View.FOCUS_DOWN: + distance = touchableBounds.top; + break; + } + + if (distance < edgeSlop) { + // Give preference to innermost views + if (closest == null || + closestBounds.contains(touchableBounds) || + (!touchableBounds.contains(closestBounds) && distance < minDistance)) { + minDistance = distance; + closest = touchable; + closestBounds.set(touchableBounds); + switch (direction) { + case View.FOCUS_LEFT: + deltas[0] = -distance; + break; + case View.FOCUS_RIGHT: + deltas[0] = distance; + break; + case View.FOCUS_UP: + deltas[1] = -distance; + break; + case View.FOCUS_DOWN: + deltas[1] = distance; + break; + } + } + } + } + return closest; + } + + + /** + * Is destRect a candidate for the next touch given the direction? + */ + private boolean isTouchCandidate(int x, int y, Rect destRect, int direction) { + switch (direction) { + case View.FOCUS_LEFT: + return destRect.left <= x && destRect.top <= y && y <= destRect.bottom; + case View.FOCUS_RIGHT: + return destRect.left >= x && destRect.top <= y && y <= destRect.bottom; + case View.FOCUS_UP: + return destRect.top <= y && destRect.left <= x && x <= destRect.right; + case View.FOCUS_DOWN: + return destRect.top >= y && destRect.left <= x && x <= destRect.right; + } + throw new IllegalArgumentException("direction must be one of " + + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); + } +} |