blob: 713cfb48c95f13cda7e801c40b1b20ce9ad18b69 [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
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
17package android.view;
18
Vadim Tryshev01b0c9e2016-11-21 15:25:01 -080019import android.annotation.NonNull;
20import android.annotation.Nullable;
Evan Roskyd114e0f2017-03-23 11:20:04 -070021import android.annotation.TestApi;
Evan Rosky18b886e2017-02-15 13:26:51 -080022import android.content.pm.PackageManager;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080023import android.graphics.Rect;
George Mount140ea622015-10-27 08:46:44 -070024import android.util.ArrayMap;
Evan Rosky84485232017-04-06 18:48:33 -070025import android.util.ArraySet;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080026
27import java.util.ArrayList;
Evan Roskyd114e0f2017-03-23 11:20:04 -070028import java.util.Arrays;
Jeff Brown4e6319b2010-12-13 10:36:51 -080029import java.util.Collections;
30import java.util.Comparator;
Evan Roskyd114e0f2017-03-23 11:20:04 -070031import java.util.HashMap;
Vadim Tryshev01b0c9e2016-11-21 15:25:01 -080032import java.util.List;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080033
34/**
35 * The algorithm used for finding the next focusable view in a given direction
36 * from a view that currently has focus.
37 */
38public class FocusFinder {
39
Svetoslav Ganov76f287e2012-04-23 11:02:36 -070040 private static final ThreadLocal<FocusFinder> tlFocusFinder =
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080041 new ThreadLocal<FocusFinder>() {
Svetoslav Ganov42138042012-03-20 11:51:39 -070042 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080043 protected FocusFinder initialValue() {
44 return new FocusFinder();
45 }
46 };
47
48 /**
49 * Get the focus finder for this thread.
50 */
51 public static FocusFinder getInstance() {
52 return tlFocusFinder.get();
53 }
54
Svetoslav Ganov76f287e2012-04-23 11:02:36 -070055 final Rect mFocusedRect = new Rect();
56 final Rect mOtherRect = new Rect();
57 final Rect mBestCandidateRect = new Rect();
Evan Rosky3b94bf52017-01-10 17:05:28 -080058 private final UserSpecifiedFocusComparator mUserSpecifiedFocusComparator =
Evan Rosky84485232017-04-06 18:48:33 -070059 new UserSpecifiedFocusComparator((r, v) -> isValidId(v.getNextFocusForwardId())
60 ? v.findUserSetNextFocus(r, View.FOCUS_FORWARD) : null);
Evan Roskybd10c522017-03-27 15:50:38 -070061 private final UserSpecifiedFocusComparator mUserSpecifiedClusterComparator =
Evan Rosky84485232017-04-06 18:48:33 -070062 new UserSpecifiedFocusComparator((r, v) -> isValidId(v.getNextClusterForwardId())
63 ? v.findUserSetNextKeyboardNavigationCluster(r, View.FOCUS_FORWARD) : null);
Evan Roskyd114e0f2017-03-23 11:20:04 -070064 private final FocusSorter mFocusSorter = new FocusSorter();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080065
Svetoslav Ganov42138042012-03-20 11:51:39 -070066 private final ArrayList<View> mTempList = new ArrayList<View>();
67
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080068 // enforce thread local access
69 private FocusFinder() {}
70
71 /**
72 * Find the next view to take focus in root's descendants, starting from the view
73 * that currently is focused.
Fabrice Di Meglio7e0a3722012-03-27 16:06:25 -070074 * @param root Contains focused. Cannot be null.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080075 * @param focused Has focus now.
76 * @param direction Direction to look.
77 * @return The next focusable view, or null if none exists.
78 */
79 public final View findNextFocus(ViewGroup root, View focused, int direction) {
Svetoslav Ganov951bb422012-04-30 13:15:21 -070080 return findNextFocus(root, focused, null, direction);
Svetoslav Ganov42138042012-03-20 11:51:39 -070081 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080082
Svetoslav Ganov42138042012-03-20 11:51:39 -070083 /**
84 * Find the next view to take focus in root's descendants, searching from
85 * a particular rectangle in root's coordinates.
86 * @param root Contains focusedRect. Cannot be null.
87 * @param focusedRect The starting point of the search.
88 * @param direction Direction to look.
89 * @return The next focusable view, or null if none exists.
90 */
91 public View findNextFocusFromRect(ViewGroup root, Rect focusedRect, int direction) {
Svetoslav Ganov76f287e2012-04-23 11:02:36 -070092 mFocusedRect.set(focusedRect);
93 return findNextFocus(root, null, mFocusedRect, direction);
Svetoslav Ganov42138042012-03-20 11:51:39 -070094 }
95
96 private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
Svetoslav Ganov76f287e2012-04-23 11:02:36 -070097 View next = null;
Evan Rosky18b886e2017-02-15 13:26:51 -080098 ViewGroup effectiveRoot = getEffectiveRoot(root, focused);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080099 if (focused != null) {
Evan Rosky18b886e2017-02-15 13:26:51 -0800100 next = findNextUserSpecifiedFocus(effectiveRoot, focused, direction);
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700101 }
102 if (next != null) {
103 return next;
104 }
105 ArrayList<View> focusables = mTempList;
106 try {
107 focusables.clear();
Evan Rosky18b886e2017-02-15 13:26:51 -0800108 effectiveRoot.addFocusables(focusables, direction);
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700109 if (!focusables.isEmpty()) {
Evan Rosky18b886e2017-02-15 13:26:51 -0800110 next = findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800111 }
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700112 } finally {
113 focusables.clear();
114 }
115 return next;
116 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800117
Vadim Tryshev01b0c9e2016-11-21 15:25:01 -0800118 /**
Evan Rosky18b886e2017-02-15 13:26:51 -0800119 * Returns the "effective" root of a view. The "effective" root is the closest ancestor
120 * within-which focus should cycle.
121 * <p>
122 * For example: normal focus navigation would stay within a ViewGroup marked as
123 * touchscreenBlocksFocus and keyboardNavigationCluster until a cluster-jump out.
124 * @return the "effective" root of {@param focused}
125 */
126 private ViewGroup getEffectiveRoot(ViewGroup root, View focused) {
Evan Roskye55f4a62017-04-03 11:37:20 -0700127 if (focused == null || focused == root) {
Evan Rosky18b886e2017-02-15 13:26:51 -0800128 return root;
129 }
Evan Rosky0e8a6832017-04-10 12:35:15 -0700130 ViewGroup effective = null;
131 ViewParent nextParent = focused.getParent();
Evan Rosky18b886e2017-02-15 13:26:51 -0800132 do {
Evan Rosky0e8a6832017-04-10 12:35:15 -0700133 if (nextParent == root) {
134 return effective != null ? effective : root;
Evan Rosky18b886e2017-02-15 13:26:51 -0800135 }
Evan Rosky0e8a6832017-04-10 12:35:15 -0700136 ViewGroup vg = (ViewGroup) nextParent;
Evan Rosky18b886e2017-02-15 13:26:51 -0800137 if (vg.getTouchscreenBlocksFocus()
138 && focused.getContext().getPackageManager().hasSystemFeature(
139 PackageManager.FEATURE_TOUCHSCREEN)
140 && vg.isKeyboardNavigationCluster()) {
Evan Rosky0e8a6832017-04-10 12:35:15 -0700141 // Don't stop and return here because the cluster could be nested and we only
142 // care about the top-most one.
143 effective = vg;
Evan Rosky18b886e2017-02-15 13:26:51 -0800144 }
Evan Rosky0e8a6832017-04-10 12:35:15 -0700145 nextParent = nextParent.getParent();
146 } while (nextParent instanceof ViewGroup);
Evan Rosky18b886e2017-02-15 13:26:51 -0800147 return root;
148 }
149
150 /**
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800151 * Find the root of the next keyboard navigation cluster after the current one.
Vadim Tryshev8957f2d2016-12-21 19:22:26 -0800152 * @param root The view tree to look inside. Cannot be null
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800153 * @param currentCluster The starting point of the search. Null means the default cluster
Vadim Tryshev01b0c9e2016-11-21 15:25:01 -0800154 * @param direction Direction to look
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800155 * @return The next cluster, or null if none exists
Vadim Tryshev01b0c9e2016-11-21 15:25:01 -0800156 */
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800157 public View findNextKeyboardNavigationCluster(
Vadim Tryshev8957f2d2016-12-21 19:22:26 -0800158 @NonNull View root,
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800159 @Nullable View currentCluster,
Evan Rosky57223312017-02-08 14:42:45 -0800160 @View.FocusDirection int direction) {
Vadim Tryshev01b0c9e2016-11-21 15:25:01 -0800161 View next = null;
Evan Roskybd10c522017-03-27 15:50:38 -0700162 if (currentCluster != null) {
163 next = findNextUserSpecifiedKeyboardNavigationCluster(root, currentCluster, direction);
164 if (next != null) {
165 return next;
166 }
167 }
Vadim Tryshev01b0c9e2016-11-21 15:25:01 -0800168
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800169 final ArrayList<View> clusters = mTempList;
Vadim Tryshev01b0c9e2016-11-21 15:25:01 -0800170 try {
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800171 clusters.clear();
172 root.addKeyboardNavigationClusters(clusters, direction);
173 if (!clusters.isEmpty()) {
174 next = findNextKeyboardNavigationCluster(
175 root, currentCluster, clusters, direction);
Vadim Tryshev01b0c9e2016-11-21 15:25:01 -0800176 }
177 } finally {
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800178 clusters.clear();
Vadim Tryshev01b0c9e2016-11-21 15:25:01 -0800179 }
180 return next;
181 }
182
Evan Roskybd10c522017-03-27 15:50:38 -0700183 private View findNextUserSpecifiedKeyboardNavigationCluster(View root, View currentCluster,
184 int direction) {
185 View userSetNextCluster =
186 currentCluster.findUserSetNextKeyboardNavigationCluster(root, direction);
187 if (userSetNextCluster != null && userSetNextCluster.hasFocusable()) {
188 return userSetNextCluster;
189 }
190 return null;
191 }
192
Svetoslav Ganov27e2da72012-07-02 18:12:00 -0700193 private View findNextUserSpecifiedFocus(ViewGroup root, View focused, int direction) {
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700194 // check for user specified next focus
195 View userSetNextFocus = focused.findUserSetNextFocus(root, direction);
Evan Rosky9d934862017-07-12 10:58:07 -0700196 View cycleCheck = userSetNextFocus;
197 boolean cycleStep = true; // we want the first toggle to yield false
Evan Roskyca6b8762017-05-01 16:36:24 -0700198 while (userSetNextFocus != null) {
199 if (userSetNextFocus.isFocusable()
200 && userSetNextFocus.getVisibility() == View.VISIBLE
201 && (!userSetNextFocus.isInTouchMode()
202 || userSetNextFocus.isFocusableInTouchMode())) {
203 return userSetNextFocus;
204 }
205 userSetNextFocus = userSetNextFocus.findUserSetNextFocus(root, direction);
Evan Rosky9d934862017-07-12 10:58:07 -0700206 if (cycleStep = !cycleStep) {
207 cycleCheck = cycleCheck.findUserSetNextFocus(root, direction);
208 if (cycleCheck == userSetNextFocus) {
209 // found a cycle, user-specified focus forms a loop and none of the views
210 // are currently focusable.
211 break;
212 }
213 }
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700214 }
215 return null;
216 }
217
218 private View findNextFocus(ViewGroup root, View focused, Rect focusedRect,
219 int direction, ArrayList<View> focusables) {
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700220 if (focused != null) {
Svetoslav Ganov951bb422012-04-30 13:15:21 -0700221 if (focusedRect == null) {
222 focusedRect = mFocusedRect;
223 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800224 // fill in interesting rect from focused
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700225 focused.getFocusedRect(focusedRect);
226 root.offsetDescendantRectToMyCoords(focused, focusedRect);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800227 } else {
Svetoslav Ganov951bb422012-04-30 13:15:21 -0700228 if (focusedRect == null) {
229 focusedRect = mFocusedRect;
230 // make up a rect at top left or bottom right of root
Svetoslav Ganov27e2da72012-07-02 18:12:00 -0700231 switch (direction) {
Svetoslav Ganov951bb422012-04-30 13:15:21 -0700232 case View.FOCUS_RIGHT:
233 case View.FOCUS_DOWN:
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700234 setFocusTopLeft(root, focusedRect);
Svetoslav Ganov951bb422012-04-30 13:15:21 -0700235 break;
236 case View.FOCUS_FORWARD:
237 if (root.isLayoutRtl()) {
238 setFocusBottomRight(root, focusedRect);
239 } else {
240 setFocusTopLeft(root, focusedRect);
241 }
242 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800243
Svetoslav Ganov951bb422012-04-30 13:15:21 -0700244 case View.FOCUS_LEFT:
245 case View.FOCUS_UP:
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700246 setFocusBottomRight(root, focusedRect);
Svetoslav Ganov951bb422012-04-30 13:15:21 -0700247 break;
248 case View.FOCUS_BACKWARD:
249 if (root.isLayoutRtl()) {
250 setFocusTopLeft(root, focusedRect);
251 } else {
252 setFocusBottomRight(root, focusedRect);
253 break;
254 }
Fabrice Di Meglio702e8f92012-03-23 18:09:20 -0700255 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800256 }
257 }
Svetoslav Ganov42138042012-03-20 11:51:39 -0700258
Svetoslav Ganov27e2da72012-07-02 18:12:00 -0700259 switch (direction) {
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700260 case View.FOCUS_FORWARD:
261 case View.FOCUS_BACKWARD:
Svetoslav Ganov27e2da72012-07-02 18:12:00 -0700262 return findNextFocusInRelativeDirection(focusables, root, focused, focusedRect,
263 direction);
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700264 case View.FOCUS_UP:
265 case View.FOCUS_DOWN:
266 case View.FOCUS_LEFT:
267 case View.FOCUS_RIGHT:
Svetoslav Ganov27e2da72012-07-02 18:12:00 -0700268 return findNextFocusInAbsoluteDirection(focusables, root, focused,
269 focusedRect, direction);
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700270 default:
Svetoslav Ganov27e2da72012-07-02 18:12:00 -0700271 throw new IllegalArgumentException("Unknown direction: " + direction);
Svetoslav Ganov42138042012-03-20 11:51:39 -0700272 }
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700273 }
Svetoslav Ganov42138042012-03-20 11:51:39 -0700274
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800275 private View findNextKeyboardNavigationCluster(
Vadim Tryshev8957f2d2016-12-21 19:22:26 -0800276 View root,
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800277 View currentCluster,
278 List<View> clusters,
Evan Rosky57223312017-02-08 14:42:45 -0800279 @View.FocusDirection int direction) {
Evan Roskybd10c522017-03-27 15:50:38 -0700280 try {
281 // Note: This sort is stable.
Evan Rosky84485232017-04-06 18:48:33 -0700282 mUserSpecifiedClusterComparator.setFocusables(clusters, root);
Evan Roskybd10c522017-03-27 15:50:38 -0700283 Collections.sort(clusters, mUserSpecifiedClusterComparator);
284 } finally {
285 mUserSpecifiedClusterComparator.recycle();
286 }
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800287 final int count = clusters.size();
Vadim Tryshev01b0c9e2016-11-21 15:25:01 -0800288
289 switch (direction) {
290 case View.FOCUS_FORWARD:
291 case View.FOCUS_DOWN:
292 case View.FOCUS_RIGHT:
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800293 return getNextKeyboardNavigationCluster(root, currentCluster, clusters, count);
Vadim Tryshev01b0c9e2016-11-21 15:25:01 -0800294 case View.FOCUS_BACKWARD:
295 case View.FOCUS_UP:
296 case View.FOCUS_LEFT:
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800297 return getPreviousKeyboardNavigationCluster(root, currentCluster, clusters, count);
Vadim Tryshev01b0c9e2016-11-21 15:25:01 -0800298 default:
299 throw new IllegalArgumentException("Unknown direction: " + direction);
300 }
301 }
302
Svetoslav Ganov27e2da72012-07-02 18:12:00 -0700303 private View findNextFocusInRelativeDirection(ArrayList<View> focusables, ViewGroup root,
Svetoslav Ganov42138042012-03-20 11:51:39 -0700304 View focused, Rect focusedRect, int direction) {
305 try {
306 // Note: This sort is stable.
Evan Rosky84485232017-04-06 18:48:33 -0700307 mUserSpecifiedFocusComparator.setFocusables(focusables, root);
Evan Rosky3b94bf52017-01-10 17:05:28 -0800308 Collections.sort(focusables, mUserSpecifiedFocusComparator);
Svetoslav Ganov42138042012-03-20 11:51:39 -0700309 } finally {
Evan Rosky3b94bf52017-01-10 17:05:28 -0800310 mUserSpecifiedFocusComparator.recycle();
Svetoslav Ganov42138042012-03-20 11:51:39 -0700311 }
312
313 final int count = focusables.size();
314 switch (direction) {
315 case View.FOCUS_FORWARD:
Fabrice Di Meglio3167c882013-05-07 15:39:55 -0700316 return getNextFocusable(focused, focusables, count);
Svetoslav Ganov42138042012-03-20 11:51:39 -0700317 case View.FOCUS_BACKWARD:
Fabrice Di Meglio3167c882013-05-07 15:39:55 -0700318 return getPreviousFocusable(focused, focusables, count);
Svetoslav Ganov42138042012-03-20 11:51:39 -0700319 }
320 return focusables.get(count - 1);
321 }
322
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700323 private void setFocusBottomRight(ViewGroup root, Rect focusedRect) {
Fabrice Di Meglio702e8f92012-03-23 18:09:20 -0700324 final int rootBottom = root.getScrollY() + root.getHeight();
325 final int rootRight = root.getScrollX() + root.getWidth();
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700326 focusedRect.set(rootRight, rootBottom, rootRight, rootBottom);
Fabrice Di Meglio702e8f92012-03-23 18:09:20 -0700327 }
328
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700329 private void setFocusTopLeft(ViewGroup root, Rect focusedRect) {
Fabrice Di Meglio702e8f92012-03-23 18:09:20 -0700330 final int rootTop = root.getScrollY();
331 final int rootLeft = root.getScrollX();
Svetoslav Ganov76f287e2012-04-23 11:02:36 -0700332 focusedRect.set(rootLeft, rootTop, rootLeft, rootTop);
Fabrice Di Meglio702e8f92012-03-23 18:09:20 -0700333 }
334
Svetoslav Ganov27e2da72012-07-02 18:12:00 -0700335 View findNextFocusInAbsoluteDirection(ArrayList<View> focusables, ViewGroup root, View focused,
Svetoslav Ganov42138042012-03-20 11:51:39 -0700336 Rect focusedRect, int direction) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800337 // initialize the best candidate to something impossible
338 // (so the first plausible view will become the best choice)
339 mBestCandidateRect.set(focusedRect);
340 switch(direction) {
341 case View.FOCUS_LEFT:
342 mBestCandidateRect.offset(focusedRect.width() + 1, 0);
343 break;
344 case View.FOCUS_RIGHT:
345 mBestCandidateRect.offset(-(focusedRect.width() + 1), 0);
346 break;
347 case View.FOCUS_UP:
348 mBestCandidateRect.offset(0, focusedRect.height() + 1);
349 break;
350 case View.FOCUS_DOWN:
351 mBestCandidateRect.offset(0, -(focusedRect.height() + 1));
352 }
353
354 View closest = null;
355
356 int numFocusables = focusables.size();
357 for (int i = 0; i < numFocusables; i++) {
358 View focusable = focusables.get(i);
359
360 // only interested in other non-root views
361 if (focusable == focused || focusable == root) continue;
362
Tobias Duboisdefdb1e2010-12-15 11:35:30 +0100363 // get focus bounds of other view in same coordinate system
Fabrice Di Meglioc11f77f2012-09-18 15:33:07 -0700364 focusable.getFocusedRect(mOtherRect);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800365 root.offsetDescendantRectToMyCoords(focusable, mOtherRect);
366
367 if (isBetterCandidate(direction, focusedRect, mOtherRect, mBestCandidateRect)) {
368 mBestCandidateRect.set(mOtherRect);
369 closest = focusable;
370 }
371 }
372 return closest;
373 }
374
Fabrice Di Meglio7e0a3722012-03-27 16:06:25 -0700375 private static View getNextFocusable(View focused, ArrayList<View> focusables, int count) {
Fabrice Di Meglio702e8f92012-03-23 18:09:20 -0700376 if (focused != null) {
377 int position = focusables.lastIndexOf(focused);
378 if (position >= 0 && position + 1 < count) {
379 return focusables.get(position + 1);
380 }
381 }
Svetoslav Ganove5dfa47d2012-05-08 15:58:32 -0700382 if (!focusables.isEmpty()) {
383 return focusables.get(0);
384 }
385 return null;
Fabrice Di Meglio702e8f92012-03-23 18:09:20 -0700386 }
387
Fabrice Di Meglio7e0a3722012-03-27 16:06:25 -0700388 private static View getPreviousFocusable(View focused, ArrayList<View> focusables, int count) {
Fabrice Di Meglio702e8f92012-03-23 18:09:20 -0700389 if (focused != null) {
390 int position = focusables.indexOf(focused);
391 if (position > 0) {
392 return focusables.get(position - 1);
393 }
394 }
Svetoslav Ganove5dfa47d2012-05-08 15:58:32 -0700395 if (!focusables.isEmpty()) {
396 return focusables.get(count - 1);
397 }
398 return null;
Fabrice Di Meglio702e8f92012-03-23 18:09:20 -0700399 }
400
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800401 private static View getNextKeyboardNavigationCluster(
Vadim Tryshev8957f2d2016-12-21 19:22:26 -0800402 View root,
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800403 View currentCluster,
404 List<View> clusters,
Vadim Tryshev8957f2d2016-12-21 19:22:26 -0800405 int count) {
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800406 if (currentCluster == null) {
407 // The current cluster is the default one.
408 // The next cluster after the default one is the first one.
409 // Note that the caller guarantees that 'clusters' is not empty.
410 return clusters.get(0);
Vadim Tryshev01b0c9e2016-11-21 15:25:01 -0800411 }
412
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800413 final int position = clusters.lastIndexOf(currentCluster);
Vadim Tryshev01b0c9e2016-11-21 15:25:01 -0800414 if (position >= 0 && position + 1 < count) {
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800415 // Return the next non-default cluster if we can find it.
416 return clusters.get(position + 1);
Vadim Tryshev01b0c9e2016-11-21 15:25:01 -0800417 }
418
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800419 // The current cluster is the last one. The next one is the default one, i.e. the
420 // root.
421 return root;
Vadim Tryshev01b0c9e2016-11-21 15:25:01 -0800422 }
423
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800424 private static View getPreviousKeyboardNavigationCluster(
Vadim Tryshev8957f2d2016-12-21 19:22:26 -0800425 View root,
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800426 View currentCluster,
427 List<View> clusters,
Vadim Tryshev8957f2d2016-12-21 19:22:26 -0800428 int count) {
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800429 if (currentCluster == null) {
430 // The current cluster is the default one.
431 // The previous cluster before the default one is the last one.
432 // Note that the caller guarantees that 'clusters' is not empty.
433 return clusters.get(count - 1);
Vadim Tryshev01b0c9e2016-11-21 15:25:01 -0800434 }
435
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800436 final int position = clusters.indexOf(currentCluster);
Vadim Tryshev01b0c9e2016-11-21 15:25:01 -0800437 if (position > 0) {
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800438 // Return the previous non-default cluster if we can find it.
439 return clusters.get(position - 1);
Vadim Tryshev01b0c9e2016-11-21 15:25:01 -0800440 }
441
Vadim Tryshevb5ced222017-01-17 19:31:35 -0800442 // The current cluster is the first one. The previous one is the default one, i.e.
443 // the root.
444 return root;
Vadim Tryshev01b0c9e2016-11-21 15:25:01 -0800445 }
446
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800447 /**
448 * Is rect1 a better candidate than rect2 for a focus search in a particular
449 * direction from a source rect? This is the core routine that determines
450 * the order of focus searching.
451 * @param direction the direction (up, down, left, right)
452 * @param source The source we are searching from
453 * @param rect1 The candidate rectangle
454 * @param rect2 The current best candidate.
455 * @return Whether the candidate is the new best.
456 */
457 boolean isBetterCandidate(int direction, Rect source, Rect rect1, Rect rect2) {
458
459 // to be a better candidate, need to at least be a candidate in the first
460 // place :)
461 if (!isCandidate(source, rect1, direction)) {
462 return false;
463 }
464
465 // we know that rect1 is a candidate.. if rect2 is not a candidate,
466 // rect1 is better
467 if (!isCandidate(source, rect2, direction)) {
468 return true;
469 }
470
471 // if rect1 is better by beam, it wins
472 if (beamBeats(direction, source, rect1, rect2)) {
473 return true;
474 }
475
476 // if rect2 is better, then rect1 cant' be :)
477 if (beamBeats(direction, source, rect2, rect1)) {
478 return false;
479 }
480
481 // otherwise, do fudge-tastic comparison of the major and minor axis
482 return (getWeightedDistanceFor(
483 majorAxisDistance(direction, source, rect1),
484 minorAxisDistance(direction, source, rect1))
485 < getWeightedDistanceFor(
486 majorAxisDistance(direction, source, rect2),
487 minorAxisDistance(direction, source, rect2)));
488 }
489
490 /**
491 * One rectangle may be another candidate than another by virtue of being
492 * exclusively in the beam of the source rect.
493 * @return Whether rect1 is a better candidate than rect2 by virtue of it being in src's
494 * beam
495 */
496 boolean beamBeats(int direction, Rect source, Rect rect1, Rect rect2) {
497 final boolean rect1InSrcBeam = beamsOverlap(direction, source, rect1);
498 final boolean rect2InSrcBeam = beamsOverlap(direction, source, rect2);
499
500 // if rect1 isn't exclusively in the src beam, it doesn't win
501 if (rect2InSrcBeam || !rect1InSrcBeam) {
502 return false;
503 }
504
505 // we know rect1 is in the beam, and rect2 is not
506
507 // if rect1 is to the direction of, and rect2 is not, rect1 wins.
508 // for example, for direction left, if rect1 is to the left of the source
509 // and rect2 is below, then we always prefer the in beam rect1, since rect2
510 // could be reached by going down.
511 if (!isToDirectionOf(direction, source, rect2)) {
512 return true;
513 }
514
515 // for horizontal directions, being exclusively in beam always wins
516 if ((direction == View.FOCUS_LEFT || direction == View.FOCUS_RIGHT)) {
517 return true;
518 }
519
520 // for vertical directions, beams only beat up to a point:
521 // now, as long as rect2 isn't completely closer, rect1 wins
522 // e.g for direction down, completely closer means for rect2's top
523 // edge to be closer to the source's top edge than rect1's bottom edge.
524 return (majorAxisDistance(direction, source, rect1)
525 < majorAxisDistanceToFarEdge(direction, source, rect2));
526 }
527
528 /**
529 * Fudge-factor opportunity: how to calculate distance given major and minor
530 * axis distances. Warning: this fudge factor is finely tuned, be sure to
531 * run all focus tests if you dare tweak it.
532 */
Niklas Brunlid2ecbe692017-11-02 09:26:37 +0100533 long getWeightedDistanceFor(long majorAxisDistance, long minorAxisDistance) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800534 return 13 * majorAxisDistance * majorAxisDistance
535 + minorAxisDistance * minorAxisDistance;
536 }
537
538 /**
539 * Is destRect a candidate for the next focus given the direction? This
540 * checks whether the dest is at least partially to the direction of (e.g left of)
541 * from source.
542 *
543 * Includes an edge case for an empty rect (which is used in some cases when
544 * searching from a point on the screen).
545 */
546 boolean isCandidate(Rect srcRect, Rect destRect, int direction) {
547 switch (direction) {
548 case View.FOCUS_LEFT:
549 return (srcRect.right > destRect.right || srcRect.left >= destRect.right)
550 && srcRect.left > destRect.left;
551 case View.FOCUS_RIGHT:
552 return (srcRect.left < destRect.left || srcRect.right <= destRect.left)
553 && srcRect.right < destRect.right;
554 case View.FOCUS_UP:
555 return (srcRect.bottom > destRect.bottom || srcRect.top >= destRect.bottom)
556 && srcRect.top > destRect.top;
557 case View.FOCUS_DOWN:
558 return (srcRect.top < destRect.top || srcRect.bottom <= destRect.top)
559 && srcRect.bottom < destRect.bottom;
560 }
561 throw new IllegalArgumentException("direction must be one of "
562 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
563 }
564
565
566 /**
Fabrice Di Meglio7e0a3722012-03-27 16:06:25 -0700567 * Do the "beams" w.r.t the given direction's axis of rect1 and rect2 overlap?
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800568 * @param direction the direction (up, down, left, right)
569 * @param rect1 The first rectangle
570 * @param rect2 The second rectangle
571 * @return whether the beams overlap
572 */
573 boolean beamsOverlap(int direction, Rect rect1, Rect rect2) {
574 switch (direction) {
575 case View.FOCUS_LEFT:
576 case View.FOCUS_RIGHT:
Evan Rosky815fb1f2017-07-10 16:14:15 -0700577 return (rect2.bottom > rect1.top) && (rect2.top < rect1.bottom);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800578 case View.FOCUS_UP:
579 case View.FOCUS_DOWN:
Evan Rosky815fb1f2017-07-10 16:14:15 -0700580 return (rect2.right > rect1.left) && (rect2.left < rect1.right);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800581 }
582 throw new IllegalArgumentException("direction must be one of "
583 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
584 }
585
586 /**
587 * e.g for left, is 'to left of'
588 */
589 boolean isToDirectionOf(int direction, Rect src, Rect dest) {
590 switch (direction) {
591 case View.FOCUS_LEFT:
592 return src.left >= dest.right;
593 case View.FOCUS_RIGHT:
594 return src.right <= dest.left;
595 case View.FOCUS_UP:
596 return src.top >= dest.bottom;
597 case View.FOCUS_DOWN:
598 return src.bottom <= dest.top;
599 }
600 throw new IllegalArgumentException("direction must be one of "
601 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
602 }
603
604 /**
605 * @return The distance from the edge furthest in the given direction
606 * of source to the edge nearest in the given direction of dest. If the
607 * dest is not in the direction from source, return 0.
608 */
609 static int majorAxisDistance(int direction, Rect source, Rect dest) {
610 return Math.max(0, majorAxisDistanceRaw(direction, source, dest));
611 }
612
613 static int majorAxisDistanceRaw(int direction, Rect source, Rect dest) {
614 switch (direction) {
615 case View.FOCUS_LEFT:
616 return source.left - dest.right;
617 case View.FOCUS_RIGHT:
618 return dest.left - source.right;
619 case View.FOCUS_UP:
620 return source.top - dest.bottom;
621 case View.FOCUS_DOWN:
622 return dest.top - source.bottom;
623 }
624 throw new IllegalArgumentException("direction must be one of "
625 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
626 }
627
628 /**
629 * @return The distance along the major axis w.r.t the direction from the
630 * edge of source to the far edge of dest. If the
631 * dest is not in the direction from source, return 1 (to break ties with
632 * {@link #majorAxisDistance}).
633 */
634 static int majorAxisDistanceToFarEdge(int direction, Rect source, Rect dest) {
635 return Math.max(1, majorAxisDistanceToFarEdgeRaw(direction, source, dest));
636 }
637
638 static int majorAxisDistanceToFarEdgeRaw(int direction, Rect source, Rect dest) {
639 switch (direction) {
640 case View.FOCUS_LEFT:
641 return source.left - dest.left;
642 case View.FOCUS_RIGHT:
643 return dest.right - source.right;
644 case View.FOCUS_UP:
645 return source.top - dest.top;
646 case View.FOCUS_DOWN:
647 return dest.bottom - source.bottom;
648 }
649 throw new IllegalArgumentException("direction must be one of "
650 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
651 }
652
653 /**
654 * Find the distance on the minor axis w.r.t the direction to the nearest
Fabrice Di Meglio7e0a3722012-03-27 16:06:25 -0700655 * edge of the destination rectangle.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800656 * @param direction the direction (up, down, left, right)
657 * @param source The source rect.
658 * @param dest The destination rect.
659 * @return The distance.
660 */
661 static int minorAxisDistance(int direction, Rect source, Rect dest) {
662 switch (direction) {
663 case View.FOCUS_LEFT:
664 case View.FOCUS_RIGHT:
665 // the distance between the center verticals
666 return Math.abs(
667 ((source.top + source.height() / 2) -
668 ((dest.top + dest.height() / 2))));
669 case View.FOCUS_UP:
670 case View.FOCUS_DOWN:
671 // the distance between the center horizontals
672 return Math.abs(
673 ((source.left + source.width() / 2) -
674 ((dest.left + dest.width() / 2))));
675 }
676 throw new IllegalArgumentException("direction must be one of "
677 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
678 }
679
680 /**
681 * Find the nearest touchable view to the specified view.
682 *
683 * @param root The root of the tree in which to search
684 * @param x X coordinate from which to start the search
685 * @param y Y coordinate from which to start the search
686 * @param direction Direction to look
687 * @param deltas Offset from the <x, y> to the edge of the nearest view. Note that this array
688 * may already be populated with values.
689 * @return The nearest touchable view, or null if none exists.
690 */
691 public View findNearestTouchable(ViewGroup root, int x, int y, int direction, int[] deltas) {
692 ArrayList<View> touchables = root.getTouchables();
693 int minDistance = Integer.MAX_VALUE;
694 View closest = null;
695
696 int numTouchables = touchables.size();
697
698 int edgeSlop = ViewConfiguration.get(root.mContext).getScaledEdgeSlop();
699
700 Rect closestBounds = new Rect();
701 Rect touchableBounds = mOtherRect;
702
703 for (int i = 0; i < numTouchables; i++) {
704 View touchable = touchables.get(i);
705
706 // get visible bounds of other view in same coordinate system
707 touchable.getDrawingRect(touchableBounds);
708
709 root.offsetRectBetweenParentAndChild(touchable, touchableBounds, true, true);
710
711 if (!isTouchCandidate(x, y, touchableBounds, direction)) {
712 continue;
713 }
714
715 int distance = Integer.MAX_VALUE;
716
717 switch (direction) {
718 case View.FOCUS_LEFT:
719 distance = x - touchableBounds.right + 1;
720 break;
721 case View.FOCUS_RIGHT:
722 distance = touchableBounds.left;
723 break;
724 case View.FOCUS_UP:
725 distance = y - touchableBounds.bottom + 1;
726 break;
727 case View.FOCUS_DOWN:
728 distance = touchableBounds.top;
729 break;
730 }
731
732 if (distance < edgeSlop) {
733 // Give preference to innermost views
734 if (closest == null ||
735 closestBounds.contains(touchableBounds) ||
736 (!touchableBounds.contains(closestBounds) && distance < minDistance)) {
737 minDistance = distance;
738 closest = touchable;
739 closestBounds.set(touchableBounds);
740 switch (direction) {
741 case View.FOCUS_LEFT:
742 deltas[0] = -distance;
743 break;
744 case View.FOCUS_RIGHT:
745 deltas[0] = distance;
746 break;
747 case View.FOCUS_UP:
748 deltas[1] = -distance;
749 break;
750 case View.FOCUS_DOWN:
751 deltas[1] = distance;
752 break;
753 }
754 }
755 }
756 }
757 return closest;
758 }
759
760
761 /**
762 * Is destRect a candidate for the next touch given the direction?
763 */
764 private boolean isTouchCandidate(int x, int y, Rect destRect, int direction) {
765 switch (direction) {
766 case View.FOCUS_LEFT:
767 return destRect.left <= x && destRect.top <= y && y <= destRect.bottom;
768 case View.FOCUS_RIGHT:
769 return destRect.left >= x && destRect.top <= y && y <= destRect.bottom;
770 case View.FOCUS_UP:
771 return destRect.top <= y && destRect.left <= x && x <= destRect.right;
772 case View.FOCUS_DOWN:
773 return destRect.top >= y && destRect.left <= x && x <= destRect.right;
774 }
775 throw new IllegalArgumentException("direction must be one of "
776 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
777 }
Jeff Brown4e6319b2010-12-13 10:36:51 -0800778
George Mount140ea622015-10-27 08:46:44 -0700779 private static final boolean isValidId(final int id) {
780 return id != 0 && id != View.NO_ID;
781 }
782
Evan Roskyd114e0f2017-03-23 11:20:04 -0700783 static final class FocusSorter {
784 private ArrayList<Rect> mRectPool = new ArrayList<>();
785 private int mLastPoolRect;
786 private int mRtlMult;
787 private HashMap<View, Rect> mRectByView = null;
Evan Rosky3b94bf52017-01-10 17:05:28 -0800788
Evan Roskyd114e0f2017-03-23 11:20:04 -0700789 private Comparator<View> mTopsComparator = (first, second) -> {
Evan Rosky3b94bf52017-01-10 17:05:28 -0800790 if (first == second) {
791 return 0;
792 }
793
Evan Roskyd114e0f2017-03-23 11:20:04 -0700794 Rect firstRect = mRectByView.get(first);
795 Rect secondRect = mRectByView.get(second);
Evan Rosky3b94bf52017-01-10 17:05:28 -0800796
Evan Roskyd114e0f2017-03-23 11:20:04 -0700797 int result = firstRect.top - secondRect.top;
798 if (result == 0) {
799 return firstRect.bottom - secondRect.bottom;
800 }
801 return result;
802 };
803
804 private Comparator<View> mSidesComparator = (first, second) -> {
805 if (first == second) {
Evan Roskye4667522017-03-09 20:32:49 +0000806 return 0;
Evan Rosky3b94bf52017-01-10 17:05:28 -0800807 }
Evan Rosky3b94bf52017-01-10 17:05:28 -0800808
Evan Roskyd114e0f2017-03-23 11:20:04 -0700809 Rect firstRect = mRectByView.get(first);
810 Rect secondRect = mRectByView.get(second);
811
812 int result = firstRect.left - secondRect.left;
813 if (result == 0) {
814 return firstRect.right - secondRect.right;
815 }
816 return mRtlMult * result;
817 };
818
819 public void sort(View[] views, int start, int end, ViewGroup root, boolean isRtl) {
820 int count = end - start;
821 if (count < 2) {
822 return;
823 }
824 if (mRectByView == null) {
825 mRectByView = new HashMap<>();
826 }
827 mRtlMult = isRtl ? -1 : 1;
828 for (int i = mRectPool.size(); i < count; ++i) {
829 mRectPool.add(new Rect());
830 }
831 for (int i = start; i < end; ++i) {
832 Rect next = mRectPool.get(mLastPoolRect++);
833 views[i].getDrawingRect(next);
834 root.offsetDescendantRectToMyCoords(views[i], next);
835 mRectByView.put(views[i], next);
836 }
837
838 // Sort top-to-bottom
839 Arrays.sort(views, start, count, mTopsComparator);
840 // Sweep top-to-bottom to identify rows
841 int sweepBottom = mRectByView.get(views[start]).bottom;
842 int rowStart = start;
843 int sweepIdx = start + 1;
844 for (; sweepIdx < end; ++sweepIdx) {
845 Rect currRect = mRectByView.get(views[sweepIdx]);
846 if (currRect.top >= sweepBottom) {
847 // Next view is on a new row, sort the row we've just finished left-to-right.
848 if ((sweepIdx - rowStart) > 1) {
849 Arrays.sort(views, rowStart, sweepIdx, mSidesComparator);
850 }
851 sweepBottom = currRect.bottom;
852 rowStart = sweepIdx;
853 } else {
854 // Next view vertically overlaps, we need to extend our "row height"
855 sweepBottom = Math.max(sweepBottom, currRect.bottom);
856 }
857 }
858 // Sort whatever's left (final row) left-to-right
859 if ((sweepIdx - rowStart) > 1) {
860 Arrays.sort(views, rowStart, sweepIdx, mSidesComparator);
861 }
862
863 mLastPoolRect = 0;
864 mRectByView.clear();
Evan Rosky3b94bf52017-01-10 17:05:28 -0800865 }
866 }
867
868 /**
Evan Roskyd114e0f2017-03-23 11:20:04 -0700869 * Public for testing.
870 *
871 * @hide
872 */
873 @TestApi
874 public static void sort(View[] views, int start, int end, ViewGroup root, boolean isRtl) {
875 getInstance().mFocusSorter.sort(views, start, end, root, isRtl);
876 }
877
878 /**
Evan Rosky3b94bf52017-01-10 17:05:28 -0800879 * Sorts views according to any explicitly-specified focus-chains. If there are no explicitly
880 * specified focus chains (eg. no nextFocusForward attributes defined), this should be a no-op.
881 */
882 private static final class UserSpecifiedFocusComparator implements Comparator<View> {
Evan Rosky84485232017-04-06 18:48:33 -0700883 private final ArrayMap<View, View> mNextFoci = new ArrayMap<>();
884 private final ArraySet<View> mIsConnectedTo = new ArraySet<>();
Evan Rosky3b94bf52017-01-10 17:05:28 -0800885 private final ArrayMap<View, View> mHeadsOfChains = new ArrayMap<View, View>();
886 private final ArrayMap<View, Integer> mOriginalOrdinal = new ArrayMap<>();
Evan Rosky84485232017-04-06 18:48:33 -0700887 private final NextFocusGetter mNextFocusGetter;
888 private View mRoot;
Evan Roskybd10c522017-03-27 15:50:38 -0700889
Evan Rosky84485232017-04-06 18:48:33 -0700890 public interface NextFocusGetter {
891 View get(View root, View view);
Evan Roskybd10c522017-03-27 15:50:38 -0700892 }
893
Evan Rosky84485232017-04-06 18:48:33 -0700894 UserSpecifiedFocusComparator(NextFocusGetter nextFocusGetter) {
895 mNextFocusGetter = nextFocusGetter;
Evan Roskybd10c522017-03-27 15:50:38 -0700896 }
Evan Rosky3b94bf52017-01-10 17:05:28 -0800897
898 public void recycle() {
Evan Rosky84485232017-04-06 18:48:33 -0700899 mRoot = null;
Evan Rosky3b94bf52017-01-10 17:05:28 -0800900 mHeadsOfChains.clear();
901 mIsConnectedTo.clear();
902 mOriginalOrdinal.clear();
Evan Rosky84485232017-04-06 18:48:33 -0700903 mNextFoci.clear();
Fabrice Di Meglio3167c882013-05-07 15:39:55 -0700904 }
905
Evan Rosky84485232017-04-06 18:48:33 -0700906 public void setFocusables(List<View> focusables, View root) {
907 mRoot = root;
Evan Rosky3b94bf52017-01-10 17:05:28 -0800908 for (int i = 0; i < focusables.size(); ++i) {
909 mOriginalOrdinal.put(focusables.get(i), i);
910 }
Evan Rosky84485232017-04-06 18:48:33 -0700911
912 for (int i = focusables.size() - 1; i >= 0; i--) {
913 final View view = focusables.get(i);
914 final View next = mNextFocusGetter.get(mRoot, view);
915 if (next != null && mOriginalOrdinal.containsKey(next)) {
916 mNextFoci.put(view, next);
917 mIsConnectedTo.add(next);
918 }
919 }
920
921 for (int i = focusables.size() - 1; i >= 0; i--) {
922 final View view = focusables.get(i);
923 final View next = mNextFoci.get(view);
924 if (next != null && !mIsConnectedTo.contains(view)) {
925 setHeadOfChain(view);
926 }
927 }
George Mount140ea622015-10-27 08:46:44 -0700928 }
929
930 private void setHeadOfChain(View head) {
Evan Rosky84485232017-04-06 18:48:33 -0700931 for (View view = head; view != null; view = mNextFoci.get(view)) {
George Mount140ea622015-10-27 08:46:44 -0700932 final View otherHead = mHeadsOfChains.get(view);
933 if (otherHead != null) {
934 if (otherHead == head) {
935 return; // This view has already had its head set properly
936 }
937 // A hydra -- multi-headed focus chain (e.g. A->C and B->C)
938 // Use the one we've already chosen instead and reset this chain.
939 view = head;
940 head = otherHead;
941 }
942 mHeadsOfChains.put(view, head);
943 }
944 }
945
Jeff Brown4e6319b2010-12-13 10:36:51 -0800946 public int compare(View first, View second) {
947 if (first == second) {
948 return 0;
949 }
George Mount140ea622015-10-27 08:46:44 -0700950 // Order between views within a chain is immaterial -- next/previous is
951 // within a chain is handled elsewhere.
952 View firstHead = mHeadsOfChains.get(first);
953 View secondHead = mHeadsOfChains.get(second);
954 if (firstHead == secondHead && firstHead != null) {
955 if (first == firstHead) {
956 return -1; // first is the head, it should be first
957 } else if (second == firstHead) {
958 return 1; // second is the head, it should be first
Evan Rosky84485232017-04-06 18:48:33 -0700959 } else if (mNextFoci.get(first) != null) {
George Mount140ea622015-10-27 08:46:44 -0700960 return -1; // first is not the end of the chain
961 } else {
962 return 1; // first is end of chain
963 }
964 }
Evan Rosky3b94bf52017-01-10 17:05:28 -0800965 boolean involvesChain = false;
George Mount140ea622015-10-27 08:46:44 -0700966 if (firstHead != null) {
967 first = firstHead;
Evan Rosky3b94bf52017-01-10 17:05:28 -0800968 involvesChain = true;
George Mount140ea622015-10-27 08:46:44 -0700969 }
970 if (secondHead != null) {
971 second = secondHead;
Evan Rosky3b94bf52017-01-10 17:05:28 -0800972 involvesChain = true;
George Mount140ea622015-10-27 08:46:44 -0700973 }
Jeff Brown4e6319b2010-12-13 10:36:51 -0800974
Evan Rosky3b94bf52017-01-10 17:05:28 -0800975 if (involvesChain) {
976 // keep original order between chains
977 return mOriginalOrdinal.get(first) < mOriginalOrdinal.get(second) ? -1 : 1;
Jeff Brown4e6319b2010-12-13 10:36:51 -0800978 } else {
Jeff Brown4e6319b2010-12-13 10:36:51 -0800979 return 0;
980 }
981 }
Jeff Brown4e6319b2010-12-13 10:36:51 -0800982 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800983}