998 lines
38 KiB
Java
998 lines
38 KiB
Java
![]() |
/*
|
||
|
* 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.annotation.NonNull;
|
||
|
import android.annotation.Nullable;
|
||
|
import android.annotation.TestApi;
|
||
|
import android.content.pm.PackageManager;
|
||
|
import android.graphics.Rect;
|
||
|
import android.util.ArrayMap;
|
||
|
import android.util.ArraySet;
|
||
|
|
||
|
import java.util.ArrayList;
|
||
|
import java.util.Arrays;
|
||
|
import java.util.Collections;
|
||
|
import java.util.Comparator;
|
||
|
import java.util.HashMap;
|
||
|
import java.util.List;
|
||
|
|
||
|
/**
|
||
|
* 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 final ThreadLocal<FocusFinder> tlFocusFinder =
|
||
|
new ThreadLocal<FocusFinder>() {
|
||
|
@Override
|
||
|
protected FocusFinder initialValue() {
|
||
|
return new FocusFinder();
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Get the focus finder for this thread.
|
||
|
*/
|
||
|
public static FocusFinder getInstance() {
|
||
|
return tlFocusFinder.get();
|
||
|
}
|
||
|
|
||
|
final Rect mFocusedRect = new Rect();
|
||
|
final Rect mOtherRect = new Rect();
|
||
|
final Rect mBestCandidateRect = new Rect();
|
||
|
private final UserSpecifiedFocusComparator mUserSpecifiedFocusComparator =
|
||
|
new UserSpecifiedFocusComparator((r, v) -> isValidId(v.getNextFocusForwardId())
|
||
|
? v.findUserSetNextFocus(r, View.FOCUS_FORWARD) : null);
|
||
|
private final UserSpecifiedFocusComparator mUserSpecifiedClusterComparator =
|
||
|
new UserSpecifiedFocusComparator((r, v) -> isValidId(v.getNextClusterForwardId())
|
||
|
? v.findUserSetNextKeyboardNavigationCluster(r, View.FOCUS_FORWARD) : null);
|
||
|
private final FocusSorter mFocusSorter = new FocusSorter();
|
||
|
|
||
|
private final ArrayList<View> mTempList = new ArrayList<View>();
|
||
|
|
||
|
// 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. Cannot be null.
|
||
|
* @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) {
|
||
|
return findNextFocus(root, focused, null, 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. Cannot be null.
|
||
|
* @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) {
|
||
|
mFocusedRect.set(focusedRect);
|
||
|
return findNextFocus(root, null, mFocusedRect, direction);
|
||
|
}
|
||
|
|
||
|
private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
|
||
|
View next = null;
|
||
|
ViewGroup effectiveRoot = getEffectiveRoot(root, focused);
|
||
|
if (focused != null) {
|
||
|
next = findNextUserSpecifiedFocus(effectiveRoot, focused, direction);
|
||
|
}
|
||
|
if (next != null) {
|
||
|
return next;
|
||
|
}
|
||
|
ArrayList<View> focusables = mTempList;
|
||
|
try {
|
||
|
focusables.clear();
|
||
|
effectiveRoot.addFocusables(focusables, direction);
|
||
|
if (!focusables.isEmpty()) {
|
||
|
next = findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables);
|
||
|
}
|
||
|
} finally {
|
||
|
focusables.clear();
|
||
|
}
|
||
|
return next;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the "effective" root of a view. The "effective" root is the closest ancestor
|
||
|
* within-which focus should cycle.
|
||
|
* <p>
|
||
|
* For example: normal focus navigation would stay within a ViewGroup marked as
|
||
|
* touchscreenBlocksFocus and keyboardNavigationCluster until a cluster-jump out.
|
||
|
* @return the "effective" root of {@param focused}
|
||
|
*/
|
||
|
private ViewGroup getEffectiveRoot(ViewGroup root, View focused) {
|
||
|
if (focused == null || focused == root) {
|
||
|
return root;
|
||
|
}
|
||
|
ViewGroup effective = null;
|
||
|
ViewParent nextParent = focused.getParent();
|
||
|
while (nextParent instanceof ViewGroup) {
|
||
|
if (nextParent == root) {
|
||
|
return effective != null ? effective : root;
|
||
|
}
|
||
|
ViewGroup vg = (ViewGroup) nextParent;
|
||
|
if (vg.getTouchscreenBlocksFocus()
|
||
|
&& focused.getContext().getPackageManager().hasSystemFeature(
|
||
|
PackageManager.FEATURE_TOUCHSCREEN)
|
||
|
&& vg.isKeyboardNavigationCluster()) {
|
||
|
// Don't stop and return here because the cluster could be nested and we only
|
||
|
// care about the top-most one.
|
||
|
effective = vg;
|
||
|
}
|
||
|
nextParent = nextParent.getParent();
|
||
|
}
|
||
|
return root;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Find the root of the next keyboard navigation cluster after the current one.
|
||
|
* @param root The view tree to look inside. Cannot be null
|
||
|
* @param currentCluster The starting point of the search. Null means the default cluster
|
||
|
* @param direction Direction to look
|
||
|
* @return The next cluster, or null if none exists
|
||
|
*/
|
||
|
public View findNextKeyboardNavigationCluster(
|
||
|
@NonNull View root,
|
||
|
@Nullable View currentCluster,
|
||
|
@View.FocusDirection int direction) {
|
||
|
View next = null;
|
||
|
if (currentCluster != null) {
|
||
|
next = findNextUserSpecifiedKeyboardNavigationCluster(root, currentCluster, direction);
|
||
|
if (next != null) {
|
||
|
return next;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
final ArrayList<View> clusters = mTempList;
|
||
|
try {
|
||
|
clusters.clear();
|
||
|
root.addKeyboardNavigationClusters(clusters, direction);
|
||
|
if (!clusters.isEmpty()) {
|
||
|
next = findNextKeyboardNavigationCluster(
|
||
|
root, currentCluster, clusters, direction);
|
||
|
}
|
||
|
} finally {
|
||
|
clusters.clear();
|
||
|
}
|
||
|
return next;
|
||
|
}
|
||
|
|
||
|
private View findNextUserSpecifiedKeyboardNavigationCluster(View root, View currentCluster,
|
||
|
int direction) {
|
||
|
View userSetNextCluster =
|
||
|
currentCluster.findUserSetNextKeyboardNavigationCluster(root, direction);
|
||
|
if (userSetNextCluster != null && userSetNextCluster.hasFocusable()) {
|
||
|
return userSetNextCluster;
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
private View findNextUserSpecifiedFocus(ViewGroup root, View focused, int direction) {
|
||
|
// check for user specified next focus
|
||
|
View userSetNextFocus = focused.findUserSetNextFocus(root, direction);
|
||
|
View cycleCheck = userSetNextFocus;
|
||
|
boolean cycleStep = true; // we want the first toggle to yield false
|
||
|
while (userSetNextFocus != null) {
|
||
|
if (userSetNextFocus.isFocusable()
|
||
|
&& userSetNextFocus.getVisibility() == View.VISIBLE
|
||
|
&& (!userSetNextFocus.isInTouchMode()
|
||
|
|| userSetNextFocus.isFocusableInTouchMode())) {
|
||
|
return userSetNextFocus;
|
||
|
}
|
||
|
userSetNextFocus = userSetNextFocus.findUserSetNextFocus(root, direction);
|
||
|
if (cycleStep = !cycleStep) {
|
||
|
cycleCheck = cycleCheck.findUserSetNextFocus(root, direction);
|
||
|
if (cycleCheck == userSetNextFocus) {
|
||
|
// found a cycle, user-specified focus forms a loop and none of the views
|
||
|
// are currently focusable.
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
private View findNextFocus(ViewGroup root, View focused, Rect focusedRect,
|
||
|
int direction, ArrayList<View> focusables) {
|
||
|
if (focused != null) {
|
||
|
if (focusedRect == null) {
|
||
|
focusedRect = mFocusedRect;
|
||
|
}
|
||
|
// fill in interesting rect from focused
|
||
|
focused.getFocusedRect(focusedRect);
|
||
|
root.offsetDescendantRectToMyCoords(focused, focusedRect);
|
||
|
} else {
|
||
|
if (focusedRect == null) {
|
||
|
focusedRect = mFocusedRect;
|
||
|
// make up a rect at top left or bottom right of root
|
||
|
switch (direction) {
|
||
|
case View.FOCUS_RIGHT:
|
||
|
case View.FOCUS_DOWN:
|
||
|
setFocusTopLeft(root, focusedRect);
|
||
|
break;
|
||
|
case View.FOCUS_FORWARD:
|
||
|
if (root.isLayoutRtl()) {
|
||
|
setFocusBottomRight(root, focusedRect);
|
||
|
} else {
|
||
|
setFocusTopLeft(root, focusedRect);
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case View.FOCUS_LEFT:
|
||
|
case View.FOCUS_UP:
|
||
|
setFocusBottomRight(root, focusedRect);
|
||
|
break;
|
||
|
case View.FOCUS_BACKWARD:
|
||
|
if (root.isLayoutRtl()) {
|
||
|
setFocusTopLeft(root, focusedRect);
|
||
|
} else {
|
||
|
setFocusBottomRight(root, focusedRect);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
switch (direction) {
|
||
|
case View.FOCUS_FORWARD:
|
||
|
case View.FOCUS_BACKWARD:
|
||
|
return findNextFocusInRelativeDirection(focusables, root, focused, focusedRect,
|
||
|
direction);
|
||
|
case View.FOCUS_UP:
|
||
|
case View.FOCUS_DOWN:
|
||
|
case View.FOCUS_LEFT:
|
||
|
case View.FOCUS_RIGHT:
|
||
|
return findNextFocusInAbsoluteDirection(focusables, root, focused,
|
||
|
focusedRect, direction);
|
||
|
default:
|
||
|
throw new IllegalArgumentException("Unknown direction: " + direction);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private View findNextKeyboardNavigationCluster(
|
||
|
View root,
|
||
|
View currentCluster,
|
||
|
List<View> clusters,
|
||
|
@View.FocusDirection int direction) {
|
||
|
try {
|
||
|
// Note: This sort is stable.
|
||
|
mUserSpecifiedClusterComparator.setFocusables(clusters, root);
|
||
|
Collections.sort(clusters, mUserSpecifiedClusterComparator);
|
||
|
} finally {
|
||
|
mUserSpecifiedClusterComparator.recycle();
|
||
|
}
|
||
|
final int count = clusters.size();
|
||
|
|
||
|
switch (direction) {
|
||
|
case View.FOCUS_FORWARD:
|
||
|
case View.FOCUS_DOWN:
|
||
|
case View.FOCUS_RIGHT:
|
||
|
return getNextKeyboardNavigationCluster(root, currentCluster, clusters, count);
|
||
|
case View.FOCUS_BACKWARD:
|
||
|
case View.FOCUS_UP:
|
||
|
case View.FOCUS_LEFT:
|
||
|
return getPreviousKeyboardNavigationCluster(root, currentCluster, clusters, count);
|
||
|
default:
|
||
|
throw new IllegalArgumentException("Unknown direction: " + direction);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private View findNextFocusInRelativeDirection(ArrayList<View> focusables, ViewGroup root,
|
||
|
View focused, Rect focusedRect, int direction) {
|
||
|
try {
|
||
|
// Note: This sort is stable.
|
||
|
mUserSpecifiedFocusComparator.setFocusables(focusables, root);
|
||
|
Collections.sort(focusables, mUserSpecifiedFocusComparator);
|
||
|
} finally {
|
||
|
mUserSpecifiedFocusComparator.recycle();
|
||
|
}
|
||
|
|
||
|
final int count = focusables.size();
|
||
|
if (count < 2) {
|
||
|
return null;
|
||
|
}
|
||
|
View next = null;
|
||
|
final boolean[] looped = new boolean[1];
|
||
|
switch (direction) {
|
||
|
case View.FOCUS_FORWARD:
|
||
|
next = getNextFocusable(focused, focusables, count, looped);
|
||
|
break;
|
||
|
case View.FOCUS_BACKWARD:
|
||
|
next = getPreviousFocusable(focused, focusables, count, looped);
|
||
|
break;
|
||
|
}
|
||
|
if (root != null && root.mAttachInfo != null && root == root.getRootView()) {
|
||
|
root.mAttachInfo.mNextFocusLooped = looped[0];
|
||
|
}
|
||
|
return next != null ? next : focusables.get(count - 1);
|
||
|
}
|
||
|
|
||
|
private void setFocusBottomRight(ViewGroup root, Rect focusedRect) {
|
||
|
final int rootBottom = root.getScrollY() + root.getHeight();
|
||
|
final int rootRight = root.getScrollX() + root.getWidth();
|
||
|
focusedRect.set(rootRight, rootBottom, rootRight, rootBottom);
|
||
|
}
|
||
|
|
||
|
private void setFocusTopLeft(ViewGroup root, Rect focusedRect) {
|
||
|
final int rootTop = root.getScrollY();
|
||
|
final int rootLeft = root.getScrollX();
|
||
|
focusedRect.set(rootLeft, rootTop, rootLeft, rootTop);
|
||
|
}
|
||
|
|
||
|
View findNextFocusInAbsoluteDirection(ArrayList<View> focusables, ViewGroup root, View focused,
|
||
|
Rect focusedRect, int 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 focus bounds of other view in same coordinate system
|
||
|
focusable.getFocusedRect(mOtherRect);
|
||
|
root.offsetDescendantRectToMyCoords(focusable, mOtherRect);
|
||
|
|
||
|
if (isBetterCandidate(direction, focusedRect, mOtherRect, mBestCandidateRect)) {
|
||
|
mBestCandidateRect.set(mOtherRect);
|
||
|
closest = focusable;
|
||
|
}
|
||
|
}
|
||
|
return closest;
|
||
|
}
|
||
|
|
||
|
private static View getNextFocusable(View focused, ArrayList<View> focusables, int count,
|
||
|
boolean[] outLooped) {
|
||
|
if (count < 2) {
|
||
|
return null;
|
||
|
}
|
||
|
if (focused != null) {
|
||
|
int position = focusables.lastIndexOf(focused);
|
||
|
if (position >= 0 && position + 1 < count) {
|
||
|
return focusables.get(position + 1);
|
||
|
}
|
||
|
}
|
||
|
outLooped[0] = true;
|
||
|
return focusables.get(0);
|
||
|
}
|
||
|
|
||
|
private static View getPreviousFocusable(View focused, ArrayList<View> focusables, int count,
|
||
|
boolean[] outLooped) {
|
||
|
if (count < 2) {
|
||
|
return null;
|
||
|
}
|
||
|
if (focused != null) {
|
||
|
int position = focusables.indexOf(focused);
|
||
|
if (position > 0) {
|
||
|
return focusables.get(position - 1);
|
||
|
}
|
||
|
}
|
||
|
outLooped[0] = true;
|
||
|
return focusables.get(count - 1);
|
||
|
}
|
||
|
|
||
|
private static View getNextKeyboardNavigationCluster(
|
||
|
View root,
|
||
|
View currentCluster,
|
||
|
List<View> clusters,
|
||
|
int count) {
|
||
|
if (currentCluster == null) {
|
||
|
// The current cluster is the default one.
|
||
|
// The next cluster after the default one is the first one.
|
||
|
// Note that the caller guarantees that 'clusters' is not empty.
|
||
|
return clusters.get(0);
|
||
|
}
|
||
|
|
||
|
final int position = clusters.lastIndexOf(currentCluster);
|
||
|
if (position >= 0 && position + 1 < count) {
|
||
|
// Return the next non-default cluster if we can find it.
|
||
|
return clusters.get(position + 1);
|
||
|
}
|
||
|
|
||
|
// The current cluster is the last one. The next one is the default one, i.e. the
|
||
|
// root.
|
||
|
return root;
|
||
|
}
|
||
|
|
||
|
private static View getPreviousKeyboardNavigationCluster(
|
||
|
View root,
|
||
|
View currentCluster,
|
||
|
List<View> clusters,
|
||
|
int count) {
|
||
|
if (currentCluster == null) {
|
||
|
// The current cluster is the default one.
|
||
|
// The previous cluster before the default one is the last one.
|
||
|
// Note that the caller guarantees that 'clusters' is not empty.
|
||
|
return clusters.get(count - 1);
|
||
|
}
|
||
|
|
||
|
final int position = clusters.indexOf(currentCluster);
|
||
|
if (position > 0) {
|
||
|
// Return the previous non-default cluster if we can find it.
|
||
|
return clusters.get(position - 1);
|
||
|
}
|
||
|
|
||
|
// The current cluster is the first one. The previous one is the default one, i.e.
|
||
|
// the root.
|
||
|
return root;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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.
|
||
|
*/
|
||
|
long getWeightedDistanceFor(long majorAxisDistance, long 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 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
|
||
|
*/
|
||
|
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 rectangle.
|
||
|
* @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}.");
|
||
|
}
|
||
|
|
||
|
private static final boolean isValidId(final int id) {
|
||
|
return id != 0 && id != View.NO_ID;
|
||
|
}
|
||
|
|
||
|
static final class FocusSorter {
|
||
|
private ArrayList<Rect> mRectPool = new ArrayList<>();
|
||
|
private int mLastPoolRect;
|
||
|
private int mRtlMult;
|
||
|
private HashMap<View, Rect> mRectByView = null;
|
||
|
|
||
|
private Comparator<View> mTopsComparator = (first, second) -> {
|
||
|
if (first == second) {
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
Rect firstRect = mRectByView.get(first);
|
||
|
Rect secondRect = mRectByView.get(second);
|
||
|
|
||
|
int result = firstRect.top - secondRect.top;
|
||
|
if (result == 0) {
|
||
|
return firstRect.bottom - secondRect.bottom;
|
||
|
}
|
||
|
return result;
|
||
|
};
|
||
|
|
||
|
private Comparator<View> mSidesComparator = (first, second) -> {
|
||
|
if (first == second) {
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
Rect firstRect = mRectByView.get(first);
|
||
|
Rect secondRect = mRectByView.get(second);
|
||
|
|
||
|
int result = firstRect.left - secondRect.left;
|
||
|
if (result == 0) {
|
||
|
return firstRect.right - secondRect.right;
|
||
|
}
|
||
|
return mRtlMult * result;
|
||
|
};
|
||
|
|
||
|
public void sort(View[] views, int start, int end, ViewGroup root, boolean isRtl) {
|
||
|
int count = end - start;
|
||
|
if (count < 2) {
|
||
|
return;
|
||
|
}
|
||
|
if (mRectByView == null) {
|
||
|
mRectByView = new HashMap<>();
|
||
|
}
|
||
|
mRtlMult = isRtl ? -1 : 1;
|
||
|
for (int i = mRectPool.size(); i < count; ++i) {
|
||
|
mRectPool.add(new Rect());
|
||
|
}
|
||
|
for (int i = start; i < end; ++i) {
|
||
|
Rect next = mRectPool.get(mLastPoolRect++);
|
||
|
views[i].getDrawingRect(next);
|
||
|
root.offsetDescendantRectToMyCoords(views[i], next);
|
||
|
mRectByView.put(views[i], next);
|
||
|
}
|
||
|
|
||
|
// Sort top-to-bottom
|
||
|
Arrays.sort(views, start, count, mTopsComparator);
|
||
|
// Sweep top-to-bottom to identify rows
|
||
|
int sweepBottom = mRectByView.get(views[start]).bottom;
|
||
|
int rowStart = start;
|
||
|
int sweepIdx = start + 1;
|
||
|
for (; sweepIdx < end; ++sweepIdx) {
|
||
|
Rect currRect = mRectByView.get(views[sweepIdx]);
|
||
|
if (currRect.top >= sweepBottom) {
|
||
|
// Next view is on a new row, sort the row we've just finished left-to-right.
|
||
|
if ((sweepIdx - rowStart) > 1) {
|
||
|
Arrays.sort(views, rowStart, sweepIdx, mSidesComparator);
|
||
|
}
|
||
|
sweepBottom = currRect.bottom;
|
||
|
rowStart = sweepIdx;
|
||
|
} else {
|
||
|
// Next view vertically overlaps, we need to extend our "row height"
|
||
|
sweepBottom = Math.max(sweepBottom, currRect.bottom);
|
||
|
}
|
||
|
}
|
||
|
// Sort whatever's left (final row) left-to-right
|
||
|
if ((sweepIdx - rowStart) > 1) {
|
||
|
Arrays.sort(views, rowStart, sweepIdx, mSidesComparator);
|
||
|
}
|
||
|
|
||
|
mLastPoolRect = 0;
|
||
|
mRectByView.clear();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Public for testing.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
@TestApi
|
||
|
public static void sort(View[] views, int start, int end, ViewGroup root, boolean isRtl) {
|
||
|
getInstance().mFocusSorter.sort(views, start, end, root, isRtl);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sorts views according to any explicitly-specified focus-chains. If there are no explicitly
|
||
|
* specified focus chains (eg. no nextFocusForward attributes defined), this should be a no-op.
|
||
|
*/
|
||
|
private static final class UserSpecifiedFocusComparator implements Comparator<View> {
|
||
|
private final ArrayMap<View, View> mNextFoci = new ArrayMap<>();
|
||
|
private final ArraySet<View> mIsConnectedTo = new ArraySet<>();
|
||
|
private final ArrayMap<View, View> mHeadsOfChains = new ArrayMap<View, View>();
|
||
|
private final ArrayMap<View, Integer> mOriginalOrdinal = new ArrayMap<>();
|
||
|
private final NextFocusGetter mNextFocusGetter;
|
||
|
private View mRoot;
|
||
|
|
||
|
public interface NextFocusGetter {
|
||
|
View get(View root, View view);
|
||
|
}
|
||
|
|
||
|
UserSpecifiedFocusComparator(NextFocusGetter nextFocusGetter) {
|
||
|
mNextFocusGetter = nextFocusGetter;
|
||
|
}
|
||
|
|
||
|
public void recycle() {
|
||
|
mRoot = null;
|
||
|
mHeadsOfChains.clear();
|
||
|
mIsConnectedTo.clear();
|
||
|
mOriginalOrdinal.clear();
|
||
|
mNextFoci.clear();
|
||
|
}
|
||
|
|
||
|
public void setFocusables(List<View> focusables, View root) {
|
||
|
mRoot = root;
|
||
|
for (int i = 0; i < focusables.size(); ++i) {
|
||
|
mOriginalOrdinal.put(focusables.get(i), i);
|
||
|
}
|
||
|
|
||
|
for (int i = focusables.size() - 1; i >= 0; i--) {
|
||
|
final View view = focusables.get(i);
|
||
|
final View next = mNextFocusGetter.get(mRoot, view);
|
||
|
if (next != null && mOriginalOrdinal.containsKey(next)) {
|
||
|
mNextFoci.put(view, next);
|
||
|
mIsConnectedTo.add(next);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
for (int i = focusables.size() - 1; i >= 0; i--) {
|
||
|
final View view = focusables.get(i);
|
||
|
final View next = mNextFoci.get(view);
|
||
|
if (next != null && !mIsConnectedTo.contains(view)) {
|
||
|
setHeadOfChain(view);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void setHeadOfChain(View head) {
|
||
|
for (View view = head; view != null; view = mNextFoci.get(view)) {
|
||
|
final View otherHead = mHeadsOfChains.get(view);
|
||
|
if (otherHead != null) {
|
||
|
if (otherHead == head) {
|
||
|
return; // This view has already had its head set properly
|
||
|
}
|
||
|
// A hydra -- multi-headed focus chain (e.g. A->C and B->C)
|
||
|
// Use the one we've already chosen instead and reset this chain.
|
||
|
view = head;
|
||
|
head = otherHead;
|
||
|
}
|
||
|
mHeadsOfChains.put(view, head);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public int compare(View first, View second) {
|
||
|
if (first == second) {
|
||
|
return 0;
|
||
|
}
|
||
|
// Order between views within a chain is immaterial -- next/previous is
|
||
|
// within a chain is handled elsewhere.
|
||
|
View firstHead = mHeadsOfChains.get(first);
|
||
|
View secondHead = mHeadsOfChains.get(second);
|
||
|
if (firstHead == secondHead && firstHead != null) {
|
||
|
if (first == firstHead) {
|
||
|
return -1; // first is the head, it should be first
|
||
|
} else if (second == firstHead) {
|
||
|
return 1; // second is the head, it should be first
|
||
|
} else if (mNextFoci.get(first) != null) {
|
||
|
return -1; // first is not the end of the chain
|
||
|
} else {
|
||
|
return 1; // first is end of chain
|
||
|
}
|
||
|
}
|
||
|
boolean involvesChain = false;
|
||
|
if (firstHead != null) {
|
||
|
first = firstHead;
|
||
|
involvesChain = true;
|
||
|
}
|
||
|
if (secondHead != null) {
|
||
|
second = secondHead;
|
||
|
involvesChain = true;
|
||
|
}
|
||
|
|
||
|
if (involvesChain) {
|
||
|
// keep original order between chains
|
||
|
return mOriginalOrdinal.get(first) < mOriginalOrdinal.get(second) ? -1 : 1;
|
||
|
} else {
|
||
|
return 0;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|